diff --git a/.ci.yaml b/.ci.yaml index 64a4de522fd43..48874999b2155 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -50,7 +50,8 @@ platform_properties: ] os: Linux device_type: "msm8952" - linux_samsung_s10: + + linux_pixel_7pro: properties: dependencies: >- [ @@ -59,7 +60,8 @@ platform_properties: {"dependency": "curl", "version": "version:7.64.0"} ] os: Linux - device_type: "SM-G973U1" + device_type: "Pixel 7 Pro" + linux_samsung_a02: properties: dependencies: >- @@ -70,11 +72,12 @@ platform_properties: ] os: Linux device_type: "SM-A025V" + mac: properties: dependencies: >- [ - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] os: Mac-12 device_type: none @@ -86,7 +89,7 @@ platform_properties: properties: dependencies: >- [ - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] os: Mac-12 device_type: none @@ -99,7 +102,7 @@ platform_properties: properties: dependencies: >- [ - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] device_type: none mac_model: "Macmini8,1" @@ -114,7 +117,7 @@ platform_properties: properties: dependencies: >- [ - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] os: Mac-12 device_type: none @@ -128,7 +131,7 @@ platform_properties: dependencies: >- [ {"dependency": "gems", "version": "v3.3.14"}, - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] os: Mac-12 device_type: none @@ -142,7 +145,7 @@ platform_properties: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] os: Mac-12 @@ -163,7 +166,7 @@ platform_properties: dependencies: >- [ {"dependency": "gems", "version": "v3.3.14"}, - {"dependency": "apple_signing", "version": "version:2022_to_2023"} + {"dependency": "apple_signing", "version": "version:to_2024"} ] os: Mac-12 cpu: x86 @@ -194,13 +197,23 @@ platform_properties: ] os: Windows-10 device_type: none + windows_arm64: + properties: + # The arch can be removed after https://github.com/flutter/flutter/issues/135722. + arch: arm + dependencies: >- + [ + {"dependency": "certs", "version": "version:9563bb"} + ] + os: Windows + cpu: arm64 windows_android: properties: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, {"dependency": "certs", "version": "version:9563bb"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] os: Windows-10 @@ -234,17 +247,18 @@ targets: task_name: analyzer_benchmark - name: Linux coverage - bringup: true + presubmit: false recipe: flutter/coverage timeout: 120 + enabled_branches: + # Don't run this on release branches + - master properties: tags: > ["framework", "hostonly", "shard", "linux"] - name: Linux packages_autoroller presubmit: false - # TODO(fujino): https://github.com/flutter/flutter/issues/129744 - bringup: true recipe: pub_autoroller/pub_autoroller timeout: 30 enabled_branches: @@ -264,7 +278,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "android_virtual_device", "version": "33"}, + {"dependency": "android_virtual_device", "version": "34"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -278,7 +292,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, @@ -297,7 +311,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, @@ -316,7 +330,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, @@ -334,6 +348,7 @@ targets: properties: tags: > ["framework", "hostonly", "shard", "linux"] + backfill: "false" runIf: - .ci.yaml @@ -363,6 +378,7 @@ targets: ] tags: > ["framework", "hostonly", "linux"] + backfill: "false" validation: docs validation_name: Docs firebase_project: master-docs-flutter-dev @@ -402,7 +418,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -413,26 +429,9 @@ targets: - bin/** - .ci.yaml - - name: Linux firebase_oriol33_abstract_method_smoke_test - bringup: true - recipe: firebaselab/firebaselab - timeout: 60 - properties: - dependencies: >- - [ - {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "open_jdk", "version": "version:11"} - ] - tags: > - ["firebaselab"] - task_name: abstract_method_smoke_test - physical_devices: >- - ["--device", "model=oriole,version=33"] - virtual_devices: >- - [] - - name: Linux firebase_abstract_method_smoke_test - bringup: true # Flaky https://github.com/flutter/flutter/issues/124691 + bringup: false + presubmit: false recipe: firebaselab/firebaselab timeout: 60 properties: @@ -446,8 +445,8 @@ targets: task_name: abstract_method_smoke_test physical_devices: >- [ - "--device", "model=redfin,version=30", - "--device", "model=griffin,version=24" + "--device", "model=panther,version=33", + "--device", "model=redfin,version=30" ] # TODO(flutter/flutter#123331): This device is flaking. # "--device", "model=Nexus6P,version=25", @@ -456,6 +455,7 @@ targets: "--device", "model=Nexus5,version=21", "--device", "model=Nexus5,version=22", "--device", "model=Nexus5,version=23", + "--device", "model=Nexus5,version=24", "--device", "model=Nexus6P,version=26", "--device", "model=Nexus6P,version=27", "--device", "model=NexusLowRes,version=29" @@ -573,7 +573,8 @@ targets: properties: dependencies: >- [ - {"dependency": "android_sdk", "version": "version:33v6"} + {"dependency": "android_sdk", "version": "version:33v6"}, + {"dependency": "open_jdk", "version": "version:17"} ] shard: framework_tests subshard: slow @@ -602,7 +603,7 @@ targets: {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "open_jdk", "version": "version:11"}, + {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "android_sdk", "version": "version:33v6"} ] shard: framework_tests @@ -664,7 +665,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -682,7 +683,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -700,7 +701,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -718,7 +719,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -736,7 +737,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -754,7 +755,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -773,7 +774,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"} + {"dependency": "chrome_and_driver", "version": "version:117.0"} ] tags: > ["devicelab", "hostonly", "linux"] @@ -791,7 +792,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -810,7 +811,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -829,7 +830,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -927,7 +928,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"} + {"dependency": "chrome_and_driver", "version": "version:117.0"} ] tags: > ["devicelab", "hostonly", "linux"] @@ -953,10 +954,12 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, - {"dependency": "open_jdk", "version": "version:11"}, - {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "open_jdk", "version": "version:11"} ] shard: tool_integration_tests subshard: "1_4" @@ -977,10 +980,12 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, - {"dependency": "open_jdk", "version": "version:11"}, - {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "open_jdk", "version": "version:11"} ] shard: tool_integration_tests subshard: "2_4" @@ -1001,10 +1006,12 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, - {"dependency": "open_jdk", "version": "version:11"}, - {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "open_jdk", "version": "version:11"} ] shard: tool_integration_tests subshard: "3_4" @@ -1025,10 +1032,12 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, - {"dependency": "open_jdk", "version": "version:11"}, - {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "open_jdk", "version": "version:11"} ] shard: tool_integration_tests subshard: "4_4" @@ -1089,7 +1098,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"} + {"dependency": "chrome_and_driver", "version": "version:117.0"} ] tags: > ["devicelab","hostonly", "linux"] @@ -1097,13 +1106,12 @@ targets: - name: Linux web_benchmarks_html recipe: devicelab/devicelab_drone - presubmit: false timeout: 60 properties: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"} + {"dependency": "chrome_and_driver", "version": "version:117.0"} ] tags: > ["devicelab"] @@ -1114,7 +1122,6 @@ targets: - .ci.yaml - name: Linux web_benchmarks_skwasm - bringup: true recipe: devicelab/devicelab_drone presubmit: false timeout: 60 @@ -1122,7 +1129,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"} + {"dependency": "chrome_and_driver", "version": "version:117.0"} ] tags: > ["devicelab"] @@ -1139,13 +1146,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_long_running_tests subshard: "1_5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1159,13 +1168,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_long_running_tests subshard: "2_5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1179,13 +1190,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_long_running_tests subshard: "3_5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1199,13 +1212,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_long_running_tests subshard: "4_5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1219,13 +1234,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_long_running_tests subshard: "5_5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1239,13 +1256,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "0" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1259,13 +1278,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "1" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1279,13 +1300,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "2" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1299,13 +1322,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "3" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1319,13 +1344,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "4" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1339,13 +1366,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1359,13 +1388,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "6" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1379,13 +1410,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_tests subshard: "7_last" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1399,13 +1432,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "0" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1419,13 +1454,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "1" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1439,13 +1476,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "2" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1459,13 +1498,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "3" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1479,13 +1520,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "4" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1499,13 +1542,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "5" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1519,13 +1564,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "6" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1539,13 +1586,15 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] shard: web_canvaskit_tests subshard: "7_last" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/** @@ -1559,7 +1608,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] @@ -1567,6 +1616,8 @@ targets: subshard: "1_1" tags: > ["framework", "hostonly", "shard", "linux"] + # Retry for flakes caused by https://github.com/flutter/flutter/issues/132654 + presubmit_max_attempts: "2" runIf: - dev/** - packages/flutter_tools/** @@ -1588,9 +1639,8 @@ targets: task_name: android_defines_test dependencies: >- [ - {"dependency": "android_virtual_device", "version": "33"} + {"dependency": "android_virtual_device", "version": "34"} ] - use_emulator: "true" - name: Linux_android android_obfuscate_test recipe: devicelab/devicelab_drone @@ -1674,15 +1724,73 @@ targets: ["devicelab", "android", "linux"] task_name: backdrop_filter_perf__e2e_summary - - name: Linux_samsung_s10 backdrop_filter_perf__timeline_summary + - name: Linux_pixel_7pro backdrop_filter_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: backdrop_filter_perf__timeline_summary + - name: Linux_pixel_7pro draw_atlas_perf_opengles__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: draw_atlas_perf_opengles__timeline_summary + + - name: Linux_pixel_7pro draw_atlas_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: draw_atlas_perf__timeline_summary + + - name: Linux_pixel_7pro dynamic_path_tessellation_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: dynamic_path_tessellation_perf__timeline_summary + + - name: Linux_pixel_7pro static_path_tessellation_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: static_path_tessellation_perf__timeline_summary + + - name: Linux_pixel_7pro hello_world_impeller + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + timeout: 60 + properties: + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: hello_world_impeller + - name: Linux_android basic_material_app_android__compile recipe: devicelab/devicelab_drone presubmit: false @@ -1807,13 +1915,13 @@ targets: {"dependency": "open_jdk", "version": "version:11"} ] - - name: Linux_samsung_s10 complex_layout_scroll_perf__timeline_summary + - name: Linux_pixel_7pro complex_layout_scroll_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: complex_layout_scroll_perf__timeline_summary dependencies: >- [ @@ -1855,13 +1963,13 @@ targets: ["devicelab", "android", "linux"] task_name: cubic_bezier_perf__e2e_summary - - name: Linux_samsung_s10 cubic_bezier_perf__timeline_summary + - name: Linux_pixel_7pro cubic_bezier_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: cubic_bezier_perf__timeline_summary - name: Linux_android cull_opacity_perf__e2e_summary @@ -1873,13 +1981,13 @@ targets: ["devicelab", "android", "linux"] task_name: cull_opacity_perf__e2e_summary - - name: Linux_samsung_s10 cull_opacity_perf__timeline_summary + - name: Linux_pixel_7pro cull_opacity_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: cull_opacity_perf__timeline_summary - name: Linux_android devtools_profile_start_test @@ -2080,6 +2188,15 @@ targets: ["devicelab", "android", "linux"] task_name: fullscreen_textfield_perf__e2e_summary + - name: Linux_android very_long_picture_scrolling_perf__e2e_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 120 + properties: + tags: > + ["devicelab", "android", "linux"] + task_name: very_long_picture_scrolling_perf__e2e_summary + - name: Linux_android hello_world__memory recipe: devicelab/devicelab_drone presubmit: false @@ -2135,13 +2252,13 @@ targets: ["devicelab", "android", "linux"] task_name: imagefiltered_transform_animation_perf__timeline_summary - - name: Linux_samsung_s10 imagefiltered_transform_animation_perf__timeline_summary + - name: Linux_pixel_7pro imagefiltered_transform_animation_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: imagefiltered_transform_animation_perf__timeline_summary - name: Linux_android image_list_reported_duration @@ -2219,8 +2336,10 @@ targets: - name: Linux_android list_text_layout_impeller_perf__e2e_summary recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: + ignore_flakiness: "true" tags: > ["devicelab", "android", "linux"] task_name: list_text_layout_impeller_perf__e2e_summary @@ -2243,15 +2362,6 @@ targets: ["devicelab", "android", "linux"] task_name: old_gallery__transition_perf - - name: Linux_android new_gallery__transition_perf - recipe: devicelab/devicelab_drone - presubmit: false - timeout: 60 - properties: - tags: > - ["devicelab", "android", "linux"] - task_name: new_gallery__transition_perf - - name: Linux_build_test flutter_gallery__transition_perf recipe: devicelab/devicelab_drone_build_test presubmit: false @@ -2297,34 +2407,84 @@ targets: ["devicelab", "android", "linux"] task_name: flutter_gallery__transition_perf_with_semantics - - name: Linux_samsung_s10 new_gallery__transition_perf + # MotoG4, Skia + - name: Linux_android new_gallery__transition_perf recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux"] task_name: new_gallery__transition_perf + # Pixel 7 Pro, Skia + - name: Linux_pixel_7pro new_gallery__transition_perf + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: new_gallery__transition_perf + + # Samsung A02, Skia + - name: Linux_samsung_a02 new_gallery__transition_perf + recipe: devicelab/devicelab_drone + bringup: true + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "android", "linux", "samsung", "a02"] + task_name: new_gallery__transition_perf + + # Moto G4, Impeller (OpenGL) - name: Linux_android new_gallery_impeller__transition_perf recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: + ignore_flakiness: "true" tags: > ["devicelab", "android", "linux"] task_name: new_gallery_impeller__transition_perf - - name: Linux_samsung_s10 new_gallery_impeller__transition_perf - bringup: true # Flaky https://github.com/flutter/flutter/issues/124693 + # Pixel 7 Pro, Impeller (Vulkan) + - name: Linux_pixel_7pro new_gallery_impeller__transition_perf + recipe: devicelab/devicelab_drone + bringup: true + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: new_gallery_impeller__transition_perf + + # Samsung A02, Impeller (OpenGL) + - name: Linux_samsung_a02 new_gallery_impeller__transition_perf + bringup: true recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: + ignore_flakiness: "true" tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "samsung", "a02"] task_name: new_gallery_impeller__transition_perf + # Pixel 7 Pro, Impeller (OpenGL) + - name: Linux_pixel_7pro new_gallery_opengles_impeller__transition_perf + bringup: true + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: new_gallery_opengles_impeller__transition_perf + - name: Linux_android picture_cache_perf__e2e_summary recipe: devicelab/devicelab_drone presubmit: false @@ -2334,13 +2494,13 @@ targets: ["devicelab", "android", "linux"] task_name: picture_cache_perf__e2e_summary - - name: Linux_samsung_s10 picture_cache_perf__timeline_summary + - name: Linux_pixel_7pro picture_cache_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: picture_cache_perf__timeline_summary - name: Linux_android android_picture_cache_complexity_scoring_perf__timeline_summary @@ -2362,6 +2522,7 @@ targets: task_name: slider_perf_android - name: Linux_android platform_channels_benchmarks + bringup: true # Flaky https://github.com/flutter/flutter/issues/135105 recipe: devicelab/devicelab_drone presubmit: false timeout: 60 @@ -2397,13 +2558,13 @@ targets: ["devicelab", "android", "linux"] task_name: platform_views_scroll_perf__timeline_summary - - name: Linux_samsung_s10 platform_views_scroll_perf__timeline_summary + - name: Linux_pixel_7pro platform_views_scroll_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: platform_views_scroll_perf__timeline_summary - name: Linux_android platform_views_scroll_perf_impeller__timeline_summary @@ -2412,18 +2573,20 @@ targets: presubmit: false timeout: 60 properties: + ignore_flakiness: "true" tags: > ["devicelab", "android", "linux"] task_name: platform_views_scroll_perf_impeller__timeline_summary - - name: Linux_samsung_s10 platform_views_scroll_perf_impeller__timeline_summary + - name: Linux_pixel_7pro platform_views_scroll_perf_impeller__timeline_summary bringup: true recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: + ignore_flakiness: "true" tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: platform_views_scroll_perf_impeller__timeline_summary - name: Linux_android platform_view__start_up @@ -2462,13 +2625,13 @@ targets: ["devicelab", "android", "linux"] task_name: textfield_perf__e2e_summary - - name: Linux_samsung_s10 textfield_perf__timeline_summary + - name: Linux_pixel_7pro textfield_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: tags: > - ["devicelab", "android", "linux", "samsung", "s10"] + ["devicelab", "android", "linux", "pixel", "7pro"] task_name: textfield_perf__timeline_summary - name: Linux_android tiles_scroll_perf__timeline_summary @@ -2539,6 +2702,7 @@ targets: task_name: opacity_peephole_fade_transition_text_perf__e2e_summary - name: Linux_android opacity_peephole_grid_of_alpha_savelayers_perf__e2e_summary + bringup: true # Flaky https://github.com/flutter/flutter/issues/135118 recipe: devicelab/devicelab_drone presubmit: false timeout: 60 @@ -2595,57 +2759,142 @@ targets: - name: Linux_android animated_blur_backdrop_filter_perf__timeline_summary recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: + ignore_flakiness: "true" tags: > ["devicelab", "android", "linux"] task_name: animated_blur_backdrop_filter_perf__timeline_summary - - name: Staging_build_linux analyze + - name: Linux_pixel_7pro animated_blur_backdrop_filter_perf__timeline_summary + recipe: devicelab/devicelab_drone presubmit: false + # Uses Impeller. bringup: true - recipe: flutter/flutter timeout: 60 properties: ignore_flakiness: "true" tags: > - ["framework","hostonly","linux"] - validation: analyze - validation_name: Analyze + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: animated_blur_backdrop_filter_perf__timeline_summary - - name: Staging_build_linux framework_tests_misc + - name: Linux_pixel_7pro animated_advanced_blend_perf_opengles__timeline_summary + recipe: devicelab/devicelab_drone presubmit: false + # Uses Impeller. bringup: true - recipe: flutter/flutter_drone timeout: 60 properties: ignore_flakiness: "true" - dependencies: >- - [ - {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, - {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, - {"dependency": "cmake", "version": "build_id:8787856497187628321"}, - {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "open_jdk", "version": "version:11"}, - {"dependency": "android_sdk", "version": "version:33v6"} - ] - shard: framework_tests - subshard: misc tags: > - ["framework", "hostonly", "shard", "linux"] - runIf: - - dev/** - - examples/api/** - - packages/flutter/** - - packages/flutter_driver/** - - packages/integration_test/** - - packages/flutter_localizations/** - - packages/fuchsia_remote_debug_protocol/** - - packages/flutter_test/** - - packages/flutter_goldens/** - - packages/flutter_tools/** - - bin/** - - .ci.yaml + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: animated_advanced_blend_perf_opengles__timeline_summary + + - name: Linux_pixel_7pro animated_advanced_blend_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: animated_advanced_blend_perf__timeline_summary + + - name: Mac_ios animated_advanced_blend_perf_ios__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: animated_advanced_blend_perf_ios__timeline_summary + + - name: Linux_pixel_7pro animated_blur_backdrop_filter_perf_opengles__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: animated_blur_backdrop_filter_perf_opengles__timeline_summary + + - name: Linux_pixel_7pro draw_vertices_perf_opengles__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: draw_vertices_perf_opengles__timeline_summary + + - name: Linux_pixel_7pro draw_vertices_perf__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + # Uses Impeller. + bringup: true + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["devicelab", "android", "linux", "pixel", "7pro"] + task_name: draw_vertices_perf__timeline_summary + + - name: Mac_ios draw_vertices_perf_ios__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: draw_vertices_perf_ios__timeline_summary + + - name: Mac_ios draw_atlas_perf_ios__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: draw_atlas_perf_ios__timeline_summary + + - name: Mac_ios static_path_tessellation_perf_ios__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: static_path_tessellation_perf_ios__timeline_summary + + - name: Mac_ios dynamic_path_tessellation_perf_ios__timeline_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: dynamic_path_tessellation_perf_ios__timeline_summary + + - name: Staging_build_linux analyze + presubmit: false + bringup: true + recipe: flutter/flutter + timeout: 60 + properties: + ignore_flakiness: "true" + tags: > + ["framework","hostonly","linux"] + validation: analyze + validation_name: Analyze - name: Mac_benchmark animated_complex_opacity_perf_macos__e2e_summary presubmit: false @@ -2705,7 +2954,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -2723,7 +2972,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -2741,7 +2990,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -2759,7 +3008,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -2906,7 +3155,7 @@ targets: [ {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "gems", "version": "v3.3.14"}, - {"dependency": "open_jdk", "version": "version:11"}, + {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "android_sdk", "version": "version:33v6"} ] shard: framework_tests @@ -3240,7 +3489,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -3264,7 +3513,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -3288,7 +3537,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -3312,7 +3561,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "gems", "version": "v3.3.14"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} @@ -3394,7 +3643,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] @@ -3672,6 +3921,10 @@ targets: tags: > ["devicelab", "ios", "mac"] task_name: flutter_gallery_ios__start_up_xcode_debug + $flutter/osx_sdk : >- + { + "sdk_version": "14c18" + } bringup: true - name: Mac_ios flutter_view_ios__start_up @@ -3749,6 +4002,10 @@ targets: tags: > ["devicelab", "ios", "mac"] task_name: integration_ui_ios_driver_xcode_debug + $flutter/osx_sdk : >- + { + "sdk_version": "14c18" + } bringup: true - name: Mac_ios integration_ui_ios_frame_number @@ -3778,22 +4035,30 @@ targets: ["devicelab", "ios", "mac"] task_name: integration_ui_ios_textfield - - name: Mac_ios ios_app_with_extensions_test + - name: Mac_x64 ios_app_with_extensions_test recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: + dependencies: >- + [ + {"dependency": "gems", "version": "v3.3.14"} + ] tags: > - ["devicelab", "ios", "mac"] + ["devicelab", "hostonly", "mac"] task_name: ios_app_with_extensions_test - - name: Mac_arm64_ios ios_app_with_extensions_test + - name: Mac_arm64 ios_app_with_extensions_test recipe: devicelab/devicelab_drone presubmit: false timeout: 60 properties: + dependencies: >- + [ + {"dependency": "gems", "version": "v3.3.14"} + ] tags: > - ["devicelab", "ios", "mac", "arm64"] + ["devicelab", "hostonly", "mac", "arm64"] task_name: ios_app_with_extensions_test - name: Mac_ios ios_content_validation_test @@ -3877,8 +4142,30 @@ targets: tags: > ["devicelab", "ios", "mac"] task_name: microbenchmarks_ios_xcode_debug + $flutter/osx_sdk : >- + { + "sdk_version": "14c18" + } bringup: true + - name: Mac_ios native_assets_ios_simulator + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: native_assets_ios_simulator + + - name: Mac_ios native_assets_ios + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: native_assets_ios + - name: Mac_ios native_platform_view_ui_tests_ios recipe: devicelab/devicelab_drone presubmit: false @@ -4023,6 +4310,15 @@ targets: ["devicelab", "ios", "mac"] task_name: fullscreen_textfield_perf_ios__e2e_summary + - name: Mac_ios very_long_picture_scrolling_perf_ios__e2e_summary + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 120 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: very_long_picture_scrolling_perf_ios__e2e_summary + - name: Mac_ios tiles_scroll_perf_ios__timeline_summary recipe: devicelab/devicelab_drone presubmit: false @@ -4055,7 +4351,6 @@ targets: - name: Mac_ios draw_points_perf_ios__timeline_summary recipe: devicelab/devicelab_drone presubmit: false - bringup: true timeout: 60 properties: tags: > @@ -4124,7 +4419,6 @@ targets: - name: Mac_arm64_ios run_debug_test_macos recipe: devicelab/devicelab_drone - presubmit: false # https://github.com/flutter/flutter/issues/118827 timeout: 60 properties: tags: > @@ -4168,7 +4462,7 @@ targets: - bin/** - .ci.yaml - - name: Windows build_tests_1_4 + - name: Windows build_tests_1_5 recipe: flutter/flutter_drone timeout: 60 properties: @@ -4176,17 +4470,17 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} ] shard: build_tests - subshard: "1_4" + subshard: "1_5" tags: > ["framework", "hostonly", "shard", "windows"] - - name: Windows build_tests_2_4 + - name: Windows build_tests_2_5 recipe: flutter/flutter_drone timeout: 60 properties: @@ -4194,17 +4488,17 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} ] shard: build_tests - subshard: "2_4" + subshard: "2_5" tags: > ["framework", "hostonly", "shard", "windows"] - - name: Windows build_tests_3_4 + - name: Windows build_tests_3_5 recipe: flutter/flutter_drone timeout: 60 properties: @@ -4212,17 +4506,17 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} ] shard: build_tests - subshard: "3_4" + subshard: "3_5" tags: > ["framework", "hostonly", "shard", "windows"] - - name: Windows build_tests_4_4 + - name: Windows build_tests_4_5 recipe: flutter/flutter_drone timeout: 60 properties: @@ -4230,13 +4524,31 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} ] shard: build_tests - subshard: "4_4" + subshard: "4_5" + tags: > + ["framework", "hostonly", "shard", "windows"] + + - name: Windows build_tests_5_5 + recipe: flutter/flutter_drone + timeout: 60 + properties: + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:33v6"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, + {"dependency": "open_jdk", "version": "version:17"}, + {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, + {"dependency": "vs_build", "version": "version:vs2019"} + ] + shard: build_tests + subshard: "5_5" tags: > ["framework", "hostonly", "shard", "windows"] @@ -4284,7 +4596,7 @@ targets: [ {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"}, - {"dependency": "open_jdk", "version": "version:11"}, + {"dependency": "open_jdk", "version": "version:17"}, {"dependency": "android_sdk", "version": "version:33v6"} ] shard: framework_tests @@ -4337,7 +4649,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4362,13 +4674,14 @@ targets: task_name: hot_mode_dev_cycle_win_target__benchmark - name: Windows module_custom_host_app_name_test + bringup: true # Flaky https://github.com/flutter/flutter/issues/134644 recipe: devicelab/devicelab_drone timeout: 60 properties: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4387,7 +4700,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4406,7 +4719,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4438,7 +4751,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4457,7 +4770,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"} ] tags: > @@ -4504,6 +4817,25 @@ targets: - bin/** - .ci.yaml + - name: Windows_arm64 run_debug_test_windows + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + task_name: run_debug_test_windows + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - name: Windows run_release_test_windows recipe: devicelab/devicelab_drone presubmit: false @@ -4522,6 +4854,25 @@ targets: - bin/** - .ci.yaml + - name: Windows_arm64 run_release_test_windows + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + task_name: run_release_test_windows + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - name: Windows tool_integration_tests_1_6 recipe: flutter/flutter_drone timeout: 60 @@ -4530,7 +4881,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4554,7 +4905,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4578,7 +4929,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4602,7 +4953,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4626,7 +4977,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4650,7 +5001,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"}, {"dependency": "vs_build", "version": "version:vs2019"} @@ -4713,7 +5064,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] @@ -4734,7 +5085,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:33v6"}, - {"dependency": "chrome_and_driver", "version": "version:114.0"}, + {"dependency": "chrome_and_driver", "version": "version:117.0"}, {"dependency": "open_jdk", "version": "version:11"}, {"dependency": "goldctl", "version": "git_revision:f808dcff91b221ae313e540c09d79696cd08b8de"} ] @@ -4774,6 +5125,20 @@ targets: ] task_name: hello_world_win_desktop__compile + - name: Windows_arm64 hello_world_win_desktop__compile + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: hello_world_win_desktop__compile + - name: Windows flutter_gallery_win_desktop__compile recipe: devicelab/devicelab_drone presubmit: false @@ -4787,6 +5152,20 @@ targets: ] task_name: flutter_gallery_win_desktop__compile + - name: Windows_arm64 flutter_gallery_win_desktop__compile + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: flutter_gallery_win_desktop__compile + - name: Windows flutter_gallery_win_desktop__start_up recipe: devicelab/devicelab_drone presubmit: false @@ -4800,6 +5179,20 @@ targets: ] task_name: flutter_gallery_win_desktop__start_up + - name: Windows_arm64 flutter_gallery_win_desktop__start_up + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: flutter_gallery_win_desktop__start_up + - name: Windows complex_layout_win_desktop__start_up recipe: devicelab/devicelab_drone presubmit: false @@ -4813,6 +5206,20 @@ targets: ] task_name: complex_layout_win_desktop__start_up + - name: Windows_arm64 complex_layout_win_desktop__start_up + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: complex_layout_win_desktop__start_up + - name: Windows flutter_view_win_desktop__start_up recipe: devicelab/devicelab_drone presubmit: false @@ -4826,6 +5233,20 @@ targets: ] task_name: flutter_view_win_desktop__start_up + - name: Windows_arm64 flutter_view_win_desktop__start_up + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: flutter_view_win_desktop__start_up + - name: Windows platform_view_win_desktop__start_up recipe: devicelab/devicelab_drone presubmit: false @@ -4839,6 +5260,20 @@ targets: ] task_name: platform_view_win_desktop__start_up + - name: Windows_arm64 platform_view_win_desktop__start_up + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + task_name: platform_view_win_desktop__start_up + - name: Windows_android basic_material_app_win__compile recipe: devicelab/devicelab_drone presubmit: false @@ -4918,6 +5353,20 @@ targets: ["devicelab", "hostonly", "windows"] task_name: windows_startup_test + - name: Windows_arm64 windows_startup_test + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + task_name: windows_startup_test + - name: Windows flutter_tool_startup__windows recipe: devicelab/devicelab_drone presubmit: false @@ -4927,6 +5376,16 @@ targets: ["devicelab", "hostonly", "windows"] task_name: flutter_tool_startup + - name: Windows_arm64 flutter_tool_startup__windows + recipe: devicelab/devicelab_drone + bringup: true # https://github.com/flutter/flutter/issues/134083 + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + task_name: flutter_tool_startup + - name: Linux flutter_tool_startup__linux recipe: devicelab/devicelab_drone presubmit: false diff --git a/.github/ISSUE_TEMPLATE/5_performance_speed.md b/.github/ISSUE_TEMPLATE/5_performance_speed.md deleted file mode 100644 index 0c7054c535829..0000000000000 --- a/.github/ISSUE_TEMPLATE/5_performance_speed.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: My app is slow or missing frames -about: You are writing an application but have discovered that it is slow, you are - not hitting 60Hz, or you are getting jank (missed frames). -title: '' -labels: 'from: performance template' -assignees: '' - ---- - - - -## Details - - - - - -**Target Platform:** -**Target OS version/browser:** -**Devices:** - -## Logs - -
-Logs - - - -``` -``` - - - -``` -``` - -
diff --git a/.github/ISSUE_TEMPLATE/5_performance_speed.yml b/.github/ISSUE_TEMPLATE/5_performance_speed.yml new file mode 100644 index 0000000000000..8133968f98397 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5_performance_speed.yml @@ -0,0 +1,172 @@ +name: My app is slow or missing frames +description: | + You are writing an application but have discovered that it is slow, + you are not hitting 60Hz, or you are getting jank (missed frames). +labels: 'from: performance template' +body: + - type: markdown + attributes: + value: | + Thank you for using Flutter! + + If you are looking for support, please check out our documentation + or consider asking a question on Stack Overflow: + + - https://flutter.dev/ + - https://api.flutter.dev/ + - https://stackoverflow.com/questions/tagged/flutter?sort=frequent + - type: textarea + attributes: + label: Steps to reproduce + description: Please tell us exactly how to reproduce the problem you are running into. + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + attributes: + label: Code sample + description: | + Please create a minimal reproducible sample that shows the problem and attach it below between the lines with the backticks. + + Try to reproduce the problem in a test app. Either run `flutter create janktest` and recreate the situation you are experiencing in that app, or clone your app and delete code until you have the jank reproducing with a single `.dart` file. + + If you need more than just a `.dart` file (for example, assets are needed to reproduce the issue, or plugins/packages are needed to reproduce the issue) then create a GitHub repository and upload code there. + + Without this we will unlikely be able to progress on the issue, and because of that we regretfully will have to close it. + + Note: Please do not upload screenshots of text. Instead, use code blocks or the above mentioned ways to upload your code sample. + value: | +
Code sample + + ```dart + [Paste your code here] + ``` + +
+ validations: + required: true + - type: checkboxes + attributes: + label: Performance profiling on master channel + description: | + Switch flutter to master channel and run this app on a physical device using profile mode with Skia tracing enabled, as follows: + ```console + flutter channel master + flutter run --profile --trace-skia + ``` + The bleeding edge master channel is encouraged here because Flutter is constantly fixing bugs and improving its performance. Your problem in an older Flutter version may have already been solved in the master channel. + options: + - label: The issue still persists on the master channel + required: true + - type: textarea + attributes: + label: Timeline Traces + description: | + Open Flutter DevTools and save a timeline trace of the performance issue so we know which functions might be causing it. See "How to Collect and Read Timeline Traces" on this blog post: https://medium.com/flutter/profiling-flutter-applications-using-the-timeline-a1a434964af3#a499 + + Make sure the performance overlay is turned OFF when recording the trace as that may affect the performance of the profile run. (Pressing ‘P’ on the command line toggles the overlay.) + + If the trace are too large to be uploaded to GitHub, you may upload them as a `zip` file or use online tools like https://pastebin.com to share it. + value: | +
Timeline Traces JSON + + ```json + [Paste the Timeline Traces here] + ``` + +
+ validations: + required: true + - type: textarea + attributes: + label: Video demonstration + description: | + Record a video of the performance issue using another phone so we can have an intuitive understanding of what happened. + + Don’t use "adb screenrecord", as that affects the performance of the profile run. + value: | +
+ Video demonstration + + [Upload media here] + +
+ - type: dropdown + id: target_platforms + attributes: + label: What target platforms are you seeing this bug on? + multiple: true + options: + - Android + - iOS + - Web + - macOS + - Linux + - Windows + validations: + required: true + - type: textarea + attributes: + label: OS/Browser name and version | Device information + description: | + Which target OS version is the test system running? For Web, please provide browser version. + Please also include the device information (model, CPU architecture, etc). + validations: + required: true + - type: dropdown + id: device-kind + attributes: + label: Does the problem occur on emulator/simulator as well as on physical devices? + options: + - "Unknown" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: enable-impeller + attributes: + label: Is the problem only reproducible with Impeller? + description: | + Please check https://docs.flutter.dev/perf/impeller as the guideline on how to enable/disable it. + options: + - "N/A" + - "Yes" + - "No" + validations: + required: true + - type: textarea + attributes: + label: Logs + description: | + Include the full logs of the commands you are running between the lines with the backticks below. If you are running any `flutter` commands, please include the output of running them with `--verbose`; for example, the output of running `flutter --verbose create foo`. + + If the logs are too large to be uploaded to GitHub, you may upload them as a `txt` file or use online tools like https://pastebin.com to share it. + + Note: Please do not upload screenshots of text. Instead, use code blocks or the above mentioned ways to upload logs. + value: | +
Logs + + ```console + [Paste your logs here] + ``` + +
+ - type: textarea + attributes: + label: Flutter Doctor output + description: | + Finally, paste the output of running `flutter doctor -v` here, with your device plugged in. + value: | +
Doctor output + + ```console + [Paste your output here] + ``` + +
+ validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/6_infrastructure.md b/.github/ISSUE_TEMPLATE/6_infrastructure.md deleted file mode 100644 index d12e41ae11aac..0000000000000 --- a/.github/ISSUE_TEMPLATE/6_infrastructure.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: The CI infrastructure used by Flutter has a problem -about: As a contributor, you want to file an issue about the build/test/release - infra, e.g. dashboards (http://flutter-dashboard.appspot.com), devicelab, - LUCI (https://ci.chromium.org/p/flutter) etc. -title: '' -labels: 'team: infra' -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/6_infrastructure.yml b/.github/ISSUE_TEMPLATE/6_infrastructure.yml new file mode 100644 index 0000000000000..a6cfc9fc71eef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/6_infrastructure.yml @@ -0,0 +1,75 @@ +name: The CI infrastructure used by Flutter has a problem +description: | + As a contributor, you want to file an issue about the build/test/release + infra, e.g. dashboards (http://flutter-dashboard.appspot.com), devicelab, + LUCI (https://ci.chromium.org/p/flutter) etc. +labels: ['team-infra'] +body: + - type: markdown + attributes: + value: | + Thank you for using Flutter! + + It looks like you found an issue with our Infrastructure services. + Please complete the form below so that we can help to resolve your + issue as quickly as possible. + - type: checkboxes + attributes: + label: Is there an existing issue for this? + options: + - label: I have searched the [existing infra issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Ateam-infra) + required: true + - type: dropdown + attributes: + label: Type of Request + description: | + Is this a bug, feature request or Infra Task? + + If you have a bug and you believe the issue is a blocker please add the P0 label and + set the project to 'Infra Ticket Queue.' + + If this is a devicelab feature such as a package update or a device is down please + add the 'device-lab' label to the created issue and set the project to 'Infra Ticket Queue.' + options: + - bug + - feature request + - infra task + default: 0 + validations: + required: true + - type: textarea + id: env + attributes: + label: Infrastructure Environment + description: | + Which part of the infrastructure is this issue occurring? Or, if this is a feature + request, where should the feature be implemented? + value: LUCI, Github, Cocoon scheduler, Autosubmit, etc... + validations: + required: true + - type: textarea + id: affects + attributes: + label: What is happening? + description: | + If this is an issue please describe what is happening? If this is a feature request, + please describe the use case and provide a proposal of the feature. + + Please include links to build pages, etc. + value: Please be descriptive. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: If you have a bug please include steps to reproduce the issue. + value: | + Step 1: + Step 2: + .. + Step n: + - type: textarea + attributes: + label: Expected results + description: If you have a bug, What should the expect output be? + value: I expect to see X when Y is finished. diff --git a/.github/ISSUE_TEMPLATE/9_first_party_packages.yml b/.github/ISSUE_TEMPLATE/9_first_party_packages.yml new file mode 100644 index 0000000000000..0e8ef8ab04b92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/9_first_party_packages.yml @@ -0,0 +1,222 @@ +name: Report a bug in one of Flutter's first-party packages +description: | + You found a bug in one of Flutter's first-party packages. +body: + - type: markdown + attributes: + value: | + Thank you for using Flutter! + + If you are looking for support, please check out our documentation + or consider asking a question on Stack Overflow: + + - https://flutter.dev/ + - https://api.flutter.dev/ + - https://stackoverflow.com/questions/tagged/flutter?sort=frequent + - type: checkboxes + attributes: + label: Is there an existing issue for this? + options: + - label: I have searched the [existing package issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Apackage) + required: true + - label: I have read the [guide to filing a bug](https://flutter.dev/docs/resources/bug-reports) + required: true + - type: dropdown + id: packages + attributes: + label: What package does this bug report belong to? + description: | + If the package you are reporting a bug for is not listed here, + it could be a third party package's issue. + In that case, you should report the issue to the package's developers. + options: + - animations + - camera + - cross_file + - css_colors + - dynamic_layouts + - espresso + - extension_google_sign_in_as_googleapis_auth + - file_selector + - flutter_adaptive_scaffold + - flutter_image + - flutter_lints + - flutter_markdown + - flutter_migrate + - flutter_plugin_android_lifecycle + - flutter_template_images + - go_router + - go_router_builder + - google_identity_services_web + - google_maps_flutter + - google_sign_in + - image_picker + - in_app_purchase + - ios_platform_images + - local_auth + - metrics_center + - multicast_dns + - palette_generator + - path_provider + - pigeon + - plugin_platform_interface + - pointer_interceptor + - quick_actions + - rfw + - shared_preferences + - standard_message_codec + - url_launcher + - video_player + - web_benchmarks + - webview_flutter + - xdg_directories + validations: + required: true + - type: dropdown + id: target_platforms + attributes: + label: What target platforms are you seeing this bug on? + description: Have you confirmed that package supports the platform you are reporting against? + multiple: true + options: + - Android + - iOS + - Web + - macOS + - Linux + - Windows + validations: + required: true + - type: dropdown + id: pub_upgrade + attributes: + label: Have you already upgraded your packages? + description: | + Please check if the issue still persists or not after running `flutter pub upgrade`. + options: + - "Yes" + - "No" + validations: + required: true + - type: textarea + attributes: + label: Dependency versions + description: | + `pubspec.lock` file content that includes the package and its dependencies. + + You can remove private dependencies from the file if they are sensitive. + + If the file is too large to be uploaded to GitHub or the content is too long, + you may use online tools like https://pastebin.com and share the url here. + + Note: Please do not upload screenshots of text. Instead, use code blocks + or the above mentioned ways to upload this. + value: | +
pubspec.lock + + ```lock + [Paste file content here] + ``` + +
+ - type: textarea + attributes: + label: Steps to reproduce + description: Please tell us exactly how to reproduce the problem you are running into. + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + attributes: + label: Expected results + description: Please tell us what is expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Actual results + description: Please tell us what is actually happening. + validations: + required: true + - type: textarea + attributes: + label: Code sample + description: | + Please create a minimal reproducible sample that shows the problem + and attach it below between the lines with the backticks. + + To create it, use the `flutter create bug` command and update the `main.dart` file. + + Alternatively, you can create a public GitHub repository to share your sample. + + Without this we will unlikely be able to progress on the issue, and because of that + we regretfully will have to close it. + + You can also refer to the package's example project if it is simple enough + to reproduce the bug. + + Note: Please do not upload screenshots of text. Instead, use code blocks + or the above mentioned ways to upload your code sample. + value: | +
Code sample + + ```dart + [Paste your code here] + ``` + +
+ validations: + required: true + - type: textarea + attributes: + label: Screenshots or Videos + description: | + Upload any screenshots or videos of the bug if applicable. + value: | +
+ Screenshots / Video demonstration + + [Upload media here] + +
+ - type: textarea + attributes: + label: Logs + description: | + Include the full logs of the commands you are running between the lines + with the backticks below. If you are running any `flutter` commands, + please include the output of running them with `--verbose`; for example, + the output of running `flutter --verbose create foo`. + + If the logs are too large to be uploaded to GitHub, you may upload + them as a `txt` file or use online tools like https://pastebin.com to + share it. + + Note: Please do not upload screenshots of text. Instead, use code blocks + or the above mentioned ways to upload logs. + value: | +
Logs + + ```console + [Paste your logs here] + ``` + +
+ - type: textarea + attributes: + label: Flutter Doctor output + description: | + Please provide the full output of running `flutter doctor -v` + value: | +
Doctor output + + ```console + [Paste your output here] + ``` + +
+ validations: + required: true diff --git a/.github/autosubmit.yml b/.github/autosubmit.yml deleted file mode 100644 index 74f629659d43c..0000000000000 --- a/.github/autosubmit.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright 2023 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# This file will be added to flutter's internal repository. -# https://github.com/flutter/flutter/wiki/Autosubmit-bot -config_path: 'autosubmit/flutter/autosubmit_master.yml' diff --git a/.github/labeler.yml b/.github/labeler.yml index b134dfdbc907f..543d4927e9712 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -13,6 +13,11 @@ - '**/animation/*' - '**/*animation*' +'a: desktop': + - '**/linux/**/*' + - '**/macos/**/*' + - '**/windows/**/*' + 'a: internationalization': - packages/flutter_localizations/**/* @@ -72,6 +77,7 @@ framework: - packages/flutter_goldens_client/**/* - packages/flutter_test/**/* - packages/integration_test/**/* + - examples/api/**/* 'f: integration_test': - packages/integration_test/**/* @@ -79,21 +85,10 @@ framework: platform-ios: - packages/flutter_tools/lib/src/ios/**/* -team: - - '**/pubspec.yaml' - - '**/fix_data.yaml' - - '**/*.expect' - - '**/*test_fixes*' - - .github/**/* - - dev/**/* - - examples/**/* - - packages/flutter_goldens/**/* - - packages/flutter_goldens_client/**/* - 'customer: gallery': - examples/flutter_gallery/**/* -tech-debt: +'c: tech-debt': - '**/fix_data.yaml' - '**/*.expect' - '**/*test_fixes*' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3d5816c9aef03..a1a23a0dbd350 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,11 +19,11 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'flutter/flutter' }} steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 - name: ./bin/flutter test --coverage run: pushd packages/flutter;../../bin/flutter test --coverage -j 1;popd - name: upload coverage - uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d with: files: packages/flutter/coverage/lcov.info verbose: true diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 336d04f809a15..43c62b2a44fc6 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: # Source available at https://github.com/actions/labeler/blob/main/README.md - - uses: actions/labeler@0967ca812e7fdc8f5f71402a1b486d5bd061fe20 + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 with: - sync-labels: false + sync-labels: '' diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index a587024ef761f..072d4d2de5812 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'flutter/flutter' }} steps: - - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836 + - uses: dessant/lock-threads@be8aa5be94131386884a6da4189effda9b14aa21 with: process-only: 'issues' github-token: ${{ github.token }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 1f6b0d0f57539..a568c78265469 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Mirror action step id: mirror - uses: google/mirror-branch-action@c6b07e441a7ffc5ae15860c1d0a8107a3a151db8 + uses: google/mirror-branch-action@30c52ee21f5d3bd7fb28b95501c11aae7f17eebb with: github-token: ${{ secrets.FLUTTERMIRRORINGBOT_TOKEN }} source: 'master' diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 31e4e434b4244..9ee5be88b630b 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 with: persist-credentials: false @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). - name: "Upload artifact" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 with: name: SARIF file path: results.sarif @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@04df1262e6247151b5ac09cd2c303ac36ad3f62b + uses: github/codeql-action/upload-sarif@701f152f28d4350ad289a5e31435e9ab6169a7ca with: sarif_file: results.sarif diff --git a/AUTHORS b/AUTHORS index f557468d0dbf0..974b9c9705715 100644 --- a/AUTHORS +++ b/AUTHORS @@ -116,3 +116,4 @@ Mike Rydstrom Harish Anbalagan Kim Jiun LinXunFeng +Sabin Neupane diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b24b7393fe543..86f1ec710e107 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -15,7 +15,7 @@ Specifically: Should you experience anything that makes you feel unwelcome in Flutter's community, please contact [conduct@flutter.dev](mailto:conduct@flutter.dev) or, if you prefer, directly contact someone on the project, for instance -[Hixie](mailto:ian@hixie.ch) or [Tim](mailto:timsneath@google.com). +[Hixie](mailto:ian@hixie.ch). The Flutter project will not tolerate harassment in Flutter's community, even outside of Flutter's public communication channels. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d73e488ea62e..46e2c045acee5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ for how to set up your development environment, or ask in #hackers-test on Disco Developing for Flutter ---------------------- -If you would prefer to write code, you may wish to start with our list of [good first contributions](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+contribution%22). +If you would prefer to write code, you may wish to start with our list of [good first issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). To develop for Flutter, you will eventually need to become familiar with our processes and conventions. This section lists the documents diff --git a/README.md b/README.md index 55de23b6d9950..7fa62d9b5e88e 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Information on how to get started can be found in our [Build Status - Cirrus]: https://api.cirrus-ci.com/github/flutter/flutter.svg [Build status]: https://cirrus-ci.com/github/flutter/flutter/master [Discord instructions]: https://github.com/flutter/flutter/wiki/Chat -[Discord badge]: https://img.shields.io/discord/608014603317936148 +[Discord badge]: https://img.shields.io/discord/608014603317936148?logo=discord [Twitter handle]: https://img.shields.io/twitter/follow/flutterdev.svg?style=social&label=Follow [Twitter badge]: https://twitter.com/intent/follow?screen_name=flutterdev [layered architecture]: https://docs.flutter.dev/resources/inside-flutter diff --git a/TESTOWNERS b/TESTOWNERS index 18b3626990c39..4f4096f01b11a 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -89,10 +89,21 @@ /dev/devicelab/bin/tasks/spell_check_test_ios.dart @camsim99 @flutter/android /dev/devicelab/bin/tasks/spell_check_test.dart @camsim99 @flutter/android /dev/devicelab/bin/tasks/textfield_perf__e2e_summary.dart @zanderso @flutter/engine +/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/web_size__compile_test.dart @yjbanov @flutter/web /dev/devicelab/bin/tasks/wide_gamut_ios.dart @gaaclarke @flutter/engine +/dev/devicelab/bin/tasks/animated_advanced_blend_perf__timeline_summary.dart @gaaclarke @flutter/engine +/dev/devicelab/bin/tasks/animated_advanced_blend_perf_ios__timeline_summary.dart @gaaclarke @flutter/engine +/dev/devicelab/bin/tasks/animated_advanced_blend_perf_opengles__timeline_summary.dart @gaaclarke @flutter/engine /dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf_opengles__timeline_summary.dart @gaaclarke @flutter/engine /dev/devicelab/bin/tasks/slider_perf_android.dart @tahatesser @flutter/framework +/dev/devicelab/bin/tasks/draw_vertices_perf_opengles__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/draw_atlas_perf_opengles__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/draw_vertices_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/draw_atlas_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/static_path_tessellation_perf__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf__timeline_summary.dart @jonahwilliams @flutter/engine ## Windows Android DeviceLab tests /dev/devicelab/bin/tasks/basic_material_app_win__compile.dart @zanderso @flutter/tool @@ -124,6 +135,7 @@ /dev/devicelab/bin/tasks/fullscreen_textfield_perf__timeline_summary.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/hello_world__memory.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/hello_world_android__compile.dart @zanderso @flutter/tool +/dev/devicelab/bin/tasks/hello_world_impeller.dart @gaaclarke @flutter/engine /dev/devicelab/bin/tasks/home_scroll_perf__timeline_summary.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/hot_mode_dev_cycle__benchmark.dart @eliasyishak @flutter/tool /dev/devicelab/bin/tasks/hybrid_android_views_integration_test.dart @stuartmorgan @flutter/plugin @@ -137,6 +149,7 @@ /dev/devicelab/bin/tasks/microbenchmarks.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/new_gallery__transition_perf.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/new_gallery_impeller__transition_perf.dart @zanderso @flutter/engine +/dev/devicelab/bin/tasks/new_gallery_opengles_impeller__transition_perf.dart @gaaclarke @flutter/engine /dev/devicelab/bin/tasks/picture_cache_perf__timeline_summary.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/platform_channel_sample_test.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/platform_interaction_test.dart @stuartmorgan @flutter/plugin @@ -154,7 +167,6 @@ /dev/devicelab/bin/tasks/channels_integration_test_ios.dart @cyanglaz @flutter/engine /dev/devicelab/bin/tasks/codegen_integration_mac.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/color_filter_and_fade_perf_ios__e2e_summary.dart @cyanglaz @flutter/engine -/dev/devicelab/bin/tasks/complex_layout_ios__compile.dart @cyanglaz @flutter/engine /dev/devicelab/bin/tasks/complex_layout_ios__start_up.dart @cyanglaz @flutter/engine /dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_ios__timeline_summary.dart @jonahwilliams @flutter/engine /dev/devicelab/bin/tasks/complex_layout_scroll_perf_ios__timeline_summary.dart @cyanglaz @flutter/engine @@ -189,6 +201,8 @@ /dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/microbenchmarks_ios.dart @cyanglaz @flutter/engine /dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart @vashworth @flutter/engine +/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @dacoharkes @flutter/ios +/dev/devicelab/bin/tasks/native_assets_ios.dart @dacoharkes @flutter/ios /dev/devicelab/bin/tasks/native_platform_view_ui_tests_ios.dart @hellohuanlin @flutter/ios /dev/devicelab/bin/tasks/new_gallery_ios__transition_perf.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine @@ -203,8 +217,13 @@ /dev/devicelab/bin/tasks/route_test_ios.dart @vashworth @flutter/tool /dev/devicelab/bin/tasks/simple_animation_perf_ios.dart @cyanglaz @flutter/engine /dev/devicelab/bin/tasks/tiles_scroll_perf_ios__timeline_summary.dart @cyanglaz @flutter/engine +/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf_ios__e2e_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine /dev/devicelab/bin/tasks/draw_points_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/draw_vertices_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/draw_atlas_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/static_path_tessellation_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine +/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf_ios__timeline_summary.dart @jonahwilliams @flutter/engine ## Host only DeviceLab tests /dev/devicelab/bin/tasks/animated_complex_opacity_perf_macos__e2e_summary.dart @cbracken @flutter/desktop diff --git a/analysis_options.yaml b/analysis_options.yaml index 747d8e24b4750..6bf95b55f404d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,6 +17,7 @@ analyzer: language: strict-casts: true + strict-inference: true strict-raw-types: true errors: # allow self-reference to deprecated members (we do this because otherwise we have @@ -50,7 +51,7 @@ linter: - avoid_field_initializers_in_const_classes # - avoid_final_parameters # incompatible with prefer_final_parameters - avoid_function_literals_in_foreach_calls - - avoid_implementing_value_types + # - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558 - avoid_init_to_null - avoid_js_rounded_ints # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to @@ -116,7 +117,7 @@ linter: - library_prefixes - library_private_types_in_public_api # - lines_longer_than_80_chars # not required by flutter style - # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 + - literal_only_boolean_expressions # - matching_super_parameters # blocked on https://github.com/dart-lang/language/issues/2509 - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list diff --git a/bin/dart b/bin/dart index 0c1f402819bee..9ef65c2c43645 100755 --- a/bin/dart +++ b/bin/dart @@ -45,6 +45,7 @@ function follow_links() ( PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")" BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)" +SHARED_NAME="$BIN_DIR/internal/shared.sh" OS="$(uname -s)" # If we're on Windows, invoke the batch script instead to get proper locking. @@ -53,6 +54,6 @@ if [[ $OS =~ MINGW.* || $OS =~ CYGWIN.* || $OS =~ MSYS.* ]]; then fi # To define `shared::execute()` function -source "$BIN_DIR/internal/shared.sh" +source "$SHARED_NAME" shared::execute "$@" diff --git a/bin/flutter b/bin/flutter index 9347b8dad6949..21c11c7e7e8ec 100755 --- a/bin/flutter +++ b/bin/flutter @@ -50,6 +50,7 @@ function follow_links() ( PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")" BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)" +SHARED_NAME="$BIN_DIR/internal/shared.sh" OS="$(uname -s)" # If we're on Windows, invoke the batch script instead to get proper locking. @@ -58,6 +59,6 @@ if [[ $OS =~ MINGW.* || $OS =~ CYGWIN.* || $OS =~ MSYS.* ]]; then fi # To define `shared::execute()` function -source "$BIN_DIR/internal/shared.sh" +source "$SHARED_NAME" shared::execute "$@" diff --git a/bin/internal/engine.realm b/bin/internal/engine.realm new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 94b85f4a34321..77be70ab6a124 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -0757a9001dc3dcfce6f09fdd443904827274d22e +3c2ea337f67ff735d8b47a891c044ba038cb829e \ No newline at end of file diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 3999e9c238279..cf81bee4c12fb 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -771ec9b42a382b2282d2a18f99e96b6276d7063c +c070b0a7a80a54d5fad254fecdfd98ffe764bd4e diff --git a/bin/internal/fuchsia-linux.version b/bin/internal/fuchsia-linux.version index 11f77a582348c..6331e09a5f423 100644 --- a/bin/internal/fuchsia-linux.version +++ b/bin/internal/fuchsia-linux.version @@ -1 +1 @@ -iwgWLB4KaXslnaGwKuAD5S9wamgkF0Mj9a411131XdkC +l2RxJKPfYn7QzGOoLPUPk0FyRZxbYTRv1JiQJgUbm9sC diff --git a/bin/internal/fuchsia-mac.version b/bin/internal/fuchsia-mac.version index 40252d6d5a90b..87ee95f0b4107 100644 --- a/bin/internal/fuchsia-mac.version +++ b/bin/internal/fuchsia-mac.version @@ -1 +1 @@ -C3Q7MJBYkiin8zw-fLJ9QmM-8anKHqabR7B2KFuBYUgC +4WW3KRrAbuY7VeGT0pBFAQktetsyx-3C0mKMNxCd0uYC diff --git a/bin/internal/release-candidate-branch.version b/bin/internal/release-candidate-branch.version index e9546287aea5d..0ca38b3c50500 100644 --- a/bin/internal/release-candidate-branch.version +++ b/bin/internal/release-candidate-branch.version @@ -1 +1 @@ -flutter-3.13-candidate.0 +flutter-3.16-candidate.0 diff --git a/bin/internal/shared.sh b/bin/internal/shared.sh index 3532c23114a57..75d9d3013eb27 100644 --- a/bin/internal/shared.sh +++ b/bin/internal/shared.sh @@ -229,7 +229,23 @@ function shared::execute() { exit 1 fi - upgrade_flutter 7< "$PROG_NAME" + # File descriptor 7 is prepared here so that we can use it with + # flock(1) in _lock() (see above). + # + # We use number 7 because it's a luckier number than 3; luck is + # important when making locks work reliably. Also because that way + # if anyone is redirecting other file descriptors there's less + # chance of a conflict. + # + # In any case, the file we redirect into this file descriptor is + # this very source file you are reading right now, because that's + # the only file we can truly guarantee exists, since we're running + # it. We don't use PROG_NAME because otherwise if you run `dart` and + # `flutter` simultaneously they'll end up using different lock files + # and will corrupt each others' downloads. + # + # SHARED_NAME itself is prepared by the caller script. + upgrade_flutter 7< "$SHARED_NAME" BIN_NAME="$(basename "$PROG_NAME")" case "$BIN_NAME" in diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index f4e2e77a55886..9dbef7cf5c9ab 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -18,8 +18,10 @@ $flutterRoot = (Get-Item $progName).parent.parent.FullName $cachePath = "$flutterRoot\bin\cache" $dartSdkPath = "$cachePath\dart-sdk" +$dartSdkLicense = "$cachePath\LICENSE.dart_sdk_archive.md" $engineStamp = "$cachePath\engine-dart-sdk.stamp" $engineVersion = (Get-Content "$flutterRoot\bin\internal\engine.version") +$engineRealm = (Get-Content "$flutterRoot\bin\internal\engine.realm") $oldDartSdkPrefix = "dart-sdk.old" @@ -42,14 +44,24 @@ $dartSdkBaseUrl = $Env:FLUTTER_STORAGE_BASE_URL if (-not $dartSdkBaseUrl) { $dartSdkBaseUrl = "https://storage.googleapis.com" } +if ($engineRealm) { + $dartSdkBaseUrl = "$dartSdkBaseUrl/$engineRealm" +} $dartZipName = "dart-sdk-windows-x64.zip" $dartSdkUrl = "$dartSdkBaseUrl/flutter_infra_release/flutter/$engineVersion/$dartZipName" -if (Test-Path $dartSdkPath) { +if ((Test-Path $dartSdkPath) -or (Test-Path $dartSdkLicense)) { # Move old SDK to a new location instead of deleting it in case it is still in use (e.g. by IntelliJ). $oldDartSdkSuffix = 1 while (Test-Path "$cachePath\$oldDartSdkPrefix$oldDartSdkSuffix") { $oldDartSdkSuffix++ } - Rename-Item $dartSdkPath "$oldDartSdkPrefix$oldDartSdkSuffix" + + if (Test-Path $dartSdkPath) { + Rename-Item $dartSdkPath "$oldDartSdkPrefix$oldDartSdkSuffix" + } + + if (Test-Path $dartSdkLicense) { + Rename-Item $dartSdkLicense "$oldDartSdkPrefix$oldDartSdkSuffix.LICENSE.md" + } } New-Item $dartSdkPath -force -type directory | Out-Null $dartSdkZip = "$cachePath\$dartZipName" @@ -71,18 +83,21 @@ Catch { $ProgressPreference = $OriginalProgressPreference } -Write-Host "Expanding downloaded archive..." If (Get-Command 7z -errorAction SilentlyContinue) { + Write-Host "Expanding downloaded archive with 7z..." # The built-in unzippers are painfully slow. Use 7-Zip, if available. & 7z x $dartSdkZip "-o$cachePath" -bd | Out-Null } ElseIf (Get-Command 7za -errorAction SilentlyContinue) { + Write-Host "Expanding downloaded archive with 7za..." # Use 7-Zip's standalone version 7za.exe, if available. & 7za x $dartSdkZip "-o$cachePath" -bd | Out-Null } ElseIf (Get-Command Microsoft.PowerShell.Archive\Expand-Archive -errorAction SilentlyContinue) { + Write-Host "Expanding downloaded archive with PowerShell..." # Use PowerShell's built-in unzipper, if available (requires PowerShell 5+). $global:ProgressPreference='SilentlyContinue' Microsoft.PowerShell.Archive\Expand-Archive $dartSdkZip -DestinationPath $cachePath } Else { + Write-Host "Expanding downloaded archive with Windows..." # As last resort: fall back to the Windows GUI. $shell = New-Object -com shell.application $zip = $shell.NameSpace($dartSdkZip) @@ -94,5 +109,5 @@ If (Get-Command 7z -errorAction SilentlyContinue) { Remove-Item $dartSdkZip $engineVersion | Out-File $engineStamp -Encoding ASCII -# Try to delete all old SDKs. +# Try to delete all old SDKs and license files. Get-ChildItem -Path $cachePath | Where {$_.BaseName.StartsWith($oldDartSdkPrefix)} | Remove-Item -Recurse -ErrorAction SilentlyContinue diff --git a/bin/internal/update_dart_sdk.sh b/bin/internal/update_dart_sdk.sh index 0f1a238048886..8aed2f47861d7 100755 --- a/bin/internal/update_dart_sdk.sh +++ b/bin/internal/update_dart_sdk.sh @@ -20,6 +20,7 @@ DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk" DART_SDK_PATH_OLD="$DART_SDK_PATH.old" ENGINE_STAMP="$FLUTTER_ROOT/bin/cache/engine-dart-sdk.stamp" ENGINE_VERSION=`cat "$FLUTTER_ROOT/bin/internal/engine.version"` +ENGINE_REALM=`cat "$FLUTTER_ROOT/bin/internal/engine.realm"` OS="$(uname -s)" if [ ! -f "$ENGINE_STAMP" ] || [ "$ENGINE_VERSION" != `cat "$ENGINE_STAMP"` ]; then @@ -121,7 +122,7 @@ if [ ! -f "$ENGINE_STAMP" ] || [ "$ENGINE_VERSION" != `cat "$ENGINE_STAMP"` ]; t FIND=find fi - DART_SDK_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.googleapis.com}" + DART_SDK_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.googleapis.com}${ENGINE_REALM:+/$ENGINE_REALM}" DART_SDK_URL="$DART_SDK_BASE_URL/flutter_infra_release/flutter/$ENGINE_VERSION/$DART_ZIP_NAME" # if the sdk path exists, copy it to a temporary location diff --git a/dev/a11y_assessments/.gitignore b/dev/a11y_assessments/.gitignore new file mode 100644 index 0000000000000..24476c5d1eb55 --- /dev/null +++ b/dev/a11y_assessments/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/dev/a11y_assessments/.metadata b/dev/a11y_assessments/.metadata new file mode 100644 index 0000000000000..f35b49906d8e5 --- /dev/null +++ b/dev/a11y_assessments/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "b9c3f1f74c075a1766fd74418b5d79f528cf8c74" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: android + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: ios + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: linux + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: macos + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: web + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + - platform: windows + create_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + base_revision: b9c3f1f74c075a1766fd74418b5d79f528cf8c74 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/dev/a11y_assessments/README.md b/dev/a11y_assessments/README.md new file mode 100644 index 0000000000000..1f8a64cafb983 --- /dev/null +++ b/dev/a11y_assessments/README.md @@ -0,0 +1,3 @@ +# a11y_assessments + +An application to conduct accessibility assessments. diff --git a/dev/a11y_assessments/analysis_options.yaml b/dev/a11y_assessments/analysis_options.yaml new file mode 100644 index 0000000000000..f04c6cf0f30d4 --- /dev/null +++ b/dev/a11y_assessments/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/dev/a11y_assessments/android/.gitignore b/dev/a11y_assessments/android/.gitignore new file mode 100644 index 0000000000000..6f568019d3c69 --- /dev/null +++ b/dev/a11y_assessments/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/dev/a11y_assessments/android/app/build.gradle b/dev/a11y_assessments/android/app/build.gradle new file mode 100644 index 0000000000000..c60f9f431e37e --- /dev/null +++ b/dev/a11y_assessments/android/app/build.gradle @@ -0,0 +1,71 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.a11y_assessments" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "dev.flutter.a11yassessments" + // 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 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/dev/a11y_assessments/android/app/src/debug/AndroidManifest.xml b/dev/a11y_assessments/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000000..e00f903eae2aa --- /dev/null +++ b/dev/a11y_assessments/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev/a11y_assessments/android/app/src/main/AndroidManifest.xml b/dev/a11y_assessments/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..a1c931a7a55b9 --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/android/app/src/main/kotlin/com/example/a11y_assessments/MainActivity.kt b/dev/a11y_assessments/android/app/src/main/kotlin/com/example/a11y_assessments/MainActivity.kt new file mode 100644 index 0000000000000..43273ce8d8255 --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/kotlin/com/example/a11y_assessments/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.a11y_assessments + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/dev/a11y_assessments/android/app/src/main/res/drawable-v21/launch_background.xml b/dev/a11y_assessments/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000000..9f19e2f90407e --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/dev/a11y_assessments/android/app/src/main/res/drawable/launch_background.xml b/dev/a11y_assessments/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000000..3727f9e00a029 --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/dev/a11y_assessments/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/a11y_assessments/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000..db77bb4b7b090 Binary files /dev/null and b/dev/a11y_assessments/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/dev/a11y_assessments/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/a11y_assessments/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000..17987b79bb8a3 Binary files /dev/null and b/dev/a11y_assessments/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/dev/a11y_assessments/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dev/a11y_assessments/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000..09d4391482be6 Binary files /dev/null and b/dev/a11y_assessments/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/dev/a11y_assessments/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/a11y_assessments/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000..d5f1c8d34e7a8 Binary files /dev/null and b/dev/a11y_assessments/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/dev/a11y_assessments/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/a11y_assessments/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000..4d6372eebdb28 Binary files /dev/null and b/dev/a11y_assessments/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/dev/a11y_assessments/android/app/src/main/res/values-night/styles.xml b/dev/a11y_assessments/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000000..f3ab3e83cd361 --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/dev/a11y_assessments/android/app/src/main/res/values/styles.xml b/dev/a11y_assessments/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000000..9a0ead3c04f75 --- /dev/null +++ b/dev/a11y_assessments/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/dev/a11y_assessments/android/app/src/profile/AndroidManifest.xml b/dev/a11y_assessments/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000000..e00f903eae2aa --- /dev/null +++ b/dev/a11y_assessments/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev/a11y_assessments/android/build.gradle b/dev/a11y_assessments/android/build.gradle new file mode 100644 index 0000000000000..d93b7eb6e10e2 --- /dev/null +++ b/dev/a11y_assessments/android/build.gradle @@ -0,0 +1,35 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/dev/a11y_assessments/android/gradle.properties b/dev/a11y_assessments/android/gradle.properties new file mode 100644 index 0000000000000..598d13fee4463 --- /dev/null +++ b/dev/a11y_assessments/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/dev/a11y_assessments/android/gradle/wrapper/gradle-wrapper.properties b/dev/a11y_assessments/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000..3c472b99c6f35 --- /dev/null +++ b/dev/a11y_assessments/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle b/dev/a11y_assessments/android/settings.gradle similarity index 80% rename from packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle rename to dev/a11y_assessments/android/settings.gradle index 55c4ca8b109a4..13766f66084b4 100644 --- a/packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle +++ b/dev/a11y_assessments/android/settings.gradle @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + pluginManagement { def flutterSdkPath = { def properties = new Properties() diff --git a/dev/a11y_assessments/ios/.gitignore b/dev/a11y_assessments/ios/.gitignore new file mode 100644 index 0000000000000..7a7f9873ad7dc --- /dev/null +++ b/dev/a11y_assessments/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/dev/a11y_assessments/ios/Flutter/AppFrameworkInfo.plist b/dev/a11y_assessments/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000000000..9625e105df39e --- /dev/null +++ b/dev/a11y_assessments/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/dev/a11y_assessments/ios/Flutter/Debug.xcconfig b/dev/a11y_assessments/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000000000..592ceee85b89b --- /dev/null +++ b/dev/a11y_assessments/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/dev/a11y_assessments/ios/Flutter/Release.xcconfig b/dev/a11y_assessments/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000000000..592ceee85b89b --- /dev/null +++ b/dev/a11y_assessments/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/dev/a11y_assessments/ios/Runner.xcodeproj/project.pbxproj b/dev/a11y_assessments/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000..142bb384b0dd1 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,613 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 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; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.a11yAssessments; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.a11yAssessments; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.a11yAssessments; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000..919434a6254f0 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000000..f9b0d7c5ea15f --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/dev/a11y_assessments/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dev/a11y_assessments/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000000..87131a09bea5d --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/ios/Runner.xcworkspace/contents.xcworkspacedata b/dev/a11y_assessments/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000..1d526a16ed0f1 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000000..f9b0d7c5ea15f --- /dev/null +++ b/dev/a11y_assessments/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/dev/a11y_assessments/ios/Runner/AppDelegate.swift b/dev/a11y_assessments/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000000000..d815fed684a8a --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000000..d36b1fab2d9de --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000..b6a5d3f48e4e0 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000..28c6bf03016f6 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000..2ccbfd967d969 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000..f091b6b0bca85 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000..4cde12118dda4 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000..d0ef06e7edb86 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000000000..dcdc2306c2850 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000000000..2ccbfd967d969 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000..c8f9ed8f5cee1 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000..a6d6b8609df07 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000..a6d6b8609df07 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000..75b2d164a5a98 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000..c4df70d39da79 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000000..6a84f41e14e27 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000..d0e1f58536026 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000000000..0bedcf2fd4678 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000..9da19eacad3b0 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000..9da19eacad3b0 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000..9da19eacad3b0 Binary files /dev/null and b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000000000..89c2725b70f18 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/dev/a11y_assessments/ios/Runner/Base.lproj/LaunchScreen.storyboard b/dev/a11y_assessments/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000000..f2e259c7c9390 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/ios/Runner/Base.lproj/Main.storyboard b/dev/a11y_assessments/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000000000..f3c28516fb38e --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/ios/Runner/Info.plist b/dev/a11y_assessments/ios/Runner/Info.plist new file mode 100644 index 0000000000000..74b9bb41e71fc --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + A11y Assessments + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + a11y_assessments + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/dev/a11y_assessments/ios/Runner/Runner-Bridging-Header.h b/dev/a11y_assessments/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000000000..95b7baf386d06 --- /dev/null +++ b/dev/a11y_assessments/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "GeneratedPluginRegistrant.h" diff --git a/dev/a11y_assessments/ios/RunnerTests/RunnerTests.swift b/dev/a11y_assessments/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000000..6bfa573763f8b --- /dev/null +++ b/dev/a11y_assessments/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/dev/a11y_assessments/lib/main.dart b/dev/a11y_assessments/lib/main.dart new file mode 100644 index 0000000000000..5b7a14d767a28 --- /dev/null +++ b/dev/a11y_assessments/lib/main.dart @@ -0,0 +1,80 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'use_cases/use_cases.dart'; + +// TODO(yjbanov): https://github.com/flutter/flutter/issues/83809 +// Currently this app (as most Flutter Web apps) relies on the +// `autofocus` property to guide the a11y focus when navigating +// across routes (screen transitions, dialogs, etc). We may want +// to revisit this after we figure out a long-term story for a11y +// focus. See also https://github.com/flutter/flutter/issues/97747 +void main() { + runApp(const App()); + if (kIsWeb) { + SemanticsBinding.instance.ensureSemantics(); + } +} + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + + final Map routes = Map.fromEntries( + useCases.map((UseCase useCase) => MapEntry(useCase.route, useCase.build)), + ); + return MaterialApp( + title: 'Accessibility Assessments', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + routes: { + '/': (_) => const HomePage(), + ...routes + }, + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + Widget _buildUseCaseItem(int index, UseCase useCase) { + return Padding( + padding: const EdgeInsets.all(10), + child: Builder( + builder: (BuildContext context) { + return TextButton( + autofocus: index == 0, + key: Key(useCase.name), + onPressed: () => Navigator.of(context).pushNamed(useCase.route), + child: Text(useCase.name), + ); + } + ) + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Accessibility Assessments')), + body: Center( + child: ListView( + children: List.generate( + useCases.length, + (int index) => _buildUseCaseItem(index, useCases[index]), + ), + ), + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/check_box_list_tile.dart b/dev/a11y_assessments/lib/use_cases/check_box_list_tile.dart new file mode 100644 index 0000000000000..b21f5d0c95f03 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/check_box_list_tile.dart @@ -0,0 +1,59 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class CheckBoxListTile extends UseCase { + + @override + String get name => 'CheckBoxListTile'; + + @override + String get route => '/check-box-list-tile'; + + @override + Widget build(BuildContext context) => _MainWidget(); +} + +class _MainWidget extends StatefulWidget { + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + bool _checked = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('CheckBoxListTile')), + body: ListView( + children: [ + CheckboxListTile( + autofocus: true, + value: _checked, + onChanged: (bool? value) { + setState(() { + _checked = value!; + }); + }, + title: const Text('a check box list title'), + ), + CheckboxListTile( + value: _checked, + onChanged: (bool? value) { + setState(() { + _checked = value!; + }); + }, + title: const Text('a disabled check box list title'), + enabled: false, + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/date_picker.dart b/dev/a11y_assessments/lib/use_cases/date_picker.dart new file mode 100644 index 0000000000000..a7192abc475c4 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/date_picker.dart @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class DatePickerUseCase extends UseCase { + + @override + String get name => 'DatePicker'; + + @override + String get route => '/date-picker'; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatefulWidget { + const _MainWidget(); + + @override + State<_MainWidget> createState() => _MainWidgetState(); +} + +class _MainWidgetState extends State<_MainWidget> { + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('DatePicker'), + ), + body: Center( + child: TextButton( + autofocus: true, + onPressed: () => showDatePicker( + context: context, + initialEntryMode: DatePickerEntryMode.calendarOnly, + initialDate: DateTime.now(), + firstDate: DateTime.now().subtract(const Duration(days: 365)), + lastDate: DateTime.now().add(const Duration(days: 365)), + ), + child: const Text('Show Date Picker'), + ), + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/dialog.dart b/dev/a11y_assessments/lib/use_cases/dialog.dart new file mode 100644 index 0000000000000..e110259f5c633 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/dialog.dart @@ -0,0 +1,61 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class DialogUseCase extends UseCase { + + @override + String get name => 'Dialog'; + + @override + String get route => '/dialog'; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatelessWidget { + const _MainWidget(); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Dialog'), + ), + body: Center( + child: TextButton( + autofocus: true, + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('This is a typical dialog.'), + const SizedBox(height: 15), + TextButton( + autofocus: true, + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Close'), + ), + ], + ), + ), + ), + ), + child: const Text('Show Dialog'), + ), + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/slider.dart b/dev/a11y_assessments/lib/use_cases/slider.dart new file mode 100644 index 0000000000000..8dbd398bf4e3b --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/slider.dart @@ -0,0 +1,54 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class SliderUseCase extends UseCase { + + @override + String get name => 'Slider'; + + @override + String get route => '/slider'; + + @override + Widget build(BuildContext context) => const MainWidget(); +} + +class MainWidget extends StatefulWidget { + const MainWidget({super.key}); + + @override + State createState() => MainWidgetState(); +} + +class MainWidgetState extends State { + double currentSliderValue = 20; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Slider'), + ), + body: Center( + child: Slider( + autofocus: true, + value: currentSliderValue, + max: 100, + divisions: 5, + label: currentSliderValue.round().toString(), + onChanged: (double value) { + setState(() { + currentSliderValue = value; + }); + }, + ), + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/text_field.dart b/dev/a11y_assessments/lib/use_cases/text_field.dart new file mode 100644 index 0000000000000..0038cb52f25cc --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/text_field.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class TextFieldUseCase extends UseCase { + + @override + String get name => 'TextField'; + + @override + String get route => '/text-field'; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatelessWidget { + const _MainWidget(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('TextField'), + ), + body: ListView( + children: [ + const TextField( + key: Key('enabled text field'), + autofocus: true, + decoration: InputDecoration( + labelText: 'Email', + suffixText: '@gmail.com', + hintText: 'Enter your email', + ), + ), + TextField( + key: const Key('disabled text field'), + decoration: const InputDecoration( + labelText: 'Email', + suffixText: '@gmail.com', + hintText: 'Enter your email', + ), + enabled: false, + controller: TextEditingController(text: 'xyz'), + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/text_field_password.dart b/dev/a11y_assessments/lib/use_cases/text_field_password.dart new file mode 100644 index 0000000000000..3c6152dd34885 --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/text_field_password.dart @@ -0,0 +1,55 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'use_cases.dart'; + +class TextFieldPasswordUseCase extends UseCase { + + @override + String get name => 'TextField password'; + + @override + String get route => '/text-field-password'; + + @override + Widget build(BuildContext context) => const _MainWidget(); +} + +class _MainWidget extends StatelessWidget { + const _MainWidget(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('TextField password'), + ), + body: ListView( + children: const [ + TextField( + key: Key('enabled password'), + autofocus: true, + decoration: InputDecoration( + labelText: 'Password', + hintText: 'Enter your password', + ), + obscureText: true, + ), + TextField( + key: Key('disabled password'), + decoration: InputDecoration( + labelText: 'Password', + hintText: 'Enter your password', + ), + enabled: false, + obscureText: true, + ), + ], + ), + ); + } +} diff --git a/dev/a11y_assessments/lib/use_cases/use_cases.dart b/dev/a11y_assessments/lib/use_cases/use_cases.dart new file mode 100644 index 0000000000000..056f9e2b11a4b --- /dev/null +++ b/dev/a11y_assessments/lib/use_cases/use_cases.dart @@ -0,0 +1,27 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +import 'check_box_list_tile.dart'; +import 'date_picker.dart'; +import 'dialog.dart'; +import 'slider.dart'; +import 'text_field.dart'; +import 'text_field_password.dart'; + +abstract class UseCase { + String get name; + String get route; + Widget build(BuildContext context); +} + +final List useCases = [ + CheckBoxListTile(), + DialogUseCase(), + SliderUseCase(), + TextFieldUseCase(), + TextFieldPasswordUseCase(), + DatePickerUseCase(), +]; diff --git a/dev/a11y_assessments/linux/.gitignore b/dev/a11y_assessments/linux/.gitignore new file mode 100644 index 0000000000000..d3896c98444fb --- /dev/null +++ b/dev/a11y_assessments/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/dev/a11y_assessments/linux/CMakeLists.txt b/dev/a11y_assessments/linux/CMakeLists.txt new file mode 100644 index 0000000000000..07dc63e3803d9 --- /dev/null +++ b/dev/a11y_assessments/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "a11y_assessments") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.a11y_assessments") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/dev/a11y_assessments/linux/flutter/CMakeLists.txt b/dev/a11y_assessments/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..d5bd01648a96d --- /dev/null +++ b/dev/a11y_assessments/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/dev/a11y_assessments/linux/main.cc b/dev/a11y_assessments/linux/main.cc new file mode 100644 index 0000000000000..281a29e16b599 --- /dev/null +++ b/dev/a11y_assessments/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/dev/a11y_assessments/linux/my_application.cc b/dev/a11y_assessments/linux/my_application.cc new file mode 100644 index 0000000000000..05e5133c60566 --- /dev/null +++ b/dev/a11y_assessments/linux/my_application.cc @@ -0,0 +1,108 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "a11y_assessments"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "a11y_assessments"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/dev/a11y_assessments/linux/my_application.h b/dev/a11y_assessments/linux/my_application.h new file mode 100644 index 0000000000000..8c66ec485434d --- /dev/null +++ b/dev/a11y_assessments/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/dev/a11y_assessments/macos/.gitignore b/dev/a11y_assessments/macos/.gitignore new file mode 100644 index 0000000000000..746adbb6b9e14 --- /dev/null +++ b/dev/a11y_assessments/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/dev/a11y_assessments/macos/Flutter/Flutter-Debug.xcconfig b/dev/a11y_assessments/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000000..c2efd0b608ba8 --- /dev/null +++ b/dev/a11y_assessments/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/dev/a11y_assessments/macos/Flutter/Flutter-Release.xcconfig b/dev/a11y_assessments/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000000..c2efd0b608ba8 --- /dev/null +++ b/dev/a11y_assessments/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/dev/a11y_assessments/macos/Runner.xcodeproj/project.pbxproj b/dev/a11y_assessments/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000..1da16b798e668 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,695 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* a11y_assessments.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "a11y_assessments.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* a11y_assessments.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* a11y_assessments.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a11y_assessments.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a11y_assessments"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a11y_assessments.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a11y_assessments"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a11y_assessments.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a11y_assessments"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/dev/a11y_assessments/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dev/a11y_assessments/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/dev/a11y_assessments/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dev/a11y_assessments/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dev/a11y_assessments/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000000..f33a76e7500bc --- /dev/null +++ b/dev/a11y_assessments/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/macos/Runner.xcworkspace/contents.xcworkspacedata b/dev/a11y_assessments/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000..1d526a16ed0f1 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dev/a11y_assessments/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dev/a11y_assessments/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000000..18d981003d68d --- /dev/null +++ b/dev/a11y_assessments/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dev/a11y_assessments/macos/Runner/AppDelegate.swift b/dev/a11y_assessments/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000000..d080d41951d35 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000000..a2ec33f19f110 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000..3c4935a7ca84f Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000..ed4cc16421680 Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000..483be61389733 Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000..bcbf36df2f2aa Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000000000..9c0a652864769 Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000..e71a726136a47 Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000..8a31fe2dd3f91 Binary files /dev/null and b/dev/a11y_assessments/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/dev/a11y_assessments/macos/Runner/Base.lproj/MainMenu.xib b/dev/a11y_assessments/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000000..80e867a4e06b4 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/macos/Runner/Configs/AppInfo.xcconfig b/dev/a11y_assessments/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000000..d209bdba247d7 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = a11y_assessments + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.a11yAssessments + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/dev/a11y_assessments/macos/Runner/Configs/Debug.xcconfig b/dev/a11y_assessments/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000000..36b0fd9464f45 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/dev/a11y_assessments/macos/Runner/Configs/Release.xcconfig b/dev/a11y_assessments/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000000..dff4f49561c81 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/dev/a11y_assessments/macos/Runner/Configs/Warnings.xcconfig b/dev/a11y_assessments/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000000..42bcbf4780b18 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/dev/a11y_assessments/macos/Runner/DebugProfile.entitlements b/dev/a11y_assessments/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000000..dddb8a30c851e --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/dev/a11y_assessments/macos/Runner/Info.plist b/dev/a11y_assessments/macos/Runner/Info.plist new file mode 100644 index 0000000000000..4789daa6a443e --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/dev/a11y_assessments/macos/Runner/MainFlutterWindow.swift b/dev/a11y_assessments/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000000..a6a6b9af83072 --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/dev/a11y_assessments/macos/Runner/Release.entitlements b/dev/a11y_assessments/macos/Runner/Release.entitlements new file mode 100644 index 0000000000000..852fa1a4728ae --- /dev/null +++ b/dev/a11y_assessments/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/dev/a11y_assessments/macos/RunnerTests/RunnerTests.swift b/dev/a11y_assessments/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000000..102c76116822b --- /dev/null +++ b/dev/a11y_assessments/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/dev/a11y_assessments/pubspec.yaml b/dev/a11y_assessments/pubspec.yaml new file mode 100644 index 0000000000000..c5670ebb97cc1 --- /dev/null +++ b/dev/a11y_assessments/pubspec.yaml @@ -0,0 +1,38 @@ +name: a11y_assessments +description: A new Flutter project + +environment: + sdk: '>=3.2.0-22.0.dev <4.0.0' + +dependencies: + flutter: + sdk: flutter + + characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + + async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +flutter: + uses-material-design: true + +# PUBSPEC CHECKSUM: dc9e diff --git a/dev/a11y_assessments/test/accessibility_guideline_test.dart b/dev/a11y_assessments/test/accessibility_guideline_test.dart new file mode 100644 index 0000000000000..7bb1c56fcc3c9 --- /dev/null +++ b/dev/a11y_assessments/test/accessibility_guideline_test.dart @@ -0,0 +1,23 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/main.dart'; +import 'package:a11y_assessments/use_cases/use_cases.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + for (final UseCase useCase in useCases) { + testWidgets('testing accessibility guideline for ${useCase.name}', (WidgetTester tester) async { + await tester.pumpWidget(const App()); + await tester.tap(find.byKey(Key(useCase.name))); + await tester.pumpAndSettle(); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); + await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + }); + } +} diff --git a/dev/a11y_assessments/test/date_picker_test.dart b/dev/a11y_assessments/test/date_picker_test.dart new file mode 100644 index 0000000000000..a72c0ed7fd3aa --- /dev/null +++ b/dev/a11y_assessments/test/date_picker_test.dart @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/date_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('date picker can run', (WidgetTester tester) async { + await pumpsUseCase(tester, DatePickerUseCase()); + expect(find.text('Show Date Picker'), findsOneWidget); + + await tester.tap(find.text('Show Date Picker')); + await tester.pumpAndSettle(); + expect(find.byType(DatePickerDialog), findsOneWidget); + }); +} diff --git a/dev/a11y_assessments/test/dialog_test.dart b/dev/a11y_assessments/test/dialog_test.dart new file mode 100644 index 0000000000000..98d8c71bb7f91 --- /dev/null +++ b/dev/a11y_assessments/test/dialog_test.dart @@ -0,0 +1,23 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/dialog.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('dialog can run', (WidgetTester tester) async { + await pumpsUseCase(tester, DialogUseCase()); + expect(find.text('Show Dialog'), findsOneWidget); + + await tester.tap(find.text('Show Dialog')); + await tester.pumpAndSettle(); + expect(find.text('This is a typical dialog.'), findsOneWidget); + + await tester.tap(find.text('Close')); + await tester.pumpAndSettle(); + expect(find.text('Show Dialog'), findsOneWidget); + }); +} diff --git a/dev/a11y_assessments/test/slider_test.dart b/dev/a11y_assessments/test/slider_test.dart new file mode 100644 index 0000000000000..a8769e844e03a --- /dev/null +++ b/dev/a11y_assessments/test/slider_test.dart @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('slider can run', (WidgetTester tester) async { + await pumpsUseCase(tester, SliderUseCase()); + expect(find.byType(Slider), findsOneWidget); + + await tester.tapAt(tester.getCenter(find.byType(Slider))); + await tester.pumpAndSettle(); + + final MainWidgetState state = tester.state(find.byType(MainWidget)); + expect(state.currentSliderValue, 60); + }); +} diff --git a/dev/a11y_assessments/test/test_utils.dart b/dev/a11y_assessments/test/test_utils.dart new file mode 100644 index 0000000000000..8b2ae281dbeeb --- /dev/null +++ b/dev/a11y_assessments/test/test_utils.dart @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/use_cases.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future pumpsUseCase(WidgetTester tester, UseCase useCase) async { + await tester.pumpWidget(MaterialApp( + home: Builder( + builder: (BuildContext context) { + return useCase.build(context); + }, + ), + )); +} diff --git a/dev/a11y_assessments/test/text_field_password_test.dart b/dev/a11y_assessments/test/text_field_password_test.dart new file mode 100644 index 0000000000000..90e4fd6a6a078 --- /dev/null +++ b/dev/a11y_assessments/test/text_field_password_test.dart @@ -0,0 +1,33 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/text_field_password.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('text field password can run', (WidgetTester tester) async { + await pumpsUseCase(tester, TextFieldPasswordUseCase()); + expect(find.byType(TextField), findsExactly(2)); + + // Test the enabled password + { + final Finder finder = find.byKey(const Key('enabled password')); + await tester.tap(finder); + await tester.pumpAndSettle(); + await tester.enterText(finder, 'abc'); + await tester.pumpAndSettle(); + expect(find.text('abc'), findsOneWidget); + } + + // Test the disabled password + { + final Finder finder = find.byKey(const Key('disabled password')); + final TextField passwordField = tester.widget(finder); + expect(passwordField.enabled, isFalse); + } + }); +} diff --git a/dev/a11y_assessments/test/text_field_test.dart b/dev/a11y_assessments/test/text_field_test.dart new file mode 100644 index 0000000000000..0b2ea4ad7a8c2 --- /dev/null +++ b/dev/a11y_assessments/test/text_field_test.dart @@ -0,0 +1,33 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:a11y_assessments/use_cases/text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('text field can run', (WidgetTester tester) async { + await pumpsUseCase(tester, TextFieldUseCase()); + expect(find.byType(TextField), findsExactly(2)); + + // Test the enabled text field + { + final Finder finder = find.byKey(const Key('enabled text field')); + await tester.tap(finder); + await tester.pumpAndSettle(); + await tester.enterText(finder, 'abc'); + await tester.pumpAndSettle(); + expect(find.text('abc'), findsOneWidget); + } + + // Test the disabled text field + { + final Finder finder = find.byKey(const Key('disabled text field')); + final TextField textField = tester.widget(finder); + expect(textField.enabled, isFalse); + } + }); +} diff --git a/dev/a11y_assessments/web/favicon.png b/dev/a11y_assessments/web/favicon.png new file mode 100644 index 0000000000000..8aaa46ac1ae21 Binary files /dev/null and b/dev/a11y_assessments/web/favicon.png differ diff --git a/dev/a11y_assessments/web/icons/Icon-192.png b/dev/a11y_assessments/web/icons/Icon-192.png new file mode 100644 index 0000000000000..b749bfef07473 Binary files /dev/null and b/dev/a11y_assessments/web/icons/Icon-192.png differ diff --git a/dev/a11y_assessments/web/icons/Icon-512.png b/dev/a11y_assessments/web/icons/Icon-512.png new file mode 100644 index 0000000000000..88cfd48dff116 Binary files /dev/null and b/dev/a11y_assessments/web/icons/Icon-512.png differ diff --git a/dev/a11y_assessments/web/icons/Icon-maskable-192.png b/dev/a11y_assessments/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000..b749bfef07473 Binary files /dev/null and b/dev/a11y_assessments/web/icons/Icon-maskable-192.png differ diff --git a/dev/a11y_assessments/web/icons/Icon-maskable-512.png b/dev/a11y_assessments/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000..88cfd48dff116 Binary files /dev/null and b/dev/a11y_assessments/web/icons/Icon-maskable-512.png differ diff --git a/dev/a11y_assessments/web/index.html b/dev/a11y_assessments/web/index.html new file mode 100644 index 0000000000000..a074dee382c97 --- /dev/null +++ b/dev/a11y_assessments/web/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + a11y_assessments + + + + + + + + + + diff --git a/dev/a11y_assessments/web/manifest.json b/dev/a11y_assessments/web/manifest.json new file mode 100644 index 0000000000000..b2bfc8ba99bf1 --- /dev/null +++ b/dev/a11y_assessments/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "a11y_assessments", + "short_name": "a11y_assessments", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/dev/a11y_assessments/windows/.gitignore b/dev/a11y_assessments/windows/.gitignore new file mode 100644 index 0000000000000..d492d0d98c8fd --- /dev/null +++ b/dev/a11y_assessments/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/dev/a11y_assessments/windows/CMakeLists.txt b/dev/a11y_assessments/windows/CMakeLists.txt new file mode 100644 index 0000000000000..b2bc836676f14 --- /dev/null +++ b/dev/a11y_assessments/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(a11y_assessments LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "a11y_assessments") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/dev/a11y_assessments/windows/flutter/CMakeLists.txt b/dev/a11y_assessments/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..903f4899d6fce --- /dev/null +++ b/dev/a11y_assessments/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/dev/a11y_assessments/windows/runner/CMakeLists.txt b/dev/a11y_assessments/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000000..394917c053a04 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/dev/a11y_assessments/windows/runner/Runner.rc b/dev/a11y_assessments/windows/runner/Runner.rc new file mode 100644 index 0000000000000..9b5eba68d5484 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "a11y_assessments" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "a11y_assessments" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "a11y_assessments.exe" "\0" + VALUE "ProductName", "a11y_assessments" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/dev/a11y_assessments/windows/runner/flutter_window.cpp b/dev/a11y_assessments/windows/runner/flutter_window.cpp new file mode 100644 index 0000000000000..252aa267868b6 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/flutter_window.cpp @@ -0,0 +1,75 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/dev/a11y_assessments/windows/runner/flutter_window.h b/dev/a11y_assessments/windows/runner/flutter_window.h new file mode 100644 index 0000000000000..bbc5836c018a2 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/dev/a11y_assessments/windows/runner/main.cpp b/dev/a11y_assessments/windows/runner/main.cpp new file mode 100644 index 0000000000000..125d42ba57ff2 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/main.cpp @@ -0,0 +1,47 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"a11y_assessments", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/dev/a11y_assessments/windows/runner/resource.h b/dev/a11y_assessments/windows/runner/resource.h new file mode 100644 index 0000000000000..c245ff19cb580 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/resource.h @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/dev/a11y_assessments/windows/runner/resources/app_icon.ico b/dev/a11y_assessments/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000..2e4cc829b07d6 Binary files /dev/null and b/dev/a11y_assessments/windows/runner/resources/app_icon.ico differ diff --git a/dev/a11y_assessments/windows/runner/runner.exe.manifest b/dev/a11y_assessments/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000000..a42ea7687cb67 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/dev/a11y_assessments/windows/runner/utils.cpp b/dev/a11y_assessments/windows/runner/utils.cpp new file mode 100644 index 0000000000000..f677d8d5f7141 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/dev/a11y_assessments/windows/runner/utils.h b/dev/a11y_assessments/windows/runner/utils.h new file mode 100644 index 0000000000000..54414c989ba71 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/dev/a11y_assessments/windows/runner/win32_window.cpp b/dev/a11y_assessments/windows/runner/win32_window.cpp new file mode 100644 index 0000000000000..14a183d7eaa27 --- /dev/null +++ b/dev/a11y_assessments/windows/runner/win32_window.cpp @@ -0,0 +1,292 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/dev/a11y_assessments/windows/runner/win32_window.h b/dev/a11y_assessments/windows/runner/win32_window.h new file mode 100644 index 0000000000000..bb93e8879162e --- /dev/null +++ b/dev/a11y_assessments/windows/runner/win32_window.h @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/dev/automated_tests/pubspec.yaml b/dev/automated_tests/pubspec.yaml index e930a775edacd..cbb1f6f65f8a8 100644 --- a/dev/automated_tests/pubspec.yaml +++ b/dev/automated_tests/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_automated_tests environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -12,17 +12,17 @@ dependencies: sdk: flutter integration_test: sdk: flutter - platform: 3.1.0 - test: 1.24.3 + platform: 3.1.2 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -37,7 +37,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -52,19 +52,19 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -72,4 +72,4 @@ flutter: assets: - icon/test.png -# PUBSPEC CHECKSUM: 8050 +# PUBSPEC CHECKSUM: 29b0 diff --git a/dev/benchmarks/complex_layout/android/gradle.properties b/dev/benchmarks/complex_layout/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/complex_layout/android/gradle.properties +++ b/dev/benchmarks/complex_layout/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index fe6ed3df54dc5..89380badb866a 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -2,7 +2,7 @@ name: complex_layout description: A benchmark of a relatively complex layout. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -19,33 +19,33 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 integration_test: sdk: flutter - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -70,11 +70,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -83,4 +83,4 @@ flutter: - packages/flutter_gallery_assets/people/square/ali.png - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png -# PUBSPEC CHECKSUM: 1c29 +# PUBSPEC CHECKSUM: d287 diff --git a/dev/benchmarks/complex_layout/windows/flutter/CMakeLists.txt b/dev/benchmarks/complex_layout/windows/flutter/CMakeLists.txt index 10873dd1af99c..c8f7abf1ebea9 100644 --- a/dev/benchmarks/complex_layout/windows/flutter/CMakeLists.txt +++ b/dev/benchmarks/complex_layout/windows/flutter/CMakeLists.txt @@ -14,6 +14,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -96,7 +101,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/dev/benchmarks/macrobenchmarks/android/gradle.properties b/dev/benchmarks/macrobenchmarks/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/macrobenchmarks/android/gradle.properties +++ b/dev/benchmarks/macrobenchmarks/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/project.pbxproj b/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/project.pbxproj index e319fa8e46387..4cdd00ac38a27 100644 --- a/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/project.pbxproj +++ b/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/project.pbxproj @@ -132,7 +132,6 @@ 1842E3C5134E282C88C541B8 /* Pods-Runner.release.xcconfig */, F269DC09D76325C7B7334781 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -351,7 +350,10 @@ DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.macrobenchmarks; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; @@ -464,9 +466,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.macrobenchmarks; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; @@ -479,9 +485,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.example.macrobenchmarks; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9997cfd251cd2..098cec7a297ed 100644 --- a/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/dev/benchmarks/macrobenchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,10 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + @@ -63,8 +60,6 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - const LargeImageChangerPage(), kLargeImagesRouteName: (BuildContext context) => const LargeImagesPage(), kTextRouteName: (BuildContext context) => const TextPage(), + kPathTessellationRouteName: (BuildContext context) => const PathTessellationPage(), kFullscreenTextRouteName: (BuildContext context) => const TextFieldPage(), kAnimatedPlaceholderRouteName: (BuildContext context) => const AnimatedPlaceholderPage(), kClipperCacheRouteName: (BuildContext context) => const ClipperCachePage(), @@ -89,6 +95,10 @@ class MacrobenchmarksApp extends StatelessWidget { kAnimatedBlurBackdropFilter: (BuildContext context) => const AnimatedBlurBackdropFilter(), kSlidersRouteName: (BuildContext context) => const SlidersPage(), kDrawPointsPageRougeName: (BuildContext context) => const DrawPointsPage(), + kDrawVerticesPageRouteName: (BuildContext context) => const DrawVerticesPage(), + kDrawAtlasPageRouteName: (BuildContext context) => const DrawAtlasPage(), + kAnimatedAdvancedBlend: (BuildContext context) => const AnimatedAdvancedBlend(), + kVeryLongPictureScrollingRouteName: (BuildContext context) => const VeryLongPictureScrollingPerf(), }, ); } @@ -162,6 +172,13 @@ class HomePage extends StatelessWidget { Navigator.pushNamed(context, kLargeImagesRouteName); }, ), + ElevatedButton( + key: const Key(kPathTessellationRouteName), + child: const Text('Path Tessellation'), + onPressed: () { + Navigator.pushNamed(context, kPathTessellationRouteName); + }, + ), ElevatedButton( key: const Key(kTextRouteName), child: const Text('Text'), @@ -336,7 +353,35 @@ class HomePage extends StatelessWidget { onPressed: () { Navigator.pushNamed(context, kDrawPointsPageRougeName); }, - ) + ), + ElevatedButton( + key: const Key(kDrawVerticesPageRouteName), + child: const Text('Draw Vertices'), + onPressed: () { + Navigator.pushNamed(context, kDrawVerticesPageRouteName); + }, + ), + ElevatedButton( + key: const Key(kDrawAtlasPageRouteName), + child: const Text('Draw Atlas'), + onPressed: () { + Navigator.pushNamed(context, kDrawAtlasPageRouteName); + }, + ), + ElevatedButton( + key: const Key(kAnimatedAdvancedBlend), + child: const Text('Animated Advanced Blend'), + onPressed: () { + Navigator.pushNamed(context, kAnimatedAdvancedBlend); + }, + ), + ElevatedButton( + key: const Key(kVeryLongPictureScrollingRouteName), + child: const Text('Very Long Picture Scrolling'), + onPressed: () { + Navigator.pushNamed(context, kVeryLongPictureScrollingRouteName); + }, + ), ], ), ); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/animated_advanced_blend.dart b/dev/benchmarks/macrobenchmarks/lib/src/animated_advanced_blend.dart new file mode 100644 index 0000000000000..c3ea0f82171db --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/animated_advanced_blend.dart @@ -0,0 +1,83 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class _MultiplyPainter extends CustomPainter { + _MultiplyPainter(this._color); + + final Color _color; + + @override + void paint(Canvas canvas, Size size) { + const int xDenominator = 2; + const int yDenominator = 10; + final double width = size.width / xDenominator; + final double height = size.height / yDenominator; + + for (int y = 0; y < yDenominator; y++) { + for (int x = 0; x < xDenominator; x++) { + final Rect rect = Offset(x * width, y * height) & Size(width, height); + final Paint basePaint = Paint() + ..color = Color.fromARGB( + (((x + 1) * width) / size.width * 255.0).floor(), + (((y + 1) * height) / size.height * 255.0).floor(), + 255, + 127); + canvas.drawRect(rect, basePaint); + + final Paint multiplyPaint = Paint() + ..color = _color + ..blendMode = BlendMode.multiply; + canvas.drawRect(rect, multiplyPaint); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +class AnimatedAdvancedBlend extends StatefulWidget { + const AnimatedAdvancedBlend({super.key}); + + @override + State createState() => _AnimatedAdvancedBlendState(); +} + +class _AnimatedAdvancedBlendState extends State with SingleTickerProviderStateMixin { + late final AnimationController controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 5000)); + late final Animation animation = controller.drive(Tween(begin: 0.0, end: 1.0)); + Color _color = const Color.fromARGB(255, 255, 0, 255); + + @override + void initState() { + super.initState(); + controller.repeat(); + animation.addListener(() { + setState(() { + _color = Color.fromARGB((animation.value * 255).floor(), 255, 0, 255); + }); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: CustomPaint( + painter: _MultiplyPainter(_color), + child: Container(), + ), + )); + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/draw_atlas.dart b/dev/benchmarks/macrobenchmarks/lib/src/draw_atlas.dart new file mode 100644 index 0000000000000..742301fb36753 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/draw_atlas.dart @@ -0,0 +1,116 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +Future loadImage(String asset) async { + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromAsset(asset); + final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + return frameInfo.image; +} + +class DrawAtlasPage extends StatefulWidget { + const DrawAtlasPage({super.key}); + + @override + State createState() => _DrawAtlasPageState(); +} + +class _DrawAtlasPageState extends State with SingleTickerProviderStateMixin { + late final AnimationController controller; + double tick = 0.0; + ui.Image? image; + + @override + void initState() { + super.initState(); + loadImage('packages/flutter_gallery_assets/food/butternut_squash_soup.png').then((ui.Image pending) { + setState(() { + image = pending; + }); + }); + controller = AnimationController(vsync: this, duration: const Duration(hours: 1)); + controller.addListener(() { + setState(() { + tick += 1; + }); + }); + controller.forward(from: 0); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + if (image == null) { + return const Placeholder(); + } + return CustomPaint( + size: const Size(500, 500), + painter: VerticesPainter(tick, image!), + child: Container(), + ); + } +} + +class VerticesPainter extends CustomPainter { + VerticesPainter(this.tick, this.image); + + final double tick; + final ui.Image image; + + @override + void paint(Canvas canvas, Size size) { + canvas.translate(0, tick); + canvas.drawAtlas( + image, + [RSTransform.fromComponents(rotation: 0, scale: 1, anchorX: 0, anchorY: 0, translateX: 0, translateY: 0)], + [Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble())], + [Colors.red], + BlendMode.plus, + null, + Paint() + ); + canvas.drawAtlas( + image, + [RSTransform.fromComponents(rotation: 0, scale: 1, anchorX: 0, anchorY: 0, translateX: 250, translateY: 0)], + [Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble())], + [Colors.green], + BlendMode.plus, + null, + Paint() + ); + canvas.drawAtlas( + image, + [RSTransform.fromComponents(rotation: 0, scale: 1, anchorX: 0, anchorY: 0, translateX: 0, translateY: 250)], + [Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble())], + [Colors.blue], + BlendMode.plus, + null, + Paint() + ); + canvas.drawAtlas( + image, + [RSTransform.fromComponents(rotation: 0, scale: 1, anchorX: 0, anchorY: 0, translateX: 250, translateY: 250)], + [Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble())], + [Colors.yellow], + BlendMode.plus, + null, + Paint() + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/draw_points.dart b/dev/benchmarks/macrobenchmarks/lib/src/draw_points.dart index a3ca792cb50d5..09989f7c695df 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/draw_points.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/draw_points.dart @@ -72,10 +72,10 @@ class PointsPainter extends CustomPainter { } canvas.drawPaint(Paint()..color = Colors.white); for (int i = 0; i < 8; i++) { - final double x = ((size.width / i) + tick) % size.width; + final double x = ((size.width / (i + 1)) + tick) % size.width; for (int j = 0; j < data.length; j += 2) { data[j] = x; - data[j + 1] = (size.height / j) + 200; + data[j + 1] = (size.height / (j + 1)) + 200; } final Paint paint = Paint() ..color = kColors[i] diff --git a/dev/benchmarks/macrobenchmarks/lib/src/draw_vertices.dart b/dev/benchmarks/macrobenchmarks/lib/src/draw_vertices.dart new file mode 100644 index 0000000000000..669919d4bcde0 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/draw_vertices.dart @@ -0,0 +1,114 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +Future loadImage(String asset) async { + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromAsset(asset); + final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + return frameInfo.image; +} + +class DrawVerticesPage extends StatefulWidget { + const DrawVerticesPage({super.key}); + + @override + State createState() => _DrawVerticesPageState(); +} + +class _DrawVerticesPageState extends State with SingleTickerProviderStateMixin { + late final AnimationController controller; + double tick = 0.0; + ui.Image? image; + + @override + void initState() { + super.initState(); + loadImage('packages/flutter_gallery_assets/food/butternut_squash_soup.png').then((ui.Image pending) { + setState(() { + image = pending; + }); + }); + controller = AnimationController(vsync: this, duration: const Duration(hours: 1)); + controller.addListener(() { + setState(() { + tick += 1; + }); + }); + controller.forward(from: 0); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) { + if (image == null) { + return const Placeholder(); + } + return CustomPaint( + size: const Size(500, 500), + painter: VerticesPainter(tick, image!), + child: Container(), + ); + } +} + +class VerticesPainter extends CustomPainter { + VerticesPainter(this.tick, this.image); + + final double tick; + final ui.Image image; + + @override + void paint(Canvas canvas, Size size) { + canvas.translate(0, tick); + final ui.Vertices vertices = ui.Vertices( + VertexMode.triangles, + const [ + Offset.zero, + Offset(0, 250), + Offset(250, 0), + Offset(0, 250), + Offset(250, 0), + Offset(250, 250) + ], + textureCoordinates: [ + Offset.zero, + Offset(0, image.height.toDouble()), + Offset(image.width.toDouble(), 0), + Offset(0, image.height.toDouble()), + Offset(image.width.toDouble(), 0), + Offset(image.width.toDouble(), image.height.toDouble()) + ], + colors: [ + Colors.red, + Colors.blue, + Colors.green, + Colors.red, + Colors.blue, + Colors.green, + ] + ); + canvas.drawVertices(vertices, BlendMode.plus, Paint()..shader = ImageShader(image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage)); + canvas.translate(250, 0); + canvas.drawVertices(vertices, BlendMode.plus, Paint()..shader = ImageShader(image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage)); + canvas.translate(0, 250); + canvas.drawVertices(vertices, BlendMode.plus, Paint()..shader = ImageShader(image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage)); + canvas.translate(-250, 0); + canvas.drawVertices(vertices, BlendMode.plus, Paint()..shader = ImageShader(image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage)); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/path_tessellation.dart b/dev/benchmarks/macrobenchmarks/lib/src/path_tessellation.dart new file mode 100644 index 0000000000000..6fc395aba9041 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/path_tessellation.dart @@ -0,0 +1,341 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +class PathTessellationPage extends StatefulWidget { + const PathTessellationPage({super.key}); + + @override + State createState() => _PathTessellationPageState(); +} + +class _PathTessellationPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = + AnimationController(vsync: this, lowerBound: 1.0, upperBound: 1.3); + _controller.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + final double scale = _controller.value; + return SafeArea( + child: ColoredBox( + color: Colors.black, + child: Stack( + fit: StackFit.expand, + children: [ + ListView.builder( + key: const Key( + 'list_view'), // this key is used by the driver test, + itemBuilder: (BuildContext context, int index) { + return Container( + margin: const EdgeInsets.all(1.0), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + ), + child: IconRow(iconSize: (30 + 0.5 * (index % 10)) * scale), + ); + }, + itemCount: 200, + itemExtent: 50, + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + color: Colors.black.withOpacity(0.7), + height: 100, + child: IconRow(iconSize: 50.0 * scale), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: ColoredBox( + color: Colors.black.withOpacity(0.7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 100, + child: IconRow(iconSize: 55.0 * scale), + ), + MaterialButton( + textColor: Colors.white, + key: const Key( + 'animate_button'), // this key is used by the driver test + child: const Text('Animate'), + onPressed: () { + if (_controller.isAnimating) { + _controller.stop(); + } else { + _controller.repeat( + period: const Duration(seconds: 1), + reverse: true, + ); + } + }, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class IconRow extends StatelessWidget { + const IconRow({ + super.key, + required this.iconSize, + }); + + final double iconSize; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox.square( + dimension: iconSize, + child: CustomPaint( + painter: _SettingsIconPainter(), + willChange: true, + ), + ), + SizedBox.square( + dimension: iconSize, + child: CustomPaint( + painter: _CameraIconPainter(), + willChange: true, + ), + ), + SizedBox.square( + dimension: iconSize, + child: CustomPaint( + painter: _CalendarIconPainter(), + willChange: true, + ), + ), + SizedBox.square( + dimension: iconSize, + child: CustomPaint( + painter: _ConversationIconPainter(), + willChange: true, + ), + ), + SizedBox.square( + dimension: iconSize, + child: CustomPaint( + painter: _GeometryIconPainter(), + willChange: true, + ), + ), + ], + ); + } +} + +/// Parses SVG path data into a [Path] object. +Path _pathFromString(String pathString) { + int start = 0; + final RegExp pattern = RegExp('[MLCHVZ]'); + Offset current = Offset.zero; + final Path path = Path(); + + void performCommand(String command) { + final String type = command[0]; + final List arguments = command + .substring(1) + .split(' ') + .where((String element) => element.isNotEmpty) + .map((String e) => double.parse(e)) + .toList(growable: false); + switch (type) { + case 'M': + path.moveTo(arguments[0], arguments[1]); + current = Offset(arguments[0], arguments[1]); + case 'L': + path.lineTo(arguments[0], arguments[1]); + current = Offset(arguments[0], arguments[1]); + case 'C': + path.cubicTo(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5]); + current = Offset(arguments[4], arguments[5]); + case 'H': + path.lineTo(arguments[0], current.dy); + current = Offset(arguments[0], current.dy); + case 'V': + path.lineTo(current.dx, arguments[0]); + current = Offset(current.dx, arguments[0]); + } + } + + while (true) { + start = pathString.indexOf(pattern, start); + if (start == -1) { + break; + } + int end = pathString.indexOf(pattern, start + 1); + if (end == -1) { + end = pathString.length; + } + final String command = pathString.substring(start, end); + performCommand(command); + start = end; + } + return path; +} + +class _SettingsIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Matrix4 scale = + Matrix4.diagonal3Values(size.width / 20, size.height / 20, 1.0); + + Path path; + path = _path1.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0x60F84F39)); + + path = _path2.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0xFFF84F39)); + + path = _path3.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0xFFF84F39)); + } + + static final Path _path1 = _pathFromString( + 'M8.3252 2.675L7.7877 4.0625L5.93771 5.1125L4.4627 4.8875C4.2171 4.85416 3.96713 4.89459 3.74456 5.00365C3.52199 5.11271 3.33686 5.28548 3.21271 5.5L2.7127 6.375C2.58458 6.59294 2.52555 6.8446 2.5434 7.09678C2.56126 7.34895 2.65516 7.58979 2.8127 7.7875L3.7502 8.95V11.05L2.8377 12.2125C2.68016 12.4102 2.58626 12.651 2.5684 12.9032C2.55055 13.1554 2.60958 13.4071 2.73771 13.625L3.2377 14.5C3.36186 14.7145 3.54699 14.8873 3.76956 14.9963C3.99213 15.1054 4.2421 15.1458 4.4877 15.1125L5.96271 14.8875L7.7877 15.9375L8.3252 17.325C8.41585 17.5599 8.57534 17.762 8.78277 17.9047C8.9902 18.0475 9.2359 18.1243 9.48771 18.125H10.5377C10.7895 18.1243 11.0352 18.0475 11.2426 17.9047C11.4501 17.762 11.6096 17.5599 11.7002 17.325L12.2377 15.9375L14.0627 14.8875L15.5377 15.1125C15.7833 15.1458 16.0333 15.1054 16.2559 14.9963C16.4784 14.8873 16.6636 14.7145 16.7877 14.5L17.2877 13.625C17.4158 13.4071 17.4749 13.1554 17.457 12.9032C17.4392 12.651 17.3453 12.4102 17.1877 12.2125L16.2502 11.05V8.95L17.1627 7.7875C17.3203 7.58979 17.4142 7.34895 17.432 7.09678C17.4499 6.8446 17.3908 6.59294 17.2627 6.375L16.7627 5.5C16.6386 5.28548 16.4534 5.11271 16.2309 5.00365C16.0083 4.89459 15.7583 4.85416 15.5127 4.8875L14.0377 5.1125L12.2127 4.0625L11.6752 2.675C11.5846 2.44008 11.4251 2.23801 11.2176 2.09527C11.0102 1.95252 10.7645 1.87574 10.5127 1.875H9.48771C9.2359 1.87574 8.9902 1.95252 8.78277 2.09527C8.57534 2.23801 8.41585 2.44008 8.3252 2.675ZM10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z'); + static final Path _path2 = _pathFromString( + 'M9.48771 1.25L9.48586 1.25001C9.10816 1.25112 8.7396 1.36628 8.42845 1.5804C8.11747 1.79441 7.87833 2.0973 7.74232 2.44945L7.2854 3.62894L5.81769 4.46197L4.55695 4.26965L4.54677 4.26818C4.17836 4.21818 3.80341 4.27882 3.46955 4.44241C3.13569 4.606 2.858 4.86515 2.67177 5.18693L2.17177 6.06191C1.98107 6.38797 1.89329 6.76406 1.91997 7.14092C1.94675 7.51918 2.08759 7.88043 2.32392 8.177L3.12521 9.17062V10.834L2.34736 11.825C2.11197 12.1212 1.97169 12.4817 1.94497 12.8591C1.91828 13.236 2.00608 13.6121 2.1968 13.9381L2.69505 14.8101L2.69677 14.8131C2.883 15.1349 3.16069 15.394 3.49455 15.5576C3.82841 15.7212 4.20336 15.7818 4.57177 15.7318L5.84067 15.5383L7.28466 16.3691L7.28539 16.3711L7.74211 17.55C7.87812 17.9021 8.11745 18.2056 8.42843 18.4196C8.73958 18.6337 9.10814 18.7489 9.48584 18.75L9.48769 18.75L10.5145 18.75C10.8922 18.7489 11.2608 18.6337 11.5719 18.4196C11.8829 18.2056 12.1473 17.9021 12.2833 17.55L12.7408 16.3691L14.1847 15.5383L15.4434 15.7304L15.4536 15.7318C15.822 15.7818 16.197 15.7212 16.5309 15.5576C16.8647 15.394 17.1424 15.1349 17.3286 14.8131L17.3304 14.8101L17.8287 13.9381C18.0193 13.612 18.1071 13.2359 18.0804 12.8591C18.0537 12.4808 17.9128 12.1196 17.6765 11.823L16.8752 10.8294V9.166L17.6515 8.177L17.6531 8.17502C17.8884 7.87883 18.0287 7.51834 18.0554 7.14092C18.0821 6.76407 17.9943 6.38798 17.8036 6.06192L17.3054 5.18991L17.3036 5.18693C17.1174 4.86515 16.8397 4.606 16.5059 4.44241C16.172 4.27882 15.797 4.21809 15.4286 4.2681L14.1597 4.46166L12.7158 3.63087L12.258 2.44923C12.122 2.09718 11.8829 1.79437 11.572 1.5804C11.2608 1.36628 10.8923 1.25112 10.5146 1.25L9.48771 1.25ZM10.5116 2.5H9.48879C9.36315 2.50053 9.24059 2.5389 9.13708 2.61013C9.03337 2.68151 8.95363 2.78254 8.9083 2.9L8.3705 4.28827C8.31845 4.42266 8.22154 4.53492 8.09621 4.60606L6.24621 5.65606C6.12411 5.72535 5.98224 5.75153 5.84346 5.73036L4.37442 5.50627C4.25298 5.49062 4.12958 5.51099 4.01957 5.5649C3.90871 5.61922 3.81643 5.70515 3.75436 5.81183L3.25154 6.69178C3.18747 6.80075 3.15792 6.92655 3.16684 7.05264C3.17574 7.17824 3.22235 7.2982 3.30057 7.39684L4.23671 8.55766C4.32633 8.66878 4.37521 8.80724 4.37521 8.95V11.05C4.37521 11.1899 4.32824 11.3258 4.24184 11.4359L3.32651 12.602C3.24773 12.7009 3.20077 12.8213 3.19184 12.9474C3.18292 13.0735 3.21243 13.1993 3.27649 13.3083L3.2804 13.3149L3.77864 14.1869L3.77933 14.1881C3.8414 14.2948 3.93369 14.3808 4.04457 14.4351C4.15457 14.489 4.27796 14.5094 4.39939 14.4937L5.86846 14.2696C6.00848 14.2483 6.15161 14.2751 6.27439 14.3458L8.09939 15.3958C8.22322 15.467 8.3189 15.5785 8.3705 15.7117L8.908 17.0992C8.95333 17.2167 9.03337 17.3185 9.13708 17.3899C9.24061 17.4611 9.36321 17.4995 9.48887 17.5H10.5365C10.6622 17.4995 10.7848 17.4611 10.8883 17.3899C10.992 17.3185 11.0718 17.2175 11.1171 17.1L11.6549 15.7117C11.7065 15.5785 11.8022 15.467 11.926 15.3958L13.751 14.3458C13.8738 14.2751 14.0169 14.2483 14.157 14.2696L15.626 14.4937C15.7475 14.5094 15.8708 14.489 15.9808 14.4351C16.0917 14.3808 16.184 14.2949 16.2461 14.1882L16.2468 14.1869L16.7489 13.3082C16.8129 13.1993 16.8425 13.0735 16.8336 12.9474C16.8247 12.8218 16.7781 12.7019 16.6999 12.6032L16.6989 12.602L15.7637 11.4423C15.6741 11.3312 15.6252 11.1928 15.6252 11.05V8.95C15.6252 8.81006 15.6722 8.67418 15.7586 8.5641L16.6711 7.4016L16.6739 7.398C16.7527 7.29915 16.7996 7.17873 16.8086 7.05264C16.8175 6.92655 16.788 6.80072 16.7239 6.69175L16.72 6.68511L16.2218 5.81307L16.2211 5.81187C16.159 5.70517 16.0667 5.61923 15.9558 5.5649C15.8458 5.51099 15.7224 5.49062 15.601 5.50627L14.132 5.73036C13.9919 5.75172 13.8488 5.72488 13.726 5.65424L11.901 4.60424C11.7772 4.533 11.6815 4.42148 11.6299 4.28827L11.0924 2.90077C11.0471 2.78331 10.967 2.68151 10.8633 2.61013C10.7598 2.5389 10.6373 2.50053 10.5116 2.5Z'); + static final Path _path3 = _pathFromString( + 'M10.0002 6.875C9.1714 6.875 8.37655 7.20424 7.7905 7.79029C7.20445 8.37635 6.87521 9.1712 6.87521 10C6.87521 10.6181 7.05848 11.2223 7.40186 11.7362C7.74524 12.2501 8.2333 12.6506 8.80432 12.8871C9.37534 13.1237 10.0037 13.1855 10.6099 13.065C11.2161 12.9444 11.7729 12.6467 12.2099 12.2097C12.647 11.7727 12.9446 11.2159 13.0652 10.6097C13.1857 10.0035 13.1239 9.37514 12.8873 8.80412C12.6508 8.2331 12.2503 7.74504 11.7364 7.40166C11.2225 7.05828 10.6183 6.875 10.0002 6.875ZM10.0002 8.125C9.50292 8.125 9.02601 8.32255 8.67438 8.67418C8.32275 9.02581 8.12521 9.50272 8.12521 10C8.12521 10.3708 8.23517 10.7334 8.4412 11.0417C8.64723 11.35 8.94006 11.5904 9.28267 11.7323C9.62529 11.8742 10.0023 11.9113 10.366 11.839C10.7297 11.7666 11.0638 11.5881 11.326 11.3258C11.5883 11.0636 11.7668 10.7295 11.8392 10.3658C11.9115 10.0021 11.8744 9.62508 11.7325 9.28247C11.5906 8.93986 11.3502 8.64703 11.0419 8.441C10.7336 8.23497 10.371 8.125 10.0002 8.125Z'); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class _CameraIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Matrix4 scale = + Matrix4.diagonal3Values(size.width / 20, size.height / 20, 1.0); + + Path path; + path = _path1.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0xFFF84F39)); + + path = _path2.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0x60F84F39)); + } + + static final Path _path1 = _pathFromString( + 'M3.26366 17H16.7363C17.4857 17 18.0503 16.8123 18.4302 16.4369C18.8101 16.0615 19 15.5035 19 14.7631V7.01229C19 6.27188 18.8101 5.71397 18.4302 5.33855C18.0503 4.96313 17.4857 4.77542 16.7363 4.77542H14.6132C14.4154 4.77542 14.2567 4.76238 14.137 4.73631C14.0173 4.70503 13.9107 4.65549 13.817 4.58771C13.7233 4.51471 13.6219 4.41825 13.5126 4.29832L12.9115 3.61788C12.7242 3.41453 12.5212 3.26071 12.3027 3.15642C12.0893 3.05214 11.7953 3 11.4206 3H8.50911C8.13443 3 7.83781 3.05214 7.61925 3.15642C7.4059 3.26071 7.20555 3.41453 7.01821 3.61788L6.41717 4.29832C6.24545 4.48082 6.09193 4.60596 5.95663 4.67374C5.82134 4.74153 5.61058 4.77542 5.32437 4.77542H3.26366C2.50911 4.77542 1.94189 4.96313 1.56201 5.33855C1.18734 5.71397 1 6.27188 1 7.01229V14.7631C1 15.5035 1.18734 16.0615 1.56201 16.4369C1.94189 16.8123 2.50911 17 3.26366 17ZM3.27927 15.9207C2.89419 15.9207 2.59757 15.819 2.38942 15.6156C2.18647 15.4123 2.085 15.1099 2.085 14.7084V7.07486C2.085 6.67337 2.18647 6.37095 2.38942 6.1676C2.59757 5.95903 2.89419 5.85475 3.27927 5.85475H5.57415C5.9072 5.85475 6.1752 5.81825 6.37814 5.74525C6.58109 5.67225 6.77624 5.53147 6.96357 5.32291L7.55681 4.6581C7.76496 4.42346 7.9497 4.26965 8.11101 4.19665C8.27233 4.12365 8.50911 4.08715 8.82134 4.08715H11.1084C11.4258 4.08715 11.6626 4.12365 11.8187 4.19665C11.9801 4.26965 12.1648 4.42346 12.3729 4.6581L12.9662 5.32291C13.1535 5.53147 13.3487 5.67225 13.5516 5.74525C13.7546 5.81825 14.0225 5.85475 14.3556 5.85475H16.7207C17.1058 5.85475 17.4024 5.95903 17.6106 6.1676C17.8187 6.37095 17.9228 6.67337 17.9228 7.07486V14.7084C17.9228 15.1099 17.8187 15.4123 17.6106 15.6156C17.4024 15.819 17.1058 15.9207 16.7207 15.9207H3.27927ZM10 14.8101C10.7493 14.8101 11.4284 14.6302 12.0373 14.2704C12.6461 13.9054 13.1301 13.4179 13.4892 12.8078C13.8534 12.1926 14.0356 11.5095 14.0356 10.7587C14.0356 10.0078 13.8534 9.32477 13.4892 8.7095C13.1301 8.09423 12.6461 7.6067 12.0373 7.24693C11.4284 6.88194 10.7493 6.69944 10 6.69944C9.25585 6.69944 8.57676 6.88194 7.96271 7.24693C7.35386 7.6067 6.8673 8.09423 6.50304 8.7095C6.14397 9.32477 5.96444 10.0078 5.96444 10.7587C5.96444 11.5095 6.14397 12.1926 6.50304 12.8078C6.8673 13.4179 7.35386 13.9054 7.96271 14.2704C8.57676 14.6302 9.25585 14.8101 10 14.8101ZM10 13.7855C9.4484 13.7855 8.94363 13.6499 8.48569 13.3788C8.03296 13.1076 7.66869 12.7426 7.39289 12.2838C7.12229 11.825 6.98699 11.3166 6.98699 10.7587C6.98699 10.1955 7.12229 9.68454 7.39289 9.2257C7.66349 8.76685 8.02775 8.40447 8.48569 8.13855C8.94363 7.86741 9.4484 7.73184 10 7.73184C10.5568 7.73184 11.0616 7.86741 11.5143 8.13855C11.9722 8.40447 12.3365 8.76685 12.6071 9.2257C12.8829 9.68454 13.0208 10.1955 13.0208 10.7587C13.0208 11.3166 12.8829 11.825 12.6071 12.2838C12.3365 12.7426 11.9722 13.1076 11.5143 13.3788C11.0616 13.6499 10.5568 13.7855 10 13.7855ZM14.3556 8.04469C14.3556 8.30019 14.4467 8.51657 14.6288 8.69385C14.8109 8.86592 15.0269 8.95196 15.2767 8.95196C15.516 8.94674 15.7242 8.8581 15.9011 8.68603C16.0833 8.50875 16.1743 8.29497 16.1743 8.04469C16.1743 7.79963 16.0833 7.58845 15.9011 7.41117C15.7242 7.22868 15.516 7.13743 15.2767 7.13743C15.0269 7.13743 14.8109 7.22868 14.6288 7.41117C14.4467 7.58845 14.3556 7.79963 14.3556 8.04469Z'); + static final Path _path2 = _pathFromString( + 'M2.30754 15.6907C2.51782 15.8969 2.81748 16 3.20651 16H16.7856C17.1746 16 17.4743 15.8969 17.6846 15.6907C17.8949 15.4845 18 15.1778 18 14.7707V7.02974C18 6.6226 17.8949 6.31593 17.6846 6.10972C17.4743 5.89822 17.1746 5.79247 16.7856 5.79247H14.3963C14.0598 5.79247 13.7891 5.75545 13.584 5.68143C13.379 5.6074 13.1819 5.46464 12.9926 5.25314L12.3933 4.57898C12.183 4.34104 11.9964 4.18506 11.8334 4.11104C11.6757 4.03701 11.4365 4 11.1158 4H8.80532C8.4899 4 8.2507 4.03701 8.08773 4.11104C7.92476 4.18506 7.73813 4.34104 7.52785 4.57898L6.92854 5.25314C6.73928 5.46464 6.54214 5.6074 6.33711 5.68143C6.13208 5.75545 5.86134 5.79247 5.52489 5.79247H3.20651C2.81748 5.79247 2.51782 5.89822 2.30754 6.10972C2.10251 6.31593 2 6.6226 2 7.02974V14.7707C2 15.1778 2.10251 15.4845 2.30754 15.6907ZM9.99547 14C9.99547 14 8.76994 13.8432 8.23868 13.5297C7.71345 13.2162 7.29086 12.7941 6.97089 12.2636C6.65696 11.733 6.5 11.1451 6.5 10.5C6.5 9.84884 6.65696 9.25797 6.97089 8.72739C7.28482 8.19681 7.70742 7.77778 8.23868 7.47028C8.76994 7.15676 9.35554 7 9.99547 7C10.6414 7 11.7523 7.47028 11.7523 7.47028C11.7523 7.47028 12.7061 8.19681 13.0201 8.72739C13.34 9.25797 13.5 9.84884 13.5 10.5C13.5 11.1451 13.34 11.733 13.0201 12.2636C12.7061 12.7941 12.2835 13.2162 11.7523 13.5297C11.227 13.8432 9.99547 14 9.99547 14Z'); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class _CalendarIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Matrix4 scale = + Matrix4.diagonal3Values(size.width / 20, size.height / 20, 1.0); + + Path path; + path = _path1.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0x60F84F39)); + + path = _path2.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0xFFF84F39)); + } + + static final Path _path1 = _pathFromString( + 'M16.7812 6.85938H3.28125V4.5L5 3H15L16.7812 4.5V6.85938Z'); + static final Path _path2 = _pathFromString( + 'M6.5606 11.2462C7.07732 11.2462 7.4962 10.8273 7.4962 10.3106C7.4962 9.79388 7.07732 9.375 6.5606 9.375C6.04388 9.375 5.625 9.79388 5.625 10.3106C5.625 10.8273 6.04388 11.2462 6.5606 11.2462ZM7.4962 13.4356C7.4962 13.9523 7.07732 14.3712 6.5606 14.3712C6.04388 14.3712 5.625 13.9523 5.625 13.4356C5.625 12.9189 6.04388 12.5 6.5606 12.5C7.07732 12.5 7.4962 12.9189 7.4962 13.4356ZM10.0005 11.2462C10.5173 11.2462 10.9361 10.8273 10.9361 10.3106C10.9361 9.79388 10.5173 9.375 10.0005 9.375C9.48382 9.375 9.06494 9.79388 9.06494 10.3106C9.06494 10.8273 9.48382 11.2462 10.0005 11.2462ZM10.9361 13.4356C10.9361 13.9523 10.5173 14.3712 10.0005 14.3712C9.48382 14.3712 9.06494 13.9523 9.06494 13.4356C9.06494 12.9189 9.48382 12.5 10.0005 12.5C10.5173 12.5 10.9361 12.9189 10.9361 13.4356ZM13.4356 11.2462C13.9523 11.2462 14.3712 10.8273 14.3712 10.3106C14.3712 9.79388 13.9523 9.375 13.4356 9.375C12.9189 9.375 12.5 9.79388 12.5 10.3106C12.5 10.8273 12.9189 11.2462 13.4356 11.2462ZM17.5 5.625C17.5 3.89911 16.1009 2.5 14.375 2.5H5.625C3.89911 2.5 2.5 3.89911 2.5 5.625V14.375C2.5 16.1009 3.89911 17.5 5.625 17.5H14.375C16.1009 17.5 17.5 16.1009 17.5 14.375V5.625ZM3.75 7.5H16.25V14.375C16.25 15.4105 15.4105 16.25 14.375 16.25H5.625C4.58947 16.25 3.75 15.4105 3.75 14.375V7.5ZM5.625 3.75H14.375C15.4105 3.75 16.25 4.58947 16.25 5.625V6.25H3.75V5.625C3.75 4.58947 4.58947 3.75 5.625 3.75Z'); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class _ConversationIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Matrix4 scale = + Matrix4.diagonal3Values(size.width / 20, size.height / 20, 1.0); + + Path path; + path = _path1.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0x60F84F39)); + + path = _path2.transform(scale.storage)..fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = const Color(0xFFF84F39)); + } + + static final Path _path1 = _pathFromString( + 'M14.4141 8.33333C14.4141 11.555 11.8024 14.1667 8.58073 14.1667C7.64487 14.1667 6.76047 13.9463 5.97663 13.5546L2.65625 14.4661L3.70864 11.5424C3.10106 10.6218 2.7474 9.51887 2.7474 8.33333C2.7474 5.11167 5.35907 2.5 8.58073 2.5C11.8024 2.5 14.4141 5.11167 14.4141 8.33333Z'); + static final Path _path2 = _pathFromString( + 'M8.5382 1.81665C4.84709 1.81665 1.85486 4.80888 1.85486 8.49998C1.85486 9.65241 2.1471 10.7384 2.66182 11.686L1.89645 13.6887C1.5494 14.5968 2.38481 15.5128 3.32097 15.2506L5.74289 14.5723C6.59409 14.9647 7.54146 15.1833 8.5382 15.1833C12.2293 15.1833 15.2215 12.1911 15.2215 8.49998C15.2215 4.80888 12.2293 1.81665 8.5382 1.81665ZM3.22153 8.49998C3.22153 5.56367 5.60188 3.18332 8.5382 3.18332C11.4745 3.18332 13.8549 5.56367 13.8549 8.49998C13.8549 11.4363 11.4745 13.8167 8.5382 13.8167C7.66578 13.8167 6.84431 13.607 6.11952 13.2361L5.88143 13.1142L3.30309 13.8364L4.17436 11.5565L3.99903 11.2698C3.5059 10.4636 3.22153 9.51607 3.22153 8.49998ZM16.5636 7.07206L16.1464 6.61586L16.2475 7.22577C16.317 7.64558 16.3533 8.07677 16.3533 8.51656C16.3533 8.69251 16.3475 8.86707 16.3361 9.04007L16.3328 9.08869L16.3542 9.13249C16.6951 9.83163 16.8175 10.5975 16.8175 11.5C16.8175 12.5161 16.5332 13.4636 16.04 14.2698L15.8647 14.5565L16.736 16.8363L14.1576 16.1142L13.9195 16.2361C13.1948 16.607 12.3733 16.8166 11.5009 16.8166C10.5879 16.8166 9.87033 16.6947 9.19041 16.3496L9.14498 16.3266L9.09417 16.3303C8.90419 16.3441 8.71231 16.3511 8.51875 16.3511C8.10958 16.3511 7.70785 16.3197 7.31582 16.2593L6.72389 16.1681L7.16334 16.575C8.31731 17.6436 9.75888 18.1833 11.5009 18.1833C12.4976 18.1833 13.445 17.9647 14.2962 17.5723L16.7181 18.2506C17.6543 18.5128 18.4897 17.5968 18.1426 16.6887L17.3772 14.6859C17.892 13.7384 18.1842 12.6524 18.1842 11.5C18.1842 9.75761 17.6387 8.24759 16.5636 7.07206Z'); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class _GeometryIconPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size canvasSize) { + const Size size = Size(20, 20); + canvas.scale( + canvasSize.width / size.width, canvasSize.height / size.height); + + final Paint paint = Paint()..color = const Color(0xFFF84F39); + final Rect frame = Offset.zero & size; + canvas.drawDRRect( + RRect.fromRectAndRadius(frame, const Radius.elliptical(5, 4)), + RRect.fromRectAndRadius( + frame.deflate(1), const Radius.elliptical(4, 3)), + paint); + canvas.drawRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(3, 3, 6, 6), const Radius.elliptical(2, 1)), + paint); + canvas.drawRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(11, 11, 6, 6), const Radius.elliptical(2, 1)), + paint); + canvas.drawCircle(const Offset(14, 6), 3, paint); + canvas.drawCircle(const Offset(6, 14), 3, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/very_long_picture_scrolling.dart b/dev/benchmarks/macrobenchmarks/lib/src/very_long_picture_scrolling.dart new file mode 100644 index 0000000000000..fb6a7cf20b6a8 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/very_long_picture_scrolling.dart @@ -0,0 +1,240 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:flutter/material.dart'; + +// Adapted from test case submitted in +// https://github.com/flutter/flutter/issues/92366 +// Converted to use fixed data rather than reading a waveform file +class VeryLongPictureScrollingPerf extends StatefulWidget { + const VeryLongPictureScrollingPerf({super.key}); + + @override + State createState() => VeryLongPictureScrollingPerfState(); +} + +class VeryLongPictureScrollingPerfState extends State { + bool consolidate = false; + bool useList = false; + Int16List waveData = loadGraph(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + actions: [ + Row( + children: [ + const Text('list:'), + Checkbox(value: useList, onChanged: (bool? value) => setState(() { + useList = value!; + }),), + ], + ), + Row( + children: [ + const Text('consolidate:'), + Checkbox(value: consolidate, onChanged: (bool? value) => setState(() { + consolidate = value!; + }),), + ], + ), + ], + ), + backgroundColor: Colors.transparent, + body: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: useList + ? ListView.builder( + key: const ValueKey('vlp_list_view_scrollable'), + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + itemCount: (waveData.length / 200).ceil(), + itemExtent: 100, + itemBuilder: (BuildContext context, int index) => CustomPaint( + painter: PaintSomeTest( + waveData: waveData, + from: index * 200, + to: min((index + 1) * 200, waveData.length - 1), + ) + ), + ) + : SingleChildScrollView( + key: const ValueKey('vlp_single_child_scrollable'), + scrollDirection: Axis.horizontal, + child: SizedBox( + width: MediaQuery.of(context).size.width * 20, + height: MediaQuery.of(context).size.height, + child: RepaintBoundary( + child: CustomPaint( + isComplex: true, + painter: PaintTest( + consolidate: consolidate, + waveData: waveData, + ), + ), + ), + ), + ), + ), + ); + } +} + +class PaintTest extends CustomPainter { + const PaintTest({ + required this.consolidate, + required this.waveData, + }); + + final bool consolidate; + final Int16List waveData; + + @override + void paint(Canvas canvas, Size size) { + final double height = size.height; + double x = 0; + const double strokeSize = .5; + const double zoomFactor = .5; + + final Paint paintPos = Paint() + ..color = Colors.pink + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + final Paint paintNeg = Paint() + ..color = Colors.pink + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + final Paint paintZero = Paint() + ..color = Colors.green + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + int index = 0; + Paint? listPaint; + final Float32List offsets = Float32List(consolidate ? waveData.length * 4 : 4); + int used = 0; + for (index = 0; index < waveData.length; index++) { + Paint curPaint; + Offset p1; + if (waveData[index].isNegative) { + curPaint = paintPos; + p1 = Offset(x, height * 1 / 2 - waveData[index] / 32768 * (height / 2)); + } else if (waveData[index] == 0) { + curPaint = paintZero; + p1 = Offset(x, height * 1 / 2 + 1); + } else { + curPaint = (waveData[index] == 0) ? paintZero : paintNeg; + p1 = Offset(x, height * 1 / 2 - waveData[index] / 32767 * (height / 2)); + } + final Offset p0 = Offset(x, height * 1 / 2); + if (consolidate) { + if (listPaint != null && listPaint != curPaint) { + canvas.drawRawPoints(PointMode.lines, offsets.sublist(0, used), listPaint); + used = 0; + } + listPaint = curPaint; + offsets[used++] = p0.dx; + offsets[used++] = p0.dy; + offsets[used++] = p1.dx; + offsets[used++] = p1.dy; + } else { + canvas.drawLine(p0, p1, curPaint); + } + x += zoomFactor; + } + if (consolidate && used > 0) { + canvas.drawRawPoints(PointMode.lines, offsets.sublist(0, used), listPaint!); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate is! PaintTest || + oldDelegate.consolidate != consolidate || + oldDelegate.waveData != waveData; + } +} + +class PaintSomeTest extends CustomPainter { + const PaintSomeTest({ + required this.waveData, + int? from, + int? to, + }) : from = from ?? 0, to = to?? waveData.length; + + final Int16List waveData; + final int from; + final int to; + + @override + void paint(Canvas canvas, Size size) { + final double height = size.height; + double x = 0; + const double strokeSize = .5; + const double zoomFactor = .5; + + final Paint paintPos = Paint() + ..color = Colors.pink + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + final Paint paintNeg = Paint() + ..color = Colors.pink + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + final Paint paintZero = Paint() + ..color = Colors.green + ..strokeWidth = strokeSize + ..isAntiAlias = false + ..style = PaintingStyle.stroke; + + for (int index = from; index <= to; index++) { + Paint curPaint; + Offset p1; + if (waveData[index].isNegative) { + curPaint = paintPos; + p1 = Offset(x, height * 1 / 2 - waveData[index] / 32768 * (height / 2)); + } else if (waveData[index] == 0) { + curPaint = paintZero; + p1 = Offset(x, height * 1 / 2 + 1); + } else { + curPaint = (waveData[index] == 0) ? paintZero : paintNeg; + p1 = Offset(x, height * 1 / 2 - waveData[index] / 32767 * (height / 2)); + } + final Offset p0 = Offset(x, height * 1 / 2); + canvas.drawLine(p0, p1, curPaint); + x += zoomFactor; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate is! PaintSomeTest || + oldDelegate.waveData != waveData || + oldDelegate.from != from || + oldDelegate.to != to; + } +} + +Int16List loadGraph() { + final Int16List waveData = Int16List(350000); + final Random r = Random(0x42); + for (int i = 0; i < waveData.length; i++) { + waveData[i] = r.nextInt(32768) - 16384; + } + return waveData; +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_harness.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_harness.dart new file mode 100644 index 0000000000000..8c95cf42cf862 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_harness.dart @@ -0,0 +1,63 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import 'recorder.dart'; + +class BenchWidgetRecorder extends WidgetRecorder { + BenchWidgetRecorder() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_widget_recorder'; + + @override + Widget createWidget() { + // This is intentionally using a simple widget. The benchmark is meant to + // measure the overhead of the harness, so this method should induce as + // little work as possible. + return const SizedBox.expand(); + } +} + +class BenchWidgetBuildRecorder extends WidgetBuildRecorder { + BenchWidgetBuildRecorder() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_widget_build_recorder'; + + @override + Widget createWidget() { + // This is intentionally using a simple widget. The benchmark is meant to + // measure the overhead of the harness, so this method should induce as + // little work as possible. + return const SizedBox.expand(); + } +} + +class BenchRawRecorder extends RawRecorder { + BenchRawRecorder() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_raw_recorder'; + + @override + void body(Profile profile) { + profile.record('profile.record', () { + // This is intentionally empty. The benchmark only measures the overhead + // of the harness. + }, reported: true); + } +} + +class BenchSceneBuilderRecorder extends SceneBuilderRecorder { + BenchSceneBuilderRecorder() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_scene_builder_recorder'; + + @override + void onDrawFrame(ui.SceneBuilder sceneBuilder) { + // This is intentionally empty. The benchmark only measures the overhead + // of the harness. + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_pageview_scroll_linethrough.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_pageview_scroll_linethrough.dart index c605072714d85..f5ee8dd906eb8 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_pageview_scroll_linethrough.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_pageview_scroll_linethrough.dart @@ -95,7 +95,7 @@ class _CustomPainter extends CustomPainter { yPosition = viewPadding; _textPainter.textDirection = TextDirection.ltr; _textPainter.textWidthBasis = TextWidthBasis.longestLine; - _textPainter.textScaleFactor = 1; + _textPainter.textScaler = TextScaler.noScaling; const TextStyle textStyle = TextStyle(color: Colors.black87, fontSize: 13, fontFamily: 'Roboto'); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart index ae583dc8ba80e..89661d6ddac1c 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart @@ -4,12 +4,9 @@ import 'dart:async'; import 'dart:js_interop'; -// The analyzer currently thinks `js_interop_unsafe` is unused, but it is used -// for `JSObject.[]=`. -// ignore: unused_import -import 'dart:js_interop_unsafe'; import 'dart:math' as math; import 'dart:ui'; +import 'dart:ui_web' as ui_web; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -86,8 +83,6 @@ Future _dummyAsyncVoidCallback() async {} @sealed class Runner { /// Creates a runner for the [recorder]. - /// - /// All arguments must not be null. Runner({ required this.recorder, this.setUpAllDidRun = _dummyAsyncVoidCallback, @@ -1335,10 +1330,7 @@ void registerEngineBenchmarkValueListener(String name, EngineBenchmarkValueListe if (_engineBenchmarkListeners.isEmpty) { // The first listener is being registered. Register the global listener. - web.window['_flutter_internal_on_benchmark'.toJS] = - // Upcast to [Object] to export. - // ignore: unnecessary_cast - (_dispatchEngineBenchmarkValue as Object).toJS; + ui_web.benchmarkValueCallback = _dispatchEngineBenchmarkValue; } _engineBenchmarkListeners[name] = listener; } @@ -1349,7 +1341,7 @@ void stopListeningToEngineBenchmarkValues(String name) { if (_engineBenchmarkListeners.isEmpty) { // The last listener unregistered. Remove the global listener. - web.window['_flutter_internal_on_benchmark'.toJS] = null; + ui_web.benchmarkValueCallback = null; } } diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart index e4e18d598b908..1beddb177940f 100644 --- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart +++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart @@ -17,6 +17,7 @@ import 'src/web/bench_clipped_out_pictures.dart'; import 'src/web/bench_default_target_platform.dart'; import 'src/web/bench_draw_rect.dart'; import 'src/web/bench_dynamic_clip_on_static_picture.dart'; +import 'src/web/bench_harness.dart'; import 'src/web/bench_image_decoding.dart'; import 'src/web/bench_material_3.dart'; import 'src/web/bench_material_3_semantics.dart'; @@ -43,7 +44,13 @@ const bool isSkwasm = bool.fromEnvironment('FLUTTER_WEB_USE_SKWASM'); /// When adding a new benchmark, add it to this map. Make sure that the name /// of your benchmark is unique. final Map benchmarks = { - // Benchmarks that run both in CanvasKit and HTML modes + // Benchmarks the overhead of the benchmark harness itself. + BenchRawRecorder.benchmarkName: () => BenchRawRecorder(), + BenchWidgetRecorder.benchmarkName: () => BenchWidgetRecorder(), + BenchWidgetBuildRecorder.benchmarkName: () => BenchWidgetBuildRecorder(), + BenchSceneBuilderRecorder.benchmarkName: () => BenchSceneBuilderRecorder(), + + // Benchmarks that run in all renderers. BenchDefaultTargetPlatform.benchmarkName: () => BenchDefaultTargetPlatform(), BenchBuildImage.benchmarkName: () => BenchBuildImage(), BenchCardInfiniteScroll.benchmarkName: () => BenchCardInfiniteScroll.forward(), diff --git a/dev/benchmarks/macrobenchmarks/macos/Runner.xcodeproj/project.pbxproj b/dev/benchmarks/macrobenchmarks/macos/Runner.xcodeproj/project.pbxproj index 5f2846150efb5..1b628dcde2b6f 100644 --- a/dev/benchmarks/macrobenchmarks/macos/Runner.xcodeproj/project.pbxproj +++ b/dev/benchmarks/macrobenchmarks/macos/Runner.xcodeproj/project.pbxproj @@ -181,7 +181,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - A7656B4F0E64AD6C5DDAE467 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -310,23 +309,6 @@ 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; }; - A7656B4F0E64AD6C5DDAE467 /* [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/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml index 7617d7829eb31..8956855932a47 100644 --- a/dev/benchmarks/macrobenchmarks/pubspec.yaml +++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml @@ -2,7 +2,7 @@ name: macrobenchmarks description: Performance benchmarks using flutter drive. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -18,37 +18,37 @@ dependencies: # flutter update-packages --force-upgrade flutter_gallery_assets: 1.0.2 - web: 0.1.4-beta + web: 0.3.0 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 + test: 1.24.6 integration_test: sdk: flutter - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -71,11 +71,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -210,4 +210,4 @@ flutter: fonts: - asset: packages/flutter_gallery_assets/fonts/GalleryIcons.ttf -# PUBSPEC CHECKSUM: 1c29 +# PUBSPEC CHECKSUM: d287 diff --git a/dev/benchmarks/macrobenchmarks/test/very_long_picture_scrolling_perf_e2e.dart b/dev/benchmarks/macrobenchmarks/test/very_long_picture_scrolling_perf_e2e.dart new file mode 100644 index 0000000000000..ff54e0cce6dd4 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test/very_long_picture_scrolling_perf_e2e.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTestE2E( + 'very_long_picture_scrolling_perf', + kVeryLongPictureScrollingRouteName, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 30), + body: (WidgetController controller) async { + final Finder nestedScroll = find.byKey(const ValueKey('vlp_single_child_scrollable')); + expect(nestedScroll, findsOneWidget); + Future scrollOnce(double offset) async { + await controller.timedDrag( + nestedScroll, + Offset(offset, 0.0), + const Duration(milliseconds: 3500), + ); + await Future.delayed(const Duration(milliseconds: 500)); + } + for (int i = 0; i < 2; i += 1) { + await scrollOnce(-3000.0); + await scrollOnce(-3000.0); + await scrollOnce(3000.0); + await scrollOnce(3000.0); + } + }, + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/animated_advanced_blend_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/animated_advanced_blend_perf_test.dart new file mode 100644 index 0000000000000..9a7ad83cfb3cb --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/animated_advanced_blend_perf_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'animated_advanced_blend_perf', + kAnimatedAdvancedBlend, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/draw_atlas_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/draw_atlas_perf_test.dart new file mode 100644 index 0000000000000..a25028c1516a7 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/draw_atlas_perf_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'draw_atlas_perf', + kDrawAtlasPageRouteName, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/draw_vertices_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/draw_vertices_perf_test.dart new file mode 100644 index 0000000000000..e0c578656175e --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/draw_vertices_perf_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'draw_vertices_perf', + kDrawVerticesPageRouteName, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_dynamic_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_dynamic_perf_test.dart new file mode 100644 index 0000000000000..c18701fdb7dd7 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_dynamic_perf_test.dart @@ -0,0 +1,23 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'tessellation_perf_dynamic', + kPathTessellationRouteName, + pageDelay: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + setupOps: (FlutterDriver driver) async { + final SerializableFinder animateButton = + find.byValueKey('animate_button'); + await driver.tap(animateButton); + await Future.delayed(const Duration(seconds: 1)); + }, + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_static_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_static_perf_test.dart new file mode 100644 index 0000000000000..5be534bda7924 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/path_tessellation_static_perf_test.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'tessellation_perf_static', + kPathTessellationRouteName, + pageDelay: const Duration(seconds: 1), + driverOps: (FlutterDriver driver) async { + final SerializableFinder listView = find.byValueKey('list_view'); + Future scrollOnce(double offset) async { + await driver.scroll( + listView, 0.0, offset, const Duration(milliseconds: 450)); + await Future.delayed(const Duration(milliseconds: 500)); + } + + for (int i = 0; i < 3; i += 1) { + await scrollOnce(-600.0); + await scrollOnce(-600.0); + await scrollOnce(600.0); + await scrollOnce(600.0); + } + }, + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/util.dart b/dev/benchmarks/macrobenchmarks/test_driver/util.dart index bf74fbf8ec5ba..87b8fd2507804 100644 --- a/dev/benchmarks/macrobenchmarks/test_driver/util.dart +++ b/dev/benchmarks/macrobenchmarks/test_driver/util.dart @@ -23,7 +23,11 @@ Future runDriverTestForRoute(String routeName, DriverTestCallBack body) as expect(scrollable, isNotNull); final SerializableFinder button = find.byValueKey(routeName); expect(button, isNotNull); - await driver.scrollUntilVisible(scrollable, button, dyScroll: -100.0); + // -320 comes from the logical pixels for a full screen scroll for the + // smallest reference device, iPhone 4, whose physical screen dimensions are + // 960px × 640px. + const double dyScroll = -320.0; + await driver.scrollUntilVisible(scrollable, button, dyScroll: dyScroll); await driver.tap(button); await body(driver); diff --git a/dev/benchmarks/microbenchmarks/android/gradle.properties b/dev/benchmarks/microbenchmarks/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/microbenchmarks/android/gradle.properties +++ b/dev/benchmarks/microbenchmarks/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/microbenchmarks/lib/gestures/gesture_detector_bench.dart b/dev/benchmarks/microbenchmarks/lib/gestures/gesture_detector_bench.dart index 717535f367da1..a5ca499555987 100644 --- a/dev/benchmarks/microbenchmarks/lib/gestures/gesture_detector_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/gestures/gesture_detector_bench.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; -import './apps/button_matrix_app.dart' as button_matrix; import '../common.dart'; +import 'apps/button_matrix_app.dart' as button_matrix; const int _kNumWarmUpIters = 20; const int _kNumIters = 300; diff --git a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart index e42211d594cd7..9d72b87aa3a87 100644 --- a/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart +++ b/dev/benchmarks/microbenchmarks/lib/stocks/layout_bench.dart @@ -39,7 +39,7 @@ Future main() async { size: const Size(355.0, 635.0), view: tester.view, ); - final RenderView renderView = WidgetsBinding.instance.renderView; + final RenderView renderView = WidgetsBinding.instance.renderViews.single; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark; watch.start(); diff --git a/dev/benchmarks/microbenchmarks/pubspec.yaml b/dev/benchmarks/microbenchmarks/pubspec.yaml index c54fbc3f85e6f..0c91c7ef6683a 100644 --- a/dev/benchmarks/microbenchmarks/pubspec.yaml +++ b/dev/benchmarks/microbenchmarks/pubspec.yaml @@ -2,27 +2,27 @@ name: microbenchmarks description: Small benchmarks for very specific parts of the Flutter framework. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: - meta: 1.9.1 + meta: 1.10.0 flutter: sdk: flutter flutter_test: sdk: flutter stocks: path: ../test_apps/stocks - test: 1.24.3 + test: 1.24.6 flutter_gallery_assets: 1.0.2 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -53,19 +53,19 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -137,4 +137,4 @@ flutter: - packages/flutter_gallery_assets/people/square/stella.png - packages/flutter_gallery_assets/people/square/trevor.png -# PUBSPEC CHECKSUM: 06a0 +# PUBSPEC CHECKSUM: bcfe diff --git a/dev/benchmarks/multiple_flutters/android/app/build.gradle b/dev/benchmarks/multiple_flutters/android/app/build.gradle index 8e2b72d7c70d9..1df6b8ce74052 100644 --- a/dev/benchmarks/multiple_flutters/android/app/build.gradle +++ b/dev/benchmarks/multiple_flutters/android/app/build.gradle @@ -14,7 +14,7 @@ android { } namespace "dev.flutter.multipleflutters" - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -28,7 +28,7 @@ android { defaultConfig { applicationId "dev.flutter.multipleflutters" minSdkVersion 24 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/dev/benchmarks/multiple_flutters/android/gradle.properties b/dev/benchmarks/multiple_flutters/android/gradle.properties index 98bed167dc90f..9930279818e98 100644 --- a/dev/benchmarks/multiple_flutters/android/gradle.properties +++ b/dev/benchmarks/multiple_flutters/android/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects @@ -18,4 +18,4 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official diff --git a/dev/benchmarks/multiple_flutters/module/pubspec.yaml b/dev/benchmarks/multiple_flutters/module/pubspec.yaml index fe87a6f038a95..eacd619a209a9 100644 --- a/dev/benchmarks/multiple_flutters/module/pubspec.yaml +++ b/dev/benchmarks/multiple_flutters/module/pubspec.yaml @@ -4,43 +4,41 @@ description: A module that is embedded in the multiple_flutters benchmark test. version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 google_fonts: 4.0.4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - ffi: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http: 0.13.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider: 2.0.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_android: 2.0.27 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_foundation: 2.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_linux: 2.1.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_platform_interface: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_windows: 2.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_android: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_foundation: 2.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_linux: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_platform_interface: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_windows: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - win32: 5.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - xdg_directories: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + win32: 5.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xdg_directories: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true @@ -50,4 +48,4 @@ flutter: androidPackage: com.example.multiple_flutters_module iosBundleIdentifier: com.example.multipleFluttersModule -# PUBSPEC CHECKSUM: 4eb9 +# PUBSPEC CHECKSUM: 5c31 diff --git a/dev/benchmarks/platform_channels_benchmarks/pubspec.yaml b/dev/benchmarks/platform_channels_benchmarks/pubspec.yaml index ebb1feccb255e..97c7fe140ce69 100644 --- a/dev/benchmarks/platform_channels_benchmarks/pubspec.yaml +++ b/dev/benchmarks/platform_channels_benchmarks/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -14,16 +14,16 @@ dependencies: sdk: flutter microbenchmarks: path: ../microbenchmarks - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -39,7 +39,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -53,20 +53,20 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test: 1.24.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test: 1.24.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: @@ -74,4 +74,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 918b +# PUBSPEC CHECKSUM: 7bea diff --git a/dev/benchmarks/platform_views_layout/android/gradle.properties b/dev/benchmarks/platform_views_layout/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/platform_views_layout/android/gradle.properties +++ b/dev/benchmarks/platform_views_layout/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/platform_views_layout/pubspec.yaml b/dev/benchmarks/platform_views_layout/pubspec.yaml index 396fbd7838694..cd9dc6f60c4ee 100644 --- a/dev/benchmarks/platform_views_layout/pubspec.yaml +++ b/dev/benchmarks/platform_views_layout/pubspec.yaml @@ -2,7 +2,7 @@ name: platform_views_layout description: A benchmark for platform views. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -19,31 +19,31 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -68,11 +68,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -81,4 +81,4 @@ flutter: - packages/flutter_gallery_assets/people/square/ali.png - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png -# PUBSPEC CHECKSUM: 1c29 +# PUBSPEC CHECKSUM: d287 diff --git a/dev/benchmarks/platform_views_layout_hybrid_composition/android/gradle.properties b/dev/benchmarks/platform_views_layout_hybrid_composition/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/platform_views_layout_hybrid_composition/android/gradle.properties +++ b/dev/benchmarks/platform_views_layout_hybrid_composition/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/platform_views_layout_hybrid_composition/pubspec.yaml b/dev/benchmarks/platform_views_layout_hybrid_composition/pubspec.yaml index df764b6158940..446a0b58dcec3 100644 --- a/dev/benchmarks/platform_views_layout_hybrid_composition/pubspec.yaml +++ b/dev/benchmarks/platform_views_layout_hybrid_composition/pubspec.yaml @@ -2,7 +2,7 @@ name: platform_views_layout_hybrid_composition description: A benchmark for platform views, using hybrid composition on android. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -19,31 +19,31 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -68,11 +68,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -81,4 +81,4 @@ flutter: - packages/flutter_gallery_assets/people/square/ali.png - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png -# PUBSPEC CHECKSUM: 1c29 +# PUBSPEC CHECKSUM: d287 diff --git a/dev/benchmarks/test_apps/stocks/android/gradle.properties b/dev/benchmarks/test_apps/stocks/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/benchmarks/test_apps/stocks/android/gradle.properties +++ b/dev/benchmarks/test_apps/stocks/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/benchmarks/test_apps/stocks/pubspec.yaml b/dev/benchmarks/test_apps/stocks/pubspec.yaml index 5d08473095290..526153083cc05 100644 --- a/dev/benchmarks/test_apps/stocks/pubspec.yaml +++ b/dev/benchmarks/test_apps/stocks/pubspec.yaml @@ -1,7 +1,7 @@ name: stocks environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -15,27 +15,27 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -61,19 +61,19 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: d14b +# PUBSPEC CHECKSUM: 6aa9 diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 19056c54f36b1..39f739277f716 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -87,6 +87,9 @@ Future run(List arguments) async { foundError(['The analyze.dart script must be run with --enable-asserts.']); } + printProgress('TargetPlatform tool/framework consistency'); + await verifyTargetPlatform(flutterRoot); + printProgress('No Double.clamp'); await verifyNoDoubleClamp(flutterRoot); @@ -187,6 +190,13 @@ Future run(List arguments) async { workingDirectory: flutterRoot, ); + // Make sure that all of the existing samples are linked from at least one API doc comment. + printProgress('Code sample link validation...'); + await runCommand(dart, + ['--enable-asserts', path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart')], + workingDirectory: flutterRoot, + ); + // Try analysis against a big version of the gallery; generate into a temporary directory. printProgress('Dart analysis (mega gallery)...'); final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); @@ -259,6 +269,84 @@ class _DoubleClampVisitor extends RecursiveAstVisitor { } } +Future verifyTargetPlatform(String workingDirectory) async { + final File framework = File('$workingDirectory/packages/flutter/lib/src/foundation/platform.dart'); + final Set frameworkPlatforms = {}; + List lines = framework.readAsLinesSync(); + int index = 0; + while (true) { + if (index >= lines.length) { + foundError(['${framework.path}: Can no longer find TargetPlatform enum.']); + return; + } + if (lines[index].startsWith('enum TargetPlatform {')) { + index += 1; + break; + } + index += 1; + } + while (true) { + if (index >= lines.length) { + foundError(['${framework.path}: Could not find end of TargetPlatform enum.']); + return; + } + String line = lines[index].trim(); + final int comment = line.indexOf('//'); + if (comment >= 0) { + line = line.substring(0, comment); + } + if (line == '}') { + break; + } + if (line.isNotEmpty) { + if (line.endsWith(',')) { + frameworkPlatforms.add(line.substring(0, line.length - 1)); + } else { + foundError(['${framework.path}:$index: unparseable line when looking for TargetPlatform values']); + } + } + index += 1; + } + final File tool = File('$workingDirectory/packages/flutter_tools/lib/src/resident_runner.dart'); + final Set toolPlatforms = {}; + lines = tool.readAsLinesSync(); + index = 0; + while (true) { + if (index >= lines.length) { + foundError(['${tool.path}: Can no longer find nextPlatform logic.']); + return; + } + if (lines[index].trim().startsWith('const List platforms = [')) { + index += 1; + break; + } + index += 1; + } + while (true) { + if (index >= lines.length) { + foundError(['${tool.path}: Could not find end of nextPlatform logic.']); + return; + } + final String line = lines[index].trim(); + if (line.startsWith("'") && line.endsWith("',")) { + toolPlatforms.add(line.substring(1, line.length - 2)); + } else if (line == '];') { + break; + } else { + foundError(['${tool.path}:$index: unparseable line when looking for nextPlatform values']); + } + index += 1; + } + final Set frameworkExtra = frameworkPlatforms.difference(toolPlatforms); + if (frameworkExtra.isNotEmpty) { + foundError(['TargetPlatform has some extra values not found in the tool: ${frameworkExtra.join(", ")}']); + } + final Set toolExtra = toolPlatforms.difference(frameworkPlatforms); + if (toolExtra.isNotEmpty) { + foundError(['The nextPlatform logic in the tool has some extra values not found in TargetPlatform: ${toolExtra.join(", ")}']); + } +} + /// Verify that we use clampDouble instead of Double.clamp for performance reasons. /// /// We currently can't distinguish valid uses of clamp from problematic ones so diff --git a/dev/bots/analyze_snippet_code.dart b/dev/bots/analyze_snippet_code.dart index 119311239e780..924ae2a03efb5 100644 --- a/dev/bots/analyze_snippet_code.dart +++ b/dev/bots/analyze_snippet_code.dart @@ -5,7 +5,7 @@ // To run this, from the root of the Flutter repository: // bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/analyze_snippet_code.dart -// In general, please prefer using full inline examples in API docs. +// In general, please prefer using full linked examples in API docs. // // For documentation on creating sample code, see ../../examples/api/README.md // See also our style guide's discussion on documentation and sample code: @@ -13,7 +13,7 @@ // // This tool is used to analyze smaller snippets of code in the API docs. // Such snippets are wrapped in ```dart ... ``` blocks, which may themselves -// be wrapped in {@tool snippet} ... {@endtool} blocks to set them apart +// be wrapped in {@tool snippet} ... {@end-tool} blocks to set them apart // in the rendered output. // // Such snippets: @@ -55,10 +55,17 @@ // // At the top of a file you can say `// Examples can assume:` and then list some // commented-out declarations that will be included in the analysis for snippets -// in that file. +// in that file. This section may also contain explicit import statements. // -// Snippets generally import all the main Flutter packages (including material -// and flutter_test), as well as most core Dart packages with the usual prefixes. +// For files without an `// Examples can assume:` section or if that section +// contains no explicit imports, the snippets will implicitly import all the +// main Flutter packages (including material and flutter_test), as well as most +// core Dart packages with the usual prefixes. +// +// When invoked without an additional path argument, the script will analyze +// the code snippets for all packages in the "packages" subdirectory that do +// not specify "nodoc: true" in their pubspec.yaml (i.e. all packages for which +// we publish docs will have their doc code snippets analyzed). import 'dart:async'; import 'dart:convert'; @@ -70,7 +77,7 @@ import 'package:path/path.dart' as path; import 'package:watcher/watcher.dart'; final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); -final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib'); +final String _packageFlutter = path.join(_flutterRoot, 'packages', 'flutter', 'lib'); final String _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui'); final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); @@ -142,12 +149,28 @@ Future main(List arguments) async { exit(0); } - Directory flutterPackage; + final List flutterPackages; if (parsedArguments.rest.length == 1) { // Used for testing. - flutterPackage = Directory(parsedArguments.rest.single); + flutterPackages = [Directory(parsedArguments.rest.single)]; } else { - flutterPackage = Directory(_defaultFlutterPackage); + // By default analyze snippets in all packages in the packages subdirectory + // that do not specify "nodoc: true" in their pubspec.yaml. + flutterPackages = []; + final String packagesRoot = path.join(_flutterRoot, 'packages'); + for (final FileSystemEntity entity in Directory(packagesRoot).listSync()) { + if (entity is! Directory) { + continue; + } + final File pubspec = File(path.join(entity.path, 'pubspec.yaml')); + if (!pubspec.existsSync()) { + throw StateError("Unexpected package '${entity.path}' found in packages directory"); + } + if (!pubspec.readAsStringSync().contains('nodoc: true')) { + flutterPackages.add(Directory(path.join(entity.path, 'lib'))); + } + } + assert(flutterPackages.length >= 4); } final bool includeDartUi = parsedArguments.wasParsed('dart-ui-location') || parsedArguments['include-dart-ui'] as bool; @@ -165,14 +188,14 @@ Future main(List arguments) async { if (parsedArguments['interactive'] != null) { await _runInteractive( - flutterPackage: flutterPackage, + flutterPackages: flutterPackages, tempDirectory: parsedArguments['temp'] as String?, filePath: parsedArguments['interactive'] as String, dartUiLocation: includeDartUi ? dartUiLocation : null, ); } else { if (await _SnippetChecker( - flutterPackage, + flutterPackages, tempDirectory: parsedArguments['temp'] as String?, verbose: parsedArguments['verbose'] as bool, dartUiLocation: includeDartUi ? dartUiLocation : null, @@ -360,7 +383,7 @@ class _SnippetChecker { /// supplied, the default location of the `dart:ui` code in the Flutter /// repository is used (i.e. "/bin/cache/pkg/sky_engine/lib/ui"). _SnippetChecker( - this._flutterPackage, { + this._flutterPackages, { String? tempDirectory, this.verbose = false, Directory? dartUiLocation, @@ -438,8 +461,8 @@ class _SnippetChecker { /// automatically if there are no errors unless _keepTmp is true. final Directory _tempDirectory; - /// The package directory for the flutter package within the flutter root dir. - final Directory _flutterPackage; + /// The package directories within the flutter root dir that will be checked. + final List _flutterPackages; /// The directory for the dart:ui code to be analyzed with the flutter code. /// @@ -453,12 +476,14 @@ class _SnippetChecker { } static const List ignoresDirectives = [ - '// ignore_for_file: duplicate_ignore', '// ignore_for_file: directives_ordering', + '// ignore_for_file: duplicate_ignore', + '// ignore_for_file: no_leading_underscores_for_local_identifiers', '// ignore_for_file: prefer_final_locals', '// ignore_for_file: unnecessary_import', '// ignore_for_file: unreachable_from_main', '// ignore_for_file: unused_element', + '// ignore_for_file: unused_element_parameter', '// ignore_for_file: unused_local_variable', ]; @@ -480,7 +505,7 @@ class _SnippetChecker { "import 'dart:typed_data';", "import 'dart:ui' as ui;", "import 'package:flutter_test/flutter_test.dart';", - for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) + for (final File file in _listDartFiles(Directory(_packageFlutter))) "import 'package:flutter/${path.basename(file.path)}';", ].map<_Line>((String code) => _Line.generated(code: code)).toList(); } @@ -490,13 +515,14 @@ class _SnippetChecker { /// Returns true if any errors are found, false otherwise. Future checkSnippets() async { final Map snippets = {}; - if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) { - stderr.writeln('Unable to analyze engine dart snippets at ${_dartUiLocation!.path}.'); + if (_dartUiLocation != null && !_dartUiLocation.existsSync()) { + stderr.writeln('Unable to analyze engine dart snippets at ${_dartUiLocation.path}.'); } final List filesToAnalyze = [ - ..._listDartFiles(_flutterPackage, recursive: true), - if (_dartUiLocation != null && _dartUiLocation!.existsSync()) - ..._listDartFiles(_dartUiLocation!, recursive: true), + for (final Directory flutterPackage in _flutterPackages) + ..._listDartFiles(flutterPackage, recursive: true), + if (_dartUiLocation != null && _dartUiLocation.existsSync()) + ..._listDartFiles(_dartUiLocation, recursive: true), ]; final Set errors = {}; errors.addAll(await _extractSnippets(filesToAnalyze, snippetMap: snippets)); @@ -564,6 +590,7 @@ class _SnippetChecker { final List fileLines = file.readAsLinesSync(); final List<_Line> ignorePreambleLinesOnly = <_Line>[]; final List<_Line> preambleLines = <_Line>[]; + final List<_Line> customImports = <_Line>[]; bool inExamplesCanAssumePreamble = false; // Whether or not we're in the file-wide preamble section ("Examples can assume"). bool inToolSection = false; // Whether or not we're in a code snippet bool inDartSection = false; // Whether or not we're in a '```dart' segment. @@ -582,7 +609,11 @@ class _SnippetChecker { throw _SnippetCheckerException('Unexpected content in snippet code preamble.', file: relativeFilePath, line: lineNumber); } else { final _Line newLine = _Line(line: lineNumber, indent: 3, code: line.substring(3)); - preambleLines.add(newLine); + if (newLine.code.startsWith('import ')) { + customImports.add(newLine); + } else { + preambleLines.add(newLine); + } if (line.startsWith('// // ignore_for_file: ')) { ignorePreambleLinesOnly.add(newLine); } @@ -602,7 +633,7 @@ class _SnippetChecker { } if (trimmedLine.startsWith(_codeBlockEndRegex)) { inDartSection = false; - final _SnippetFile snippet = _processBlock(startLine, block, preambleLines, ignorePreambleLinesOnly, relativeFilePath, lastExample); + final _SnippetFile snippet = _processBlock(startLine, block, preambleLines, ignorePreambleLinesOnly, relativeFilePath, lastExample, customImports); final String path = _writeSnippetFile(snippet).path; assert(!snippetMap.containsKey(path)); snippetMap[path] = snippet; @@ -645,6 +676,7 @@ class _SnippetChecker { line.contains('```kotlin') || line.contains('```swift') || line.contains('```glsl') || + line.contains('```json') || line.contains('```csv')) { inOtherBlock = true; } else if (line.startsWith(_uncheckedCodeBlockStartRegex)) { @@ -684,7 +716,7 @@ class _SnippetChecker { /// a primitive heuristic to make snippet blocks into valid Dart code. /// /// `block` argument will get mutated, but is copied before this function returns. - _SnippetFile _processBlock(_Line startingLine, List block, List<_Line> assumptions, List<_Line> ignoreAssumptionsOnly, String filename, _SnippetFile? lastExample) { + _SnippetFile _processBlock(_Line startingLine, List block, List<_Line> assumptions, List<_Line> ignoreAssumptionsOnly, String filename, _SnippetFile? lastExample, List<_Line> customImports) { if (block.isEmpty) { throw _SnippetCheckerException('${startingLine.asLocation(filename, 0)}: Empty ```dart block in snippet code.'); } @@ -745,7 +777,7 @@ class _SnippetChecker { return _SnippetFile.fromStrings( startingLine, block.toList(), - importPreviousExample ? <_Line>[] : headersWithoutImports, + headersWithoutImports, <_Line>[ ...ignoreAssumptionsOnly, if (hasEllipsis) @@ -754,13 +786,24 @@ class _SnippetChecker { 'self-contained program', filename, ); - } else if (hasStatefulWidgetComment) { + } + + final List<_Line> headers = switch ((importPreviousExample, customImports.length)) { + (true, _) => <_Line>[], + (false, 0) => headersWithImports, + (false, _) => <_Line>[ + ...headersWithoutImports, + const _Line.generated(code: '// ignore_for_file: unused_import'), + ...customImports, + ] + }; + if (hasStatefulWidgetComment) { return _SnippetFile.fromStrings( startingLine, prefix: 'class _State extends State {', block.toList(), postfix: '}', - importPreviousExample ? <_Line>[] : headersWithImports, + headers, preamble, 'stateful widget', filename, @@ -772,7 +815,7 @@ class _SnippetChecker { return _SnippetFile.fromStrings( startingLine, block.toList(), - importPreviousExample ? <_Line>[] : headersWithImports, + headers, preamble, 'top-level declaration', filename, @@ -785,7 +828,7 @@ class _SnippetChecker { prefix: 'Future function() async {', block.toList(), postfix: '}', - importPreviousExample ? <_Line>[] : headersWithImports, + headers, preamble, 'statement', filename, @@ -797,7 +840,7 @@ class _SnippetChecker { prefix: 'class Class {', block.toList(), postfix: '}', - importPreviousExample ? <_Line>[] : headersWithImports, + headers, <_Line>[ ...preamble, const _Line.generated(code: '// ignore_for_file: avoid_classes_with_only_static_members'), @@ -839,7 +882,7 @@ class _SnippetChecker { prefix: 'dynamic expression = ', block.toList(), postfix: ';', - importPreviousExample ? <_Line>[] : headersWithImports, + headers, preamble, 'expression', filename, @@ -1083,7 +1126,7 @@ class _SnippetFile { Future _runInteractive({ required String? tempDirectory, - required Directory flutterPackage, + required List flutterPackages, required String filePath, required Directory? dartUiLocation, }) async { @@ -1105,7 +1148,7 @@ Future _runInteractive({ print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...'); print('Type "q" to quit, or "r" to force a reload.'); - final _SnippetChecker checker = _SnippetChecker(flutterPackage, tempDirectory: tempDirectory) + final _SnippetChecker checker = _SnippetChecker(flutterPackages, tempDirectory: tempDirectory) .._createConfigurationFiles(); ProcessSignal.sigint.watch().listen((_) { diff --git a/dev/bots/check_code_samples.dart b/dev/bots/check_code_samples.dart new file mode 100644 index 0000000000000..1082702e9b5be --- /dev/null +++ b/dev/bots/check_code_samples.dart @@ -0,0 +1,476 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// To run this, from the root of the Flutter repository: +// bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/check_code_sample_links.dart + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:path/path.dart' as path; + +import 'utils.dart'; + +final String _scriptLocation = path.fromUri(Platform.script); +final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation))); +final String _exampleDirectoryPath = path.join(_flutterRoot, 'examples', 'api'); +final String _packageDirectoryPath = path.join(_flutterRoot, 'packages'); +final String _dartUIDirectoryPath = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib'); + +final List _knownUnlinkedExamples = [ + // These are template files that aren't expected to be linked. + 'examples/api/lib/sample_templates/cupertino.0.dart', + 'examples/api/lib/sample_templates/widgets.0.dart', + 'examples/api/lib/sample_templates/material.0.dart', +]; + +void main(List args) { + final ArgParser argParser = ArgParser(); + argParser.addFlag( + 'help', + negatable: false, + help: 'Print help for this command.', + ); + argParser.addOption( + 'examples', + valueHelp: 'path', + defaultsTo: _exampleDirectoryPath, + help: 'A location where the API doc examples are found.', + ); + argParser.addOption( + 'packages', + valueHelp: 'path', + defaultsTo: _packageDirectoryPath, + help: 'A location where the source code that should link the API doc examples is found.', + ); + argParser.addOption( + 'dart-ui', + valueHelp: 'path', + defaultsTo: _dartUIDirectoryPath, + help: 'A location where the source code that should link the API doc examples is found.', + ); + argParser.addOption( + 'flutter-root', + valueHelp: 'path', + defaultsTo: _flutterRoot, + help: 'The path to the root of the Flutter repo.', + ); + final ArgResults parsedArgs; + + void usage() { + print('dart --enable-asserts ${path.basename(_scriptLocation)} [options]'); + print(argParser.usage); + } + + try { + parsedArgs = argParser.parse(args); + } on FormatException catch (e) { + print(e.message); + usage(); + exit(1); + } + + if (parsedArgs['help'] as bool) { + usage(); + exit(0); + } + + const FileSystem filesystem = LocalFileSystem(); + final Directory examples = filesystem.directory(parsedArgs['examples']! as String); + final Directory packages = filesystem.directory(parsedArgs['packages']! as String); + final Directory dartUIPath = filesystem.directory(parsedArgs['dart-ui']! as String); + final Directory flutterRoot = filesystem.directory(parsedArgs['flutter-root']! as String); + + final SampleChecker checker = SampleChecker( + examples: examples, + packages: packages, + dartUIPath: dartUIPath, + flutterRoot: flutterRoot, + ); + + if (!checker.checkCodeSamples()) { + reportErrorsAndExit('Some errors were found in the API docs code samples.'); + } + reportSuccessAndExit('All examples are linked and have tests.'); +} + +class SampleChecker { + SampleChecker({ + required this.examples, + required this.packages, + required this.dartUIPath, + required this.flutterRoot, + this.filesystem = const LocalFileSystem(), + }); + + final Directory examples; + final Directory packages; + final Directory dartUIPath; + final Directory flutterRoot; + final FileSystem filesystem; + + bool checkCodeSamples() { + filesystem.currentDirectory = flutterRoot; + + // Get a list of all the filenames in the source directory that end in "[0-9]+.dart". + final List exampleFilenames = getExampleFilenames(examples); + + // Get a list of all the example link paths that appear in the source files. + final Set exampleLinks = getExampleLinks(packages); + + // Also add in any that might be found in the dart:ui directory. + exampleLinks.addAll(getExampleLinks(dartUIPath)); + + // Get a list of the filenames that were not found in the source files. + final List missingFilenames = checkForMissingLinks(exampleFilenames, exampleLinks); + + // Get a list of any tests that are missing, as well as any that used to be + // missing, but have been implemented. + final (List missingTests, List noLongerMissing) = checkForMissingTests(exampleFilenames); + + // Remove any that we know are exceptions (examples that aren't expected to be + // linked into any source files). These are typically template files used to + // generate new examples. + missingFilenames.removeWhere((String file) => _knownUnlinkedExamples.contains(file)); + + if (missingFilenames.isEmpty && missingTests.isEmpty && noLongerMissing.isEmpty) { + return true; + } + + if (noLongerMissing.isNotEmpty) { + final StringBuffer buffer = StringBuffer('The following tests have been implemented! Huzzah!:\n'); + for (final File name in noLongerMissing) { + buffer.writeln(' ${getRelativePath(name)}'); + } + buffer.writeln('However, they now need to be removed from the _knownMissingTests'); + buffer.write('list in the script $_scriptLocation.'); + foundError(buffer.toString().split('\n')); + } + + if (missingTests.isNotEmpty) { + final StringBuffer buffer = StringBuffer('The following example test files are missing:\n'); + for (final File name in missingTests) { + buffer.writeln(' ${getRelativePath(name)}'); + } + foundError(buffer.toString().trimRight().split('\n')); + } + + if (missingFilenames.isNotEmpty) { + final StringBuffer buffer = + StringBuffer('The following examples are not linked from any source file API doc comments:\n'); + for (final String name in missingFilenames) { + buffer.writeln(' $name'); + } + buffer.write('Either link them to a source file API doc comment, or remove them.'); + foundError(buffer.toString().split('\n')); + } + return false; + } + + String getRelativePath(File file, [Directory? root]) { + root ??= flutterRoot; + return path.relative(file.absolute.path, from: root.absolute.path); + } + + List getFiles(Directory directory, [Pattern? filenamePattern]) { + final List filenames = directory + .listSync(recursive: true) + .map((FileSystemEntity entity) { + if (entity is File) { + return entity; + } else { + return null; + } + }) + .where((File? filename) => + filename != null && (filenamePattern == null || filename.absolute.path.contains(filenamePattern))) + .map((File? s) => s!) + .toList(); + return filenames; + } + + List getExampleFilenames(Directory directory) { + return getFiles( + directory.childDirectory('lib'), + RegExp(r'\d+\.dart$'), + ); + } + + Set getExampleLinks(Directory searchDirectory) { + final List files = getFiles(searchDirectory, RegExp(r'\.dart$')); + final Set searchStrings = {}; + final RegExp exampleRe = RegExp(r'\*\* See code in (?.*) \*\*'); + for (final File file in files) { + final String contents = file.readAsStringSync(); + searchStrings.addAll( + contents.split('\n').where((String s) => s.contains(exampleRe)).map( + (String e) { + return exampleRe.firstMatch(e)!.namedGroup('path')!; + }, + ), + ); + } + return searchStrings; + } + + List checkForMissingLinks(List exampleFilenames, Set searchStrings) { + final List missingFilenames = []; + for (final File example in exampleFilenames) { + final String relativePath = getRelativePath(example); + if (!searchStrings.contains(relativePath)) { + missingFilenames.add(relativePath); + } + } + return missingFilenames; + } + + String getTestNameForExample(File example, Directory examples) { + final String testPath = path.dirname( + path.join( + examples.absolute.path, + 'test', + getRelativePath(example, examples.childDirectory('lib')), + ), + ); + return '${path.join(testPath, path.basenameWithoutExtension(example.path))}_test.dart'; + } + + (List, List) checkForMissingTests(List exampleFilenames) { + final List missingTests = []; + final List noLongerMissingTests = []; + for (final File example in exampleFilenames) { + final File testFile = filesystem.file(getTestNameForExample(example, examples)); + final String name = path.relative(testFile.absolute.path, from: flutterRoot.absolute.path); + if (!testFile.existsSync()) { + missingTests.add(testFile); + } else if (_knownMissingTests.contains(name.replaceAll(r'\', '/'))) { + noLongerMissingTests.add(testFile); + } + } + // Skip any that we know are missing. + missingTests.removeWhere( + (File test) { + final String name = path.relative(test.absolute.path, from: flutterRoot.absolute.path).replaceAll(r'\', '/'); + return _knownMissingTests.contains(name); + }, + ); + return (missingTests, noLongerMissingTests); + } +} + +// These tests are known to be missing. They should all eventually be +// implemented, but until they are we allow them, so that we can catch any new +// examples that are added without tests. +// +// TODO(gspencergoog): implement the missing tests. +// See https://github.com/flutter/flutter/issues/130459 +final Set _knownMissingTests = { + 'examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart', + 'examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart', + 'examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart', + 'examples/api/test/material/theme/theme_extension.1_test.dart', + 'examples/api/test/material/elevated_button/elevated_button.0_test.dart', + 'examples/api/test/material/material_state/material_state_border_side.0_test.dart', + 'examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart', + 'examples/api/test/material/material_state/material_state_outlined_border.0_test.dart', + 'examples/api/test/material/material_state/material_state_property.0_test.dart', + 'examples/api/test/material/selectable_region/selectable_region.0_test.dart', + 'examples/api/test/material/text_field/text_field.2_test.dart', + 'examples/api/test/material/text_field/text_field.1_test.dart', + 'examples/api/test/material/button_style/button_style.0_test.dart', + 'examples/api/test/material/range_slider/range_slider.0_test.dart', + 'examples/api/test/material/card/card.2_test.dart', + 'examples/api/test/material/card/card.0_test.dart', + 'examples/api/test/material/selection_container/selection_container_disabled.0_test.dart', + 'examples/api/test/material/selection_container/selection_container.0_test.dart', + 'examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart', + 'examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart', + 'examples/api/test/material/menu_anchor/menu_anchor.2_test.dart', + 'examples/api/test/material/stepper/stepper.controls_builder.0_test.dart', + 'examples/api/test/material/stepper/stepper.0_test.dart', + 'examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart', + 'examples/api/test/material/data_table/data_table.0_test.dart', + 'examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart', + 'examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.0_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.2_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.1_test.dart', + 'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart', + 'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart', + 'examples/api/test/material/outlined_button/outlined_button.0_test.dart', + 'examples/api/test/material/icon_button/icon_button.2_test.dart', + 'examples/api/test/material/icon_button/icon_button.3_test.dart', + 'examples/api/test/material/icon_button/icon_button.0_test.dart', + 'examples/api/test/material/icon_button/icon_button.1_test.dart', + 'examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.1_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.material_state.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.2_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.label.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.3_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.material_state.1_test.dart', + 'examples/api/test/material/filled_button/filled_button.0_test.dart', + 'examples/api/test/material/text_form_field/text_form_field.1_test.dart', + 'examples/api/test/material/scrollbar/scrollbar.1_test.dart', + 'examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart', + 'examples/api/test/material/radio/radio.toggleable.0_test.dart', + 'examples/api/test/material/radio/radio.0_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.0_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.1_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.2_test.dart', + 'examples/api/test/material/about/about_list_tile.0_test.dart', + 'examples/api/test/material/tab_controller/tab_controller.1_test.dart', + 'examples/api/test/material/selection_area/selection_area.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.drawer.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.1_test.dart', + 'examples/api/test/material/scaffold/scaffold.of.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.2_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.of.1_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart', + 'examples/api/test/material/segmented_button/segmented_button.0_test.dart', + 'examples/api/test/material/app_bar/app_bar.2_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.1_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.2_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.3_test.dart', + 'examples/api/test/material/app_bar/app_bar.1_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.4_test.dart', + 'examples/api/test/material/app_bar/app_bar.3_test.dart', + 'examples/api/test/material/app_bar/app_bar.0_test.dart', + 'examples/api/test/material/ink_well/ink_well.0_test.dart', + 'examples/api/test/material/banner/material_banner.1_test.dart', + 'examples/api/test/material/banner/material_banner.0_test.dart', + 'examples/api/test/material/checkbox/checkbox.1_test.dart', + 'examples/api/test/material/checkbox/checkbox.0_test.dart', + 'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart', + 'examples/api/test/material/text_button/text_button.0_test.dart', + 'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', + 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', + 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', + 'examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart', + 'examples/api/test/painting/axis_direction/axis_direction.0_test.dart', + 'examples/api/test/painting/linear_border/linear_border.0_test.dart', + 'examples/api/test/painting/gradient/linear_gradient.0_test.dart', + 'examples/api/test/painting/star_border/star_border.0_test.dart', + 'examples/api/test/painting/borders/border_side.stroke_align.0_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart', + 'examples/api/test/widgets/navigator/restorable_route_future.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart', + 'examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart', + 'examples/api/test/widgets/focus_manager/focus_node.0_test.dart', + 'examples/api/test/widgets/framework/build_owner.0_test.dart', + 'examples/api/test/widgets/framework/error_widget.0_test.dart', + 'examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart', + 'examples/api/test/widgets/sliver/decorated_sliver.0_test.dart', + 'examples/api/test/widgets/autofill/autofill_group.0_test.dart', + 'examples/api/test/widgets/drag_target/draggable.0_test.dart', + 'examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart', + 'examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart', + 'examples/api/test/widgets/form/form.0_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart', + 'examples/api/test/widgets/page_view/page_view.0_test.dart', + 'examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart', + 'examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart', + 'examples/api/test/widgets/async/stream_builder.0_test.dart', + 'examples/api/test/widgets/async/future_builder.0_test.dart', + 'examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart', + 'examples/api/test/widgets/animated_size/animated_size.0_test.dart', + 'examples/api/test/widgets/table/table.0_test.dart', + 'examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart', + 'examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart', + 'examples/api/test/widgets/transitions/positioned_transition.0_test.dart', + 'examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart', + 'examples/api/test/widgets/transitions/align_transition.0_test.dart', + 'examples/api/test/widgets/transitions/fade_transition.0_test.dart', + 'examples/api/test/widgets/transitions/animated_builder.0_test.dart', + 'examples/api/test/widgets/transitions/rotation_transition.0_test.dart', + 'examples/api/test/widgets/transitions/animated_widget.0_test.dart', + 'examples/api/test/widgets/transitions/slide_transition.0_test.dart', + 'examples/api/test/widgets/transitions/listenable_builder.2_test.dart', + 'examples/api/test/widgets/transitions/scale_transition.0_test.dart', + 'examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart', + 'examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart', + 'examples/api/test/widgets/transitions/size_transition.0_test.dart', + 'examples/api/test/widgets/animated_list/animated_list.0_test.dart', + 'examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart', + 'examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart', + 'examples/api/test/widgets/image/image.error_builder.0_test.dart', + 'examples/api/test/widgets/image/image.frame_builder.0_test.dart', + 'examples/api/test/widgets/image/image.loading_builder.0_test.dart', + 'examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart', + 'examples/api/test/widgets/shortcuts/shortcuts.0_test.dart', + 'examples/api/test/widgets/shortcuts/single_activator.single_activator.0_test.dart', + 'examples/api/test/widgets/shortcuts/shortcuts.1_test.dart', + 'examples/api/test/widgets/shortcuts/character_activator.0_test.dart', + 'examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart', + 'examples/api/test/widgets/page_storage/page_storage.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart', + 'examples/api/test/widgets/notification_listener/notification.0_test.dart', + 'examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart', + 'examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart', + 'examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart', + 'examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart', + 'examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart', + 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart', + 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart', + 'examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart', + 'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart', + 'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart', + 'examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart', + 'examples/api/test/widgets/restoration/restoration_mixin.0_test.dart', + 'examples/api/test/widgets/actions/actions.0_test.dart', + 'examples/api/test/widgets/actions/action_listener.0_test.dart', + 'examples/api/test/widgets/actions/focusable_action_detector.0_test.dart', + 'examples/api/test/widgets/color_filter/color_filtered.0_test.dart', + 'examples/api/test/widgets/focus_scope/focus.2_test.dart', + 'examples/api/test/widgets/focus_scope/focus.0_test.dart', + 'examples/api/test/widgets/focus_scope/focus.1_test.dart', + 'examples/api/test/widgets/focus_scope/focus_scope.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_align.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart', + 'examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_container.0_test.dart', + 'examples/api/test/widgets/dismissible/dismissible.0_test.dart', + 'examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart', + 'examples/api/test/widgets/preferred_size/preferred_size.0_test.dart', + 'examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', + 'examples/api/test/animation/curves/curve2_d.0_test.dart', + 'examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart', +}; diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh index 6a0cec83c97d8..c070086aac4ee 100755 --- a/dev/bots/docs.sh +++ b/dev/bots/docs.sh @@ -16,102 +16,13 @@ function script_location() { cd -P "$(dirname "$script_location")" >/dev/null && pwd } -function generate_docs() { - # Install and activate dartdoc. - # When updating to a new dartdoc version, please also update - # `dartdoc_options.yaml` to include newly introduced error and warning types. - "$DART" pub global activate dartdoc 6.3.0 - - # Install and activate the snippets tool, which resides in the - # assets-for-api-docs repo: - # https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets - "$DART" pub global activate snippets 0.3.1 - - # This script generates a unified doc set, and creates - # a custom index.html, placing everything into dev/docs/doc. - (cd "$FLUTTER_ROOT/dev/tools" && "$FLUTTER" pub get) - (cd "$FLUTTER_ROOT/dev/tools" && "$DART" pub get) - (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/dartdoc.dart") - (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/java_and_objc_doc.dart") -} - -# Zip up the docs so people can download them for offline usage. -function create_offline_zip() { - # Must be run from "$FLUTTER_ROOT/dev/docs" - echo "$(date): Zipping Flutter offline docs archive." - rm -rf flutter.docs.zip doc/offline - (cd ./doc; zip -r -9 -q ../flutter.docs.zip .) -} - -# Generate the docset for Flutter docs for use with Dash, Zeal, and Velocity. -function create_docset() { - # Must be run from "$FLUTTER_ROOT/dev/docs" - # Must have dashing installed: go get -u github.com/technosophos/dashing - # Dashing produces a LOT of log output (~30MB), so we redirect it, and just - # show the end of it if there was a problem. - echo "$(date): Building Flutter docset." - rm -rf flutter.docset - # If dashing gets stuck, Cirrus will time out the build after an hour, and we - # never get to see the logs. Thus, we run it in the background and tail the logs - # while we wait for it to complete. - dashing_log=/tmp/dashing.log - dashing build --source ./doc --config ./dashing.json > $dashing_log 2>&1 & - dashing_pid=$! - wait $dashing_pid && \ - cp ./doc/flutter/static-assets/favicon.png ./flutter.docset/icon.png && \ - "$DART" --disable-dart-dev --enable-asserts ./dashing_postprocess.dart && \ - tar cf flutter.docset.tar.gz --use-compress-program="gzip --best" flutter.docset - if [[ $? -ne 0 ]]; then - >&2 echo "Dashing docset generation failed" - tail -200 $dashing_log - exit 1 - fi -} - -function deploy_docs() { - case "$LUCI_BRANCH" in - master) - echo "$(date): Updating $LUCI_BRANCH docs: https://master-api.flutter.dev/" - # Disable search indexing on the master staging site so searches get only - # the stable site. - echo -e "User-agent: *\nDisallow: /" > "$FLUTTER_ROOT/dev/docs/doc/robots.txt" - ;; - stable) - echo "$(date): Updating $LUCI_BRANCH docs: https://api.flutter.dev/" - # Enable search indexing on the master staging site so searches get only - # the stable site. - echo -e "# All robots welcome!" > "$FLUTTER_ROOT/dev/docs/doc/robots.txt" - ;; - *) - >&2 echo "Docs deployment cannot be run on the $LUCI_BRANCH branch." - exit 0 - esac -} - -# Move the offline archives into place, after all the processing of the doc -# directory is done. This avoids the tools recursively processing the archives -# as part of their process. -function move_offline_into_place() { - # Must be run from "$FLUTTER_ROOT/dev/docs" - echo "$(date): Moving offline data into place." - mkdir -p doc/offline - mv flutter.docs.zip doc/offline/flutter.docs.zip - du -sh doc/offline/flutter.docs.zip - if [[ "$LUCI_BRANCH" == "stable" ]]; then - echo -e "\n ${FLUTTER_VERSION_STRING}\n https://api.flutter.dev/offline/flutter.docset.tar.gz\n" > doc/offline/flutter.xml - else - echo -e "\n ${FLUTTER_VERSION_STRING}\n https://master-api.flutter.dev/offline/flutter.docset.tar.gz\n" > doc/offline/flutter.xml - fi - mv flutter.docset.tar.gz doc/offline/flutter.docset.tar.gz - du -sh doc/offline/flutter.docset.tar.gz -} - # So that users can run this script from anywhere and it will work as expected. SCRIPT_LOCATION="$(script_location)" # Sets the Flutter root to be "$(script_location)/../..": This script assumes # that it resides two directory levels down from the root, so if that changes, # then this line will need to as well. FLUTTER_ROOT="$(dirname "$(dirname "$SCRIPT_LOCATION")")" +export FLUTTER_ROOT echo "$(date): Running docs.sh" @@ -124,31 +35,120 @@ FLUTTER_BIN="$FLUTTER_ROOT/bin" DART_BIN="$FLUTTER_ROOT/bin/cache/dart-sdk/bin" FLUTTER="$FLUTTER_BIN/flutter" DART="$DART_BIN/dart" -export PATH="$FLUTTER_BIN:$DART_BIN:$PATH" +PATH="$FLUTTER_BIN:$DART_BIN:$PATH" -# Make sure dart is installed by invoking Flutter to download it. -# This also creates the 'version' file. -FLUTTER_VERSION=$("$FLUTTER" --version --machine) +# Make sure dart is installed by invoking Flutter to download it if it is missing. +# Also make sure the flutter command is ready to run before capturing output from +# it: if it has to rebuild itself or something, it'll spoil our JSON output. +"$FLUTTER" > /dev/null 2>&1 +FLUTTER_VERSION="$("$FLUTTER" --version --machine)" export FLUTTER_VERSION -FLUTTER_VERSION_STRING=$(cat "$FLUTTER_ROOT/version") # If the pub cache directory exists in the root, then use that. FLUTTER_PUB_CACHE="$FLUTTER_ROOT/.pub-cache" if [[ -d "$FLUTTER_PUB_CACHE" ]]; then # This has to be exported, because pub interprets setting it to the empty # string in the same way as setting it to ".". - export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}" + PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}" + export PUB_CACHE fi -generate_docs -# Skip publishing docs for PRs and release candidate branches -if [[ -n "$LUCI_CI" && -z "$LUCI_PR" ]]; then - (cd "$FLUTTER_ROOT/dev/docs"; create_offline_zip) - (cd "$FLUTTER_ROOT/dev/docs"; create_docset) - (cd "$FLUTTER_ROOT/dev/docs"; move_offline_into_place) - deploy_docs -fi +function usage() { + echo "Usage: $(basename "${BASH_SOURCE[0]}") [--keep-temp] [--output ]" + echo "" + echo " --keep-staging Do not delete the staging directory created while generating" + echo " docs. Normally the script deletes the staging directory after" + echo " generating the output ZIP file." + echo " --output specifies where the output ZIP file containing the documentation" + echo " data will be written." + echo " --staging-dir specifies where the temporary output files will be written while" + echo " generating docs. This directory will be deleted after generation" + echo " unless --keep-staging is also specified." + echo "" +} + +function parse_args() { + local arg + local args=() + STAGING_DIR= + KEEP_STAGING=0 + DESTINATION="$FLUTTER_ROOT/dev/docs/api_docs.zip" + while (( "$#" )); do + case "$1" in + --help) + usage + exit 0 + ;; + --staging-dir) + STAGING_DIR="$2" + shift + ;; + --keep-staging) + KEEP_STAGING=1 + ;; + --output) + DESTINATION="$2" + shift + ;; + *) + args=("${args[@]}" "$1") + ;; + esac + shift + done + if [[ -z $STAGING_DIR ]]; then + STAGING_DIR=$(mktemp -d /tmp/dartdoc.XXXXX) + fi + DOC_DIR="$STAGING_DIR/doc" + if [[ ${#args[@]} != 0 ]]; then + >&2 echo "ERROR: Unknown arguments: ${args[@]}" + usage + exit 1 + fi +} + +function generate_docs() { + # Install and activate dartdoc. + # When updating to a new dartdoc version, please also update + # `dartdoc_options.yaml` to include newly introduced error and warning types. + "$DART" pub global activate dartdoc 6.3.0 + + # Install and activate the snippets tool, which resides in the + # assets-for-api-docs repo: + # https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets + "$DART" pub global activate snippets 0.4.0 + + # This script generates a unified doc set, and creates + # a custom index.html, placing everything into DOC_DIR. + + # Make sure that create_api_docs.dart has all the dependencies it needs. + (cd "$FLUTTER_ROOT/dev/tools" && "$FLUTTER" pub get) + (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/create_api_docs.dart" --output-dir="$DOC_DIR") +} + +function main() { + echo "Writing docs build temporary output to $DOC_DIR" + mkdir -p "$DOC_DIR" + generate_docs + # If the destination isn't an absolute path, make it into one. + if ! [[ "$DESTINATION" =~ ^/ ]]; then + DESTINATION="$PWD/$DESTINATION" + fi + + # Make sure the destination has .zip as an extension, because zip will add it + # anyhow, and we want to print the correct output location. + DESTINATION=${DESTINATION%.zip}.zip + + # Zip up doc directory and write the output to the destination. + (cd "$STAGING_DIR"; zip -r -9 -q "$DESTINATION" ./doc) + if [[ $KEEP_STAGING -eq 1 ]]; then + echo "Staging documentation output left in $STAGING_DIR" + else + echo "Removing staging documentation output from $STAGING_DIR" + rm -rf "$STAGING_DIR" + fi + echo "Wrote docs ZIP file to $DESTINATION" +} -# Zip docs -cd "$FLUTTER_ROOT/dev/docs" -zip -r api_docs.zip doc +parse_args "$@" +main diff --git a/dev/bots/prepare_package.dart b/dev/bots/prepare_package.dart index 9f33a22cc3003..bf30ae38c4ccb 100644 --- a/dev/bots/prepare_package.dart +++ b/dev/bots/prepare_package.dart @@ -833,16 +833,8 @@ class ArchivePublisher { print('gsutil.py -- $args'); return ''; } - if (platform.isWindows) { - return _processRunner.runProcess( - ['python3', path.join(platform.environment['DEPOT_TOOLS']!, 'gsutil.py'), '--', ...args], - workingDirectory: workingDirectory, - failOk: failOk, - ); - } - return _processRunner.runProcess( - ['gsutil.py', '--', ...args], + ['python3', path.join(platform.environment['DEPOT_TOOLS']!, 'gsutil.py'), '--', ...args], workingDirectory: workingDirectory, failOk: failOk, ); diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index d1787a231ad73..6585d67631c46 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -2,36 +2,35 @@ name: tests_on_bots description: Scripts which run on bots. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: args: 2.4.2 crypto: 3.0.3 intl: 0.18.1 + file: 6.1.4 flutter_devicelab: path: ../devicelab http_parser: 4.0.2 - meta: 1.9.1 + meta: 1.10.0 path: 1.8.3 - platform: 3.1.0 + platform: 3.1.2 process: 4.2.4 - test: 1.24.3 + test: 1.24.6 _discoveryapis_commons: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" archive: 3.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" checked_yaml: 2.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - equatable: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - gcloud: 0.8.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + gcloud: 0.8.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" googleapis: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" googleapis_auth: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -42,13 +41,15 @@ dependencies: json_annotation: 4.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - metrics_center: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + metrics_center: 1.0.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 6.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pubspec_parse: 1.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + retry: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -56,20 +57,21 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xml: 6.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test_api: 0.6.0 + test_api: 0.6.1 -# PUBSPEC CHECKSUM: f223 +# PUBSPEC CHECKSUM: a602 diff --git a/dev/bots/service_worker_test.dart b/dev/bots/service_worker_test.dart index 019ccdd90b79c..b110b66875302 100644 --- a/dev/bots/service_worker_test.dart +++ b/dev/bots/service_worker_test.dart @@ -37,6 +37,8 @@ enum ServiceWorkerTestType { withFlutterJsEntrypointLoadedEvent, // Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled. withFlutterJsTrustedTypesOn, + // Uses custom serviceWorkerVersion. + withFlutterJsCustomServiceWorkerVersion, // Entrypoint generated by `flutter create`. generatedEntrypoint, } @@ -58,6 +60,7 @@ Future main() async { await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn); await runWebServiceWorkerTestWithGeneratedEntrypoint(headless: false); await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false); + await runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: false); if (hasError) { reportErrorsAndExit('${bold}One or more tests failed.$reset'); @@ -117,6 +120,8 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) { indexFile = 'index_with_flutterjs_entrypoint_loaded.html'; case ServiceWorkerTestType.withFlutterJsTrustedTypesOn: indexFile = 'index_with_flutterjs_el_tt_on.html'; + case ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion: + indexFile = 'index_with_flutterjs_custom_sw_version.html'; case ServiceWorkerTestType.generatedEntrypoint: indexFile = 'generated_entrypoint.html'; } @@ -703,3 +708,137 @@ Future runWebServiceWorkerTestWithBlockedServiceWorkers({ } print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)'); } + +/// Regression test for https://github.com/flutter/flutter/issues/130212. +Future runWebServiceWorkerTestWithCustomServiceWorkerVersion({ + required bool headless, +}) async { + final Map requestedPathCounts = {}; + void expectRequestCounts(Map expectedCounts) => + _expectRequestCounts(expectedCounts, requestedPathCounts); + + AppServer? server; + Future waitForAppToLoad(Map waitForCounts) async => + _waitForAppToLoad(waitForCounts, requestedPathCounts, server); + + Future startAppServer({ + required String cacheControl, + }) async { + final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests(); + final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests(); + server = await AppServer.start( + headless: headless, + cacheControl: cacheControl, + // TODO(yjbanov): use a better port disambiguation strategy than trying + // to guess what ports other tests use. + appUrl: 'http://localhost:$serverPort/index.html', + serverPort: serverPort, + browserDebugPort: browserDebugPort, + appDirectory: _appBuildDirectory, + additionalRequestHandlers: [ + (Request request) { + final String requestedPath = request.url.path; + requestedPathCounts.putIfAbsent(requestedPath, () => 0); + requestedPathCounts[requestedPath] = requestedPathCounts[requestedPath]! + 1; + if (requestedPath == 'CLOSE') { + return Response.ok('OK'); + } + return Response.notFound(''); + }, + ], + ); + } + + // Preserve old index.html as index_og.html so we can restore it later for other tests + await runCommand( + 'mv', + [ + 'index.html', + 'index_og.html', + ], + workingDirectory: _testAppWebDirectory, + ); + + print('BEGIN runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: $headless)'); + try { + await _rebuildApp(version: 1, testType: ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion, target: _target); + + print('Test page load'); + await startAppServer(cacheControl: 'max-age=0'); + await waitForAppToLoad({ + 'CLOSE': 1, + 'flutter_service_worker.js': 1, + 'assets/fonts/MaterialIcons-Regular.otf': 1, + }); + expectRequestCounts({ + 'index.html': 2, + 'flutter.js': 1, + 'main.dart.js': 1, + 'CLOSE': 1, + 'flutter_service_worker.js': 1, + 'assets/FontManifest.json': 1, + 'assets/AssetManifest.json': 1, + 'assets/fonts/MaterialIcons-Regular.otf': 1, + // In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. + if (!headless) + ...{ + 'manifest.json': 1, + 'favicon.ico': 1, + }, + }); + + print('Test page reload, ensure service worker is not reloaded'); + await server!.chrome.reloadPage(ignoreCache: true); + await waitForAppToLoad({ + 'CLOSE': 1, + 'flutter.js': 1, + }); + expectRequestCounts({ + 'index.html': 1, + 'flutter.js': 1, + 'main.dart.js': 1, + 'assets/FontManifest.json': 1, + 'assets/fonts/MaterialIcons-Regular.otf': 1, + 'CLOSE': 1, + // In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. + if (!headless) + ...{ + 'manifest.json': 1, + 'favicon.ico': 1, + }, + }); + + print('Test page reload after rebuild, ensure service worker is not reloaded'); + await _rebuildApp(version: 1, testType: ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion, target: _target); + await server!.chrome.reloadPage(ignoreCache: true); + await waitForAppToLoad({ + 'CLOSE': 1, + 'flutter.js': 1, + }); + expectRequestCounts({ + 'index.html': 1, + 'flutter.js': 1, + 'main.dart.js': 1, + 'assets/FontManifest.json': 1, + 'assets/fonts/MaterialIcons-Regular.otf': 1, + 'CLOSE': 1, + // In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'. + if (!headless) + ...{ + 'manifest.json': 1, + 'favicon.ico': 1, + }, + }); + } finally { + await runCommand( + 'mv', + [ + 'index_og.html', + 'index.html', + ], + workingDirectory: _testAppWebDirectory, + ); + await server?.stop(); + } + print('END runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: $headless)'); +} diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 1f3e0a0590147..243a2c638bc0a 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -83,6 +83,7 @@ final String flutter = path.join(flutterRoot, 'bin', 'flutter$bat'); final String dart = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', 'dart$exe'); final String pubCache = path.join(flutterRoot, '.pub-cache'); final String engineVersionFile = path.join(flutterRoot, 'bin', 'internal', 'engine.version'); +final String engineRealmFile = path.join(flutterRoot, 'bin', 'internal', 'engine.realm'); final String flutterPackagesVersionFile = path.join(flutterRoot, 'bin', 'internal', 'flutter_packages.version'); String get platformFolderName { @@ -212,7 +213,7 @@ String get shuffleSeed { /// /// Examples: /// SHARD=tool_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart -/// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt +/// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt --local-engine-host=host_debug_unopt Future main(List args) async { try { printProgress('STARTING ANALYSIS'); @@ -220,6 +221,9 @@ Future main(List args) async { if (arg.startsWith('--local-engine=')) { localEngineEnv['FLUTTER_LOCAL_ENGINE'] = arg.substring('--local-engine='.length); flutterTestArgs.add(arg); + } else if (arg.startsWith('--local-engine-host=')) { + localEngineEnv['FLUTTER_LOCAL_ENGINE_HOST'] = arg.substring('--local-engine-host='.length); + flutterTestArgs.add(arg); } else if (arg.startsWith('--local-engine-src-path=')) { localEngineEnv['FLUTTER_LOCAL_ENGINE_SRC_PATH'] = arg.substring('--local-engine-src-path='.length); flutterTestArgs.add(arg); @@ -980,10 +984,14 @@ Future _runFrameworkTests() async { Future runMisc() async { printProgress('${green}Running package tests$reset for directories other than packages/flutter'); await _runTestHarnessTests(); - // await runExampleTests(); - // await _runDartTest(path.join(flutterRoot, 'dev', 'bots')); - // await _runDartTest(path.join(flutterRoot, 'dev', 'devicelab'), ensurePrecompiledTool: false); // See https://github.com/flutter/flutter/issues/86209 - // await _runDartTest(path.join(flutterRoot, 'dev', 'conductor', 'core'), forceSingleCore: true); + await runExampleTests(); + await _runFlutterTest( + path.join(flutterRoot, 'dev', 'a11y_assessments'), + tests: [ 'test' ], + ); + await _runDartTest(path.join(flutterRoot, 'dev', 'bots')); + await _runDartTest(path.join(flutterRoot, 'dev', 'devicelab'), ensurePrecompiledTool: false); // See https://github.com/flutter/flutter/issues/86209 + await _runDartTest(path.join(flutterRoot, 'dev', 'conductor', 'core'), forceSingleCore: true); // TODO(gspencergoog): Remove the exception for fatalWarnings once https://github.com/flutter/flutter/issues/113782 has landed. await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'), fatalWarnings: false); await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'ui')); @@ -993,25 +1001,39 @@ Future _runFrameworkTests() async { await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'gen_keycodes')); await _runFlutterTest(path.join(flutterRoot, 'dev', 'benchmarks', 'test_apps', 'stocks')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver'), tests: [path.join('test', 'src', 'real_tests')]); - // await _runFlutterTest(path.join(flutterRoot, 'packages', 'integration_test'), options: [ - // '--enable-vmservice', - // // Web-specific tests depend on Chromium, so they run as part of the web_long_running_tests shard. - // '--exclude-tags=web', - // ]); - // await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_goldens')); + await _runFlutterTest(path.join(flutterRoot, 'packages', 'integration_test'), options: [ + '--enable-vmservice', + // Web-specific tests depend on Chromium, so they run as part of the web_long_running_tests shard. + '--exclude-tags=web', + ]); + // Run java unit tests for integration_test + // + // Generate Gradle wrapper if it doesn't exist. + Process.runSync( + flutter, + ['build', 'apk', '--config-only'], + workingDirectory: path.join(flutterRoot, 'packages', 'integration_test', 'example', 'android'), + ); + await runCommand( + path.join(flutterRoot, 'packages', 'integration_test', 'example', 'android', 'gradlew$bat'), + [ + ':integration_test:testDebugUnitTest', + '--tests', + 'dev.flutter.plugins.integration_test.FlutterDeviceScreenshotTest', + ], + workingDirectory: path.join(flutterRoot, 'packages', 'integration_test', 'example', 'android'), + ); + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_goldens')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_localizations')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test')); await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol')); await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable')); const String httpClientWarning = - 'Warning: At least one test in this suite creates an HttpClient. When\n' - 'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n' - 'requests will return status code 400, and no network request will\n' - 'actually be made. Any test expecting a real network connection and\n' - 'status code will fail.\n' - 'To test code that needs an HttpClient, provide your own HttpClient\n' - 'implementation to the code under test, so that your test can\n' - 'consistently provide a testable response to the code under test.'; + 'Warning: At least one test in this suite creates an HttpClient. When running a test suite that uses\n' + 'TestWidgetsFlutterBinding, all HTTP requests will return status code 400, and no network request\n' + 'will actually be made. Any test expecting a real network connection and status code will fail.\n' + 'To test code that needs an HttpClient, provide your own HttpClient implementation to the code under\n' + 'test, so that your test can consistently provide a testable response to the code under test.'; await _runFlutterTest( path.join(flutterRoot, 'packages', 'flutter_test'), script: path.join('test', 'bindings_test_failure.dart'), @@ -1139,6 +1161,10 @@ Future _runWebUnitTests(String webRenderer) async { /// Coarse-grained integration tests running on the Web. Future _runWebLongRunningTests() async { final String engineVersion = File(engineVersionFile).readAsStringSync().trim(); + final String engineRealm = File(engineRealmFile).readAsStringSync().trim(); + if (engineRealm.isNotEmpty) { + return; + } final List tests = [ for (final String buildMode in _kAllBuildModes) ...[ () => _runFlutterDriverWebTest( @@ -1225,6 +1251,7 @@ Future _runWebLongRunningTests() async { () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn), () => runWebServiceWorkerTestWithGeneratedEntrypoint(headless: true), () => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true), + () => runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: true), () => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'), () => _runWebStackTraceTest('release', 'lib/stack_trace.dart'), () => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'), @@ -1363,7 +1390,7 @@ Future _runWebTreeshakeTest() async { final String javaScript = mainDartJs.readAsStringSync(); // Check that we're not looking at minified JS. Otherwise this test would result in false positive. - expect(javaScript.contains('RenderObjectToWidgetElement'), true); + expect(javaScript.contains('RootElement'), true); const String word = 'debugFillProperties'; int count = 0; @@ -1462,6 +1489,13 @@ Future _runFlutterPackagesTests() async { 'run', toolScript, 'analyze', + // Fetch the oldest possible dependencies, rather than the newest, to + // insulate flutter/flutter from out-of-band failures when new versions + // of dependencies are published. This compensates for the fact that + // flutter/packages doesn't use pinned dependencies, and for the + // purposes of this test using old dependencies is fine. See + // https://github.com/flutter/flutter/issues/129633 + '--downgrade', '--custom-analysis=script/configs/custom_analysis.yaml', ], workingDirectory: checkout.path, diff --git a/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart b/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart index 85c41d9deb375..30458dd8871a7 100644 --- a/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart +++ b/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart @@ -11,7 +11,7 @@ library dart.ui; /// /// ```dart /// class MyStringBuffer { -/// error; // error (missing_const_final_var_or_type, always_specify_types) +/// error; // error (prefer_typing_uninitialized_variables, inference_failure_on_uninitialized_variable, missing_const_final_var_or_type) /// /// StringBuffer _buffer = StringBuffer(); // error (prefer_final_fields, unused_field) /// } diff --git a/dev/bots/test/analyze-snippet-code-test-input/custom_imports_broken.dart b/dev/bots/test/analyze-snippet-code-test-input/custom_imports_broken.dart new file mode 100644 index 0000000000000..46dc91139bff2 --- /dev/null +++ b/dev/bots/test/analyze-snippet-code-test-input/custom_imports_broken.dart @@ -0,0 +1,21 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is used by ../analyze_snippet_code_test.dart, which depends on the +// precise contents (including especially the comments) of this file. + +// Examples can assume: +// import 'package:flutter/rendering.dart'; + +/// no error: rendering library was imported. +/// ```dart +/// print(RenderObject); +/// ``` +String? bar; + +/// error: widgets library was not imported (not even implicitly). +/// ```dart +/// print(Widget); // error (undefined_identifier) +/// ``` +String? foo; diff --git a/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart b/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart index a822e88ec0a30..2704029efcb5f 100644 --- a/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart +++ b/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart @@ -131,7 +131,7 @@ /// Widget build(BuildContext context) { /// final String title; /// return Opacity( -/// key: globalKey, // error (undefined_identifier, argument_type_not_assignable) +/// key: globalKey, // error (undefined_identifier) /// opacity: _visible ? 1.0 : 0.0, /// child: Text(title), // error (read_potentially_unassigned_final) /// ); @@ -144,13 +144,14 @@ /// ``` /// /// ```dart -/// import 'dart:io'; // error (unused_import)/// final Widget p = Placeholder(); // error (undefined_class, undefined_function) +/// import 'dart:io'; // error (unused_import) +/// final Widget p = Placeholder(); // error (undefined_class, undefined_function) /// ``` /// /// ```dart /// // (e.g. in a stateful widget) /// void initState() { // error (must_call_super, annotate_overrides) -/// widget.toString(); // error (undefined_identifier, return_of_invalid_type) +/// widget.toString(); /// } /// ``` /// diff --git a/dev/bots/test/analyze_snippet_code_test.dart b/dev/bots/test/analyze_snippet_code_test.dart index 712052faf6eca..45ff34d405e2b 100644 --- a/dev/bots/test/analyze_snippet_code_test.dart +++ b/dev/bots/test/analyze_snippet_code_test.dart @@ -11,6 +11,7 @@ import 'dart:io'; import 'common.dart'; const List expectedMainErrors = [ + 'dev/bots/test/analyze-snippet-code-test-input/custom_imports_broken.dart:19:11: (statement) (undefined_identifier)', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:30:5: (expression) (unnecessary_new)', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:103:5: (statement) (always_specify_types)', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:111:5: (top-level declaration) (prefer_const_declarations)', @@ -20,16 +21,19 @@ const List expectedMainErrors = [ 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:134:14: (top-level declaration) (undefined_identifier)', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:136:21: (top-level declaration) (read_potentially_unassigned_final)', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:147:12: (self-contained program) (unused_import)', - 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:152:10: (stateful widget) (annotate_overrides)', - 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:152:10: (stateful widget) (must_call_super)', - 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:160:7: (top-level declaration) (undefined_identifier)', - 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:164: Found "```" in code but it did not match RegExp: pattern=^ */// *```dart\$ flags= so something is wrong. Line was: "/// ```"', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:148:11: (self-contained program) (undefined_class)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:148:22: (self-contained program) (undefined_function)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:153:10: (stateful widget) (annotate_overrides)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:153:10: (stateful widget) (must_call_super)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:161:7: (top-level declaration) (undefined_identifier)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:165: Found "```" in code but it did not match RegExp: pattern=^ */// *```dart\$ flags= so something is wrong. Line was: "/// ```"', 'dev/bots/test/analyze-snippet-code-test-input/short_but_still_broken.dart:9:12: (statement) (invalid_assignment)', 'dev/bots/test/analyze-snippet-code-test-input/short_but_still_broken.dart:18:4: Empty ```dart block in snippet code.', ]; const List expectedUiErrors = [ 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:14:7: (top-level declaration) (prefer_typing_uninitialized_variables)', + 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:14:7: (top-level declaration) (inference_failure_on_uninitialized_variable)', 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:14:7: (top-level declaration) (missing_const_final_var_or_type)', 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:16:20: (top-level declaration) (prefer_final_fields)', 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:16:20: (top-level declaration) (unused_field)', @@ -68,7 +72,7 @@ void main() { final List stderrNoDescriptions = stderrLines.map(removeLintDescriptions).toList(); expect(stderrNoDescriptions, [ ...expectedMainErrors, - 'Found 15 snippet code errors.', + 'Found 18 snippet code errors.', 'See the documentation at the top of dev/bots/analyze_snippet_code.dart for details.', '', // because we end with a newline, split gives us an extra blank line ]); @@ -92,7 +96,7 @@ void main() { expect(stderrNoDescriptions, [ ...expectedUiErrors, ...expectedMainErrors, - 'Found 19 snippet code errors.', + 'Found 23 snippet code errors.', 'See the documentation at the top of dev/bots/analyze_snippet_code.dart for details.', '', // because we end with a newline, split gives us an extra blank line ]); diff --git a/dev/bots/test/check_code_samples_test.dart b/dev/bots/test/check_code_samples_test.dart new file mode 100644 index 0000000000000..2fcae906f37fa --- /dev/null +++ b/dev/bots/test/check_code_samples_test.dart @@ -0,0 +1,190 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:path/path.dart' as path; + +import '../check_code_samples.dart'; +import '../utils.dart'; +import 'common.dart'; + +void main() { + late SampleChecker checker; + late FileSystem fs; + late Directory examples; + late Directory packages; + late Directory dartUIPath; + late Directory flutterRoot; + + String getRelativePath(File file, [Directory? from]) { + from ??= flutterRoot; + return path.relative(file.absolute.path, from: flutterRoot.absolute.path); + } + + void writeLink({required File source, required File example}) { + source + ..createSync(recursive: true) + ..writeAsStringSync(''' +/// Class documentation +/// +/// {@tool dartpad} +/// Example description +/// +/// ** See code in ${getRelativePath(example)} ** +/// {@end-tool} +'''); + } + + void buildTestFiles({bool missingLinks = false, bool missingTests = false}) { + final Directory examplesLib = examples.childDirectory('lib').childDirectory('layer')..createSync(recursive: true); + final File fooExample = examplesLib.childFile('foo_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example for foo'); + final File barExample = examplesLib.childFile('bar_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example for bar'); + final File curvesExample = + examples.childDirectory('lib').childDirectory('animation').childDirectory('curves').childFile('curve2_d.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Missing a test, but OK'); + if (missingLinks) { + examplesLib.childFile('missing_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example that is not linked'); + } + final Directory examplesTests = examples.childDirectory('test').childDirectory('layer') + ..createSync(recursive: true); + examplesTests.childFile('foo_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for foo example'); + if (!missingTests) { + examplesTests.childFile('bar_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for bar example'); + } + if (missingLinks) { + examplesTests.childFile('missing_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for foo example'); + } + final Directory flutterPackage = packages.childDirectory('flutter').childDirectory('lib').childDirectory('src') + ..createSync(recursive: true); + writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample); + writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample); + writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample); + } + + setUp(() { + fs = MemoryFileSystem(style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix); + // Get the root prefix of the current directory so that on Windows we get a + // correct root prefix. + flutterRoot = fs.directory(path.join(path.rootPrefix(fs.currentDirectory.absolute.path), 'flutter sdk'))..createSync(recursive: true); + fs.currentDirectory = flutterRoot; + examples = flutterRoot.childDirectory('examples').childDirectory('api')..createSync(recursive: true); + packages = flutterRoot.childDirectory('packages')..createSync(recursive: true); + dartUIPath = flutterRoot + .childDirectory('bin') + .childDirectory('cache') + .childDirectory('pkg') + .childDirectory('sky_engine') + .childDirectory('lib') + ..createSync(recursive: true); + checker = SampleChecker( + examples: examples, + packages: packages, + dartUIPath: dartUIPath, + flutterRoot: flutterRoot, + filesystem: fs, + ); + }); + + test('check_code_samples.dart - checkCodeSamples catches missing links', () async { + buildTestFiles(missingLinks: true); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + shouldHaveErrors: true, + ); + final String lines = [ + '╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════', + '║ The following examples are not linked from any source file API doc comments:', + '║ examples/api/lib/layer/missing_example.0.dart', + '║ Either link them to a source file API doc comment, or remove them.', + '╚═══════════════════════════════════════════════════════════════════════════════', + ].map((String line) { + return line.replaceAll('/', Platform.isWindows ? r'\' : '/'); + }).join('\n'); + expect(result, equals('$lines\n')); + expect(success, equals(false)); + }); + + test('check_code_samples.dart - checkCodeSamples catches missing tests', () async { + buildTestFiles(missingTests: true); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + shouldHaveErrors: true, + ); + final String lines = [ + '╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════', + '║ The following example test files are missing:', + '║ examples/api/test/layer/bar_example.0_test.dart', + '╚═══════════════════════════════════════════════════════════════════════════════', + ].map((String line) { + return line.replaceAll('/', Platform.isWindows ? r'\' : '/'); + }).join('\n'); + expect(result, equals('$lines\n')); + expect(success, equals(false)); + }); + + test('check_code_samples.dart - checkCodeSamples succeeds', () async { + buildTestFiles(); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + ); + expect(result, isEmpty); + expect(success, equals(true)); + }); +} + +typedef AsyncVoidCallback = Future Function(); + +Future capture(AsyncVoidCallback callback, {bool shouldHaveErrors = false}) async { + final StringBuffer buffer = StringBuffer(); + final PrintCallback oldPrint = print; + try { + print = (Object? line) { + buffer.writeln(line); + }; + await callback(); + expect( + hasError, + shouldHaveErrors, + reason: buffer.isEmpty + ? '(No output to report.)' + : hasError + ? 'Unexpected errors:\n$buffer' + : 'Unexpected success:\n$buffer', + ); + } finally { + print = oldPrint; + resetErrorStatus(); + } + if (stdout.supportsAnsiEscapes) { + // Remove ANSI escapes when this test is running on a terminal. + return buffer.toString().replaceAll(RegExp(r'(\x9B|\x1B\[)[0-?]{1,3}[ -/]*[@-~]'), ''); + } else { + return buffer.toString(); + } +} diff --git a/dev/bots/test/prepare_package_test.dart b/dev/bots/test/prepare_package_test.dart index e8e5cac08f5c1..10ba341cc3078 100644 --- a/dev/bots/test/prepare_package_test.dart +++ b/dev/bots/test/prepare_package_test.dart @@ -38,7 +38,7 @@ void main() { final FakePlatform platform = FakePlatform( operatingSystem: platformName, environment: { - 'DEPOT_TOOLS': path.join('D:', 'depot_tools'), + 'DEPOT_TOOLS': platformName == Platform.windows ? path.join('D:', 'depot_tools'): '/depot_tools', }, ); group('ProcessRunner for $platform', () { @@ -378,7 +378,7 @@ void main() { late Directory tempDir; final String gsutilCall = platform.isWindows ? 'python3 ${path.join("D:", "depot_tools", "gsutil.py")}' - : 'gsutil.py'; + : 'python3 ${path.join("/", "depot_tools", "gsutil.py")}'; final String releasesName = 'releases_$platformName.json'; final String archiveName = platform.isLinux ? 'archive.tar.xz' : 'archive.zip'; final String archiveMime = platform.isLinux ? 'application/x-gtar' : 'application/zip'; diff --git a/dev/ci/docker_linux/Dockerfile b/dev/ci/docker_linux/Dockerfile index db58a1a3a6261..caec75d6535b2 100644 --- a/dev/ci/docker_linux/Dockerfile +++ b/dev/ci/docker_linux/Dockerfile @@ -12,7 +12,7 @@ # Last manual update 2021-09-24 (changing this comment will re-build image) -FROM ubuntu:focal@sha256:f8f658407c35733471596f25fdb4ed748b80e545ab57e84efbdb1dbbb01bd70e +FROM ubuntu:focal@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba MAINTAINER Flutter Developers ENV TZ=America/Los_Angeles @@ -48,14 +48,9 @@ RUN apt-get update && apt-get install -y google-cloud-sdk && \ gcloud config set core/disable_usage_reporting true && \ gcloud config set component_manager/disable_update_check true -# Add repo for OpenJDK from JFrog.io -RUN wget -q -O - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - -RUN echo 'deb [arch=amd64] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb bullseye main' | \ - tee /etc/apt/sources.list.d/adoptopenjdk.list - # Install the dependencies needed for the rest of the build. RUN apt-get update && apt-get install -y --no-install-recommends \ - adoptopenjdk-11-hotspot \ + openjdk-17-jdk \ build-essential \ default-jdk-headless \ gcc \ @@ -70,7 +65,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ruby-dev && \ apt-get clean -ENV JAVA_HOME="/usr/lib/jvm/adoptopenjdk-11-hotspot-amd64" +ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" # Install the Android SDK Dependency. # In the event of an update you can visit this page: https://developer.android.com/studio and scroll to the bottom to find diff --git a/dev/ci/mac/Gemfile.lock b/dev/ci/mac/Gemfile.lock index dd07f525d1544..c37fe1f100f62 100644 --- a/dev/ci/mac/Gemfile.lock +++ b/dev/ci/mac/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.7.3) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -63,10 +63,10 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.1) - minitest (5.18.0) + minitest (5.19.0) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) @@ -85,7 +85,7 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.6.7) + zeitwerk (2.6.11) PLATFORMS ruby diff --git a/dev/conductor/core/bin/packages_autoroller.dart b/dev/conductor/core/bin/packages_autoroller.dart index 6cb35b2cf8712..a890927f0a6f6 100644 --- a/dev/conductor/core/bin/packages_autoroller.dart +++ b/dev/conductor/core/bin/packages_autoroller.dart @@ -80,7 +80,7 @@ ${parser.usage} } final FrameworkRepository framework = FrameworkRepository( - _localCheckouts, + _localCheckouts(token), mirrorRemote: Remote.mirror(mirrorUrl), upstreamRemote: Remote.upstream(upstreamUrl), ); @@ -106,7 +106,7 @@ String _parseOrgName(String remoteUrl) { return match.group(1)!; } -Checkouts get _localCheckouts { +Checkouts _localCheckouts(String token) { const FileSystem fileSystem = LocalFileSystem(); const ProcessManager processManager = LocalProcessManager(); const Platform platform = LocalPlatform(); @@ -114,6 +114,7 @@ Checkouts get _localCheckouts { stdout: io.stdout, stderr: io.stderr, stdin: io.stdin, + filter: (String message) => message.replaceAll(token, '[GitHub TOKEN]'), ); return Checkouts( fileSystem: fileSystem, diff --git a/dev/conductor/core/lib/src/codesign.dart b/dev/conductor/core/lib/src/codesign.dart index c5cf2038a6c6c..2e45cd0f6d574 100644 --- a/dev/conductor/core/lib/src/codesign.dart +++ b/dev/conductor/core/lib/src/codesign.dart @@ -125,7 +125,15 @@ class CodesignCommand extends Command { await framework.checkout(revision); // Ensure artifacts present - await framework.runFlutter(['precache', '--android', '--ios', '--macos']); + final io.ProcessResult result = await framework.runFlutter( + ['precache', '--android', '--ios', '--macos'], + ); + if (result.exitCode != 0) { + stdio.printError( + 'flutter precache: exitCode: ${result.exitCode}\n' + 'stdout:\n${result.stdout}\nstderr:\n${result.stderr}', + ); + } await verifyExist(); if (argResults![kSignatures] as bool) { @@ -193,10 +201,16 @@ class CodesignCommand extends Command { 'artifacts/engine/darwin-x64/libtessellator.dylib', 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', 'artifacts/engine/ios-profile/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', 'artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios-release/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', 'artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', 'artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'artifacts/engine/ios/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', 'artifacts/ios-deploy/ios-deploy', ] .map((String relativePath) => diff --git a/dev/conductor/core/lib/src/stdio.dart b/dev/conductor/core/lib/src/stdio.dart index 8777b91f40587..df8f81d056459 100644 --- a/dev/conductor/core/lib/src/stdio.dart +++ b/dev/conductor/core/lib/src/stdio.dart @@ -70,6 +70,7 @@ class VerboseStdio extends Stdio { required this.stdout, required this.stderr, required this.stdin, + this.filter, }); factory VerboseStdio.local() => VerboseStdio( @@ -82,26 +83,50 @@ class VerboseStdio extends Stdio { final io.Stdout stderr; final io.Stdin stdin; + /// If provided, all messages will be passed through this function before being logged. + final String Function(String)? filter; + @override void printError(String message) { + if (filter != null) { + message = filter!(message); + } super.printError(message); stderr.writeln(message); } + @override + void printWarning(String message) { + if (filter != null) { + message = filter!(message); + } + super.printWarning(message); + stderr.writeln(message); + } + @override void printStatus(String message) { + if (filter != null) { + message = filter!(message); + } super.printStatus(message); stdout.writeln(message); } @override void printTrace(String message) { + if (filter != null) { + message = filter!(message); + } super.printTrace(message); stdout.writeln(message); } @override void write(String message) { + if (filter != null) { + message = filter!(message); + } super.write(message); stdout.write(message); } diff --git a/dev/conductor/core/pubspec.yaml b/dev/conductor/core/pubspec.yaml index e650875668300..0822a15a429da 100644 --- a/dev/conductor/core/pubspec.yaml +++ b/dev/conductor/core/pubspec.yaml @@ -4,37 +4,37 @@ description: Flutter Automated Release Tool publish_to: none environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: archive: 3.3.2 args: 2.4.2 http: 0.13.6 intl: 0.18.1 - meta: 1.9.1 + meta: 1.10.0 path: 1.8.3 process: 4.2.4 - protobuf: 3.0.0 + protobuf: 3.1.0 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fixnum: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 - test_api: 0.6.0 + test: 1.24.6 + test_api: 0.6.1 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -56,13 +56,13 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 282c +# PUBSPEC CHECKSUM: 815a diff --git a/dev/conductor/core/test/common.dart b/dev/conductor/core/test/common.dart index 9cf66ffdfe365..f1a2f3999924f 100644 --- a/dev/conductor/core/test/common.dart +++ b/dev/conductor/core/test/common.dart @@ -6,6 +6,7 @@ import 'package:args/args.dart'; import 'package:conductor_core/src/stdio.dart'; import 'package:test/test.dart'; +export 'package:test/fake.dart'; export 'package:test/test.dart' hide isInstanceOf; export '../../../../packages/flutter_tools/test/src/fake_process_manager.dart'; diff --git a/dev/conductor/core/test/next_test.dart b/dev/conductor/core/test/next_test.dart index f124aa2417895..dee3a2e567b41 100644 --- a/dev/conductor/core/test/next_test.dart +++ b/dev/conductor/core/test/next_test.dart @@ -1227,34 +1227,10 @@ void main() { } /// A [Stdio] that will throw an exception if any of its methods are called. -class _UnimplementedStdio implements Stdio { - const _UnimplementedStdio(); +class _UnimplementedStdio extends Fake implements Stdio { + _UnimplementedStdio(); - static const _UnimplementedStdio _instance = _UnimplementedStdio(); - static _UnimplementedStdio get instance => _instance; - - Never _throw() => throw Exception('Unimplemented!'); - - @override - List get logs => _throw(); - - @override - void printError(String message) => _throw(); - - @override - void printWarning(String message) => _throw(); - - @override - void printStatus(String message) => _throw(); - - @override - void printTrace(String message) => _throw(); - - @override - void write(String message) => _throw(); - - @override - String readLineSync() => _throw(); + static final _UnimplementedStdio instance = _UnimplementedStdio(); } class _TestRepository extends Repository { diff --git a/dev/conductor/core/test/packages_autoroller_test.dart b/dev/conductor/core/test/packages_autoroller_test.dart index 50e21a1aee56f..d320f731a491d 100644 --- a/dev/conductor/core/test/packages_autoroller_test.dart +++ b/dev/conductor/core/test/packages_autoroller_test.dart @@ -11,8 +11,8 @@ import 'package:conductor_core/packages_autoroller.dart'; import 'package:file/memory.dart'; import 'package:platform/platform.dart'; -import './common.dart'; import '../bin/packages_autoroller.dart' show run; +import 'common.dart'; void main() { const String flutterRoot = '/flutter'; @@ -170,7 +170,7 @@ void main() { await expectLater( () async { final Future rollFuture = autoroller.roll(); - await controller.stream.drain(); + await controller.stream.drain(); await rollFuture; }, throwsA(isA().having( @@ -214,7 +214,7 @@ void main() { ], stdout: '[{"number": 123}]'), ]); final Future rollFuture = autoroller.roll(); - await controller.stream.drain(); + await controller.stream.drain(); await rollFuture; expect(processManager, hasNoRemainingExpectations); expect(stdio.stdout, contains('flutter-pub-roller-bot already has open tool PRs')); @@ -312,7 +312,7 @@ void main() { ]), ]); final Future rollFuture = autoroller.roll(); - await controller.stream.drain(); + await controller.stream.drain(); await rollFuture; expect(processManager, hasNoRemainingExpectations); }); @@ -512,4 +512,35 @@ void main() { expect(processManager, hasNoRemainingExpectations); }); }); + + test('VerboseStdio logger can filter out confidential pattern', () async { + const String token = 'secret'; + const String replacement = 'replacement'; + final VerboseStdio stdio = VerboseStdio( + stdin: _NoOpStdin(), + stderr: _NoOpStdout(), + stdout: _NoOpStdout(), + filter: (String msg) => msg.replaceAll(token, replacement), + ); + stdio.printStatus('Hello'); + expect(stdio.logs.last, '[status] Hello'); + + stdio.printStatus('Using $token'); + expect(stdio.logs.last, '[status] Using $replacement'); + + stdio.printWarning('Using $token'); + expect(stdio.logs.last, '[warning] Using $replacement'); + + stdio.printError('Using $token'); + expect(stdio.logs.last, '[error] Using $replacement'); + + stdio.printTrace('Using $token'); + expect(stdio.logs.last, '[trace] Using $replacement'); + }); +} + +class _NoOpStdin extends Fake implements io.Stdin {} +class _NoOpStdout extends Fake implements io.Stdout { + @override + void writeln([Object? object]) {} } diff --git a/dev/customer_testing/pubspec.yaml b/dev/customer_testing/pubspec.yaml index 9f5cb6daa43af..62f474d53c273 100644 --- a/dev/customer_testing/pubspec.yaml +++ b/dev/customer_testing/pubspec.yaml @@ -2,26 +2,26 @@ name: customer_testing description: Tool to run the tests listed in the flutter/tests repository. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: args: 2.4.2 path: 1.8.3 glob: 2.1.2 - meta: 1.9.1 + meta: 1.10.0 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -44,15 +44,15 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: df79 +# PUBSPEC CHECKSUM: 80a4 diff --git a/dev/devicelab/README.md b/dev/devicelab/README.md index 39cd020fe509e..69ba2d61ea168 100644 --- a/dev/devicelab/README.md +++ b/dev/devicelab/README.md @@ -19,6 +19,7 @@ for information on using the dashboards. * [Adding tests to continuous integration](#adding-tests-to-continuous-integration) * [Adding tests to presubmit](#adding-tests-to-presubmit) +* [Migrating to build and test model](#migrating-to-build-and-test-model) ## How the DeviceLab runs tests @@ -90,10 +91,12 @@ flags to `bin/run.dart`: ```sh ../../bin/cache/dart-sdk/bin/dart bin/run.dart --task=[some_task] \ --local-engine-src-path=[path_to_local]/engine/src \ - --local-engine=[local_engine_architecture] + --local-engine=[local_engine_architecture] \ + --local-engine-host=[local_engine_host_architecture] ``` -An example of a local engine architecture is `android_debug_unopt_x86`. +An example of a local engine architecture is `android_debug_unopt_x86` and +an example of a local engine host architecture is `host_debug_unopt`. ### Running an A/B test for engine changes @@ -110,13 +113,16 @@ Example: ```sh ../../bin/cache/dart-sdk/bin/dart bin/run.dart --ab=10 \ --local-engine=host_debug_unopt \ + --local-engine-host=host_debug_unopt \ -t bin/tasks/web_benchmarks_canvaskit.dart ``` The `--ab=10` tells the runner to run an A/B test 10 times. `--local-engine=host_debug_unopt` tells the A/B test to use the -`host_debug_unopt` engine build. `--local-engine` is required for A/B test. +`host_debug_unopt` engine build. `--local-engine-host=host_debug_unopt` uses +the same engine build to run the `frontend_server` (in this example). +`--local-engine` is required for A/B test. `--ab-result-file=filename` can be used to provide an alternate location to output the JSON results file (defaults to `ABresults#.json`). A single `#` @@ -226,3 +232,33 @@ target for each operating system. Flutter's DeviceLab has a limited capacity in presubmit. File an infra ticket to investigate feasibility of adding a test to presubmit. + +## Migrating to build and test model + +To better utilize limited DeviceLab testbed resources and speed up commit validation +time, it is now supported to separate building artifacts (.apk/.app) from testing them. +The artifact will be built on a host only bot, a VM or physical bot without a device, +and the test will run based on the artifact against a testbed with a device. + +Steps: + +1. Update the task class to extend [`BuildTestTask`](https://github.com/flutter/flutter/blob/master/dev/devicelab/lib/tasks/build_test_task.dart) + - Override function `getBuildArgs` + - Override function `getTestArgs` + - Override function `parseTaskResult` + - Override function `getApplicationBinaryPath` +2. Update the `bin/tasks/{TEST}.dart` to point to the new task class +3. Validate the task locally + - build only: `dart bin/test_runner.dart test -t {NAME_OR_PATH_OF_TEST} --task-args build --task-args application-binary-path={PATH_TO_ARTIFACT}` + - test only: `dart bin/test_runner.dart test -t {NAME_OR_PATH_OF_TEST} --task-args test --task-args application-binary-path={PATH_TO_ARTIFACT}` +4. Add tasks to continuous integration + - Mirror a target with platform `Linux_build_test` or `Mac_build_test` + - The only difference from regular targets is the artifact property: if omitted, it will use the `task_name`. +5. Once validated in CI, enable the target in `PROD` by removing `bringup: true` and deleting the old target entry without build+test model. + +Take gallery tasks for example: + +1. Linux android + - Separating PR: https://github.com/flutter/flutter/pull/103550 + - Switching PR: https://github.com/flutter/flutter/pull/110533 +2. Mac iOS: https://github.com/flutter/flutter/pull/111164 \ No newline at end of file diff --git a/dev/devicelab/bin/run.dart b/dev/devicelab/bin/run.dart index cc0cb1bb29fbf..e90ffe36bb062 100644 --- a/dev/devicelab/bin/run.dart +++ b/dev/devicelab/bin/run.dart @@ -38,6 +38,11 @@ Future main(List rawArgs) async { /// Required for A/B test mode. final String? localEngine = args['local-engine'] as String?; + /// The build of the local engine to use as the host platform. + /// + /// Required if [localEngine] is set. + final String? localEngineHost = args['local-engine-host'] as String?; + /// The build of the local Web SDK to use. /// /// Required for A/B test mode. @@ -95,10 +100,16 @@ Future main(List rawArgs) async { stderr.writeln(argParser.usage); exit(1); } + if (localEngineHost == null) { + stderr.writeln('When running in A/B test mode --local-engine-host is required.\n'); + stderr.writeln(argParser.usage); + exit(1); + } await _runABTest( runsPerTest: runsPerTest, silent: silent, localEngine: localEngine, + localEngineHost: localEngineHost, localWebSdk: localWebSdk, localEngineSrcPath: localEngineSrcPath, deviceId: deviceId, @@ -109,6 +120,7 @@ Future main(List rawArgs) async { await runTasks(taskNames, silent: silent, localEngine: localEngine, + localEngineHost: localEngineHost, localEngineSrcPath: localEngineSrcPath, deviceId: deviceId, exitOnFirstTestFailure: exitOnFirstTestFailure, @@ -125,6 +137,7 @@ Future _runABTest({ required int runsPerTest, required bool silent, required String? localEngine, + required String localEngineHost, required String? localWebSdk, required String? localEngineSrcPath, required String? deviceId, @@ -135,7 +148,11 @@ Future _runABTest({ assert(localEngine != null || localWebSdk != null); - final ABTest abTest = ABTest((localEngine ?? localWebSdk)!, taskName); + final ABTest abTest = ABTest( + localEngine: (localEngine ?? localWebSdk)!, + localEngineHost: localEngineHost, + taskName: taskName, + ); for (int i = 1; i <= runsPerTest; i++) { section('Run #$i'); @@ -161,6 +178,7 @@ Future _runABTest({ taskName, silent: silent, localEngine: localEngine, + localEngineHost: localEngineHost, localWebSdk: localWebSdk, localEngineSrcPath: localEngineSrcPath, deviceId: deviceId, @@ -275,7 +293,6 @@ ArgParser createArgParser(List taskNames) { ) ..addFlag( 'exit', - defaultsTo: true, help: 'Exit on the first test failure. Currently flakes are intentionally (though ' 'incorrectly) not considered to be failures.', ) @@ -292,6 +309,15 @@ ArgParser createArgParser(List taskNames) { 'This path is relative to --local-engine-src-path/out. This option\n' 'is required when running an A/B test (see the --ab option).', ) + ..addOption( + 'local-engine-host', + help: 'Name of a build output within the engine out directory, if you\n' + 'are building Flutter locally. Use this to select a specific\n' + 'version of the engine to use as the host platform if you have built ' + 'multiple engine targets.\n' + 'This path is relative to --local-engine-src-path/out. This option\n' + 'is required when running an A/B test (see the --ab option).', + ) ..addOption( 'local-web-sdk', help: 'Name of a build output within the engine out directory, if you\n' diff --git a/dev/devicelab/bin/tasks/animated_advanced_blend_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/animated_advanced_blend_perf__timeline_summary.dart new file mode 100644 index 0000000000000..9bca1dc045592 --- /dev/null +++ b/dev/devicelab/bin/tasks/animated_advanced_blend_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createAnimatedAdvancedBlendPerfTest(enableImpeller: true)); +} diff --git a/dev/devicelab/bin/tasks/animated_advanced_blend_perf_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/animated_advanced_blend_perf_ios__timeline_summary.dart new file mode 100644 index 0000000000000..5d09c1787022f --- /dev/null +++ b/dev/devicelab/bin/tasks/animated_advanced_blend_perf_ios__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createAnimatedAdvancedBlendPerfTest(enableImpeller: true)); +} diff --git a/dev/devicelab/bin/tasks/animated_advanced_blend_perf_opengles__timeline_summary.dart b/dev/devicelab/bin/tasks/animated_advanced_blend_perf_opengles__timeline_summary.dart new file mode 100644 index 0000000000000..113a496ac5777 --- /dev/null +++ b/dev/devicelab/bin/tasks/animated_advanced_blend_perf_opengles__timeline_summary.dart @@ -0,0 +1,15 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createAnimatedAdvancedBlendPerfTest( + enableImpeller: true, forceOpenGLES: true)); +} diff --git a/dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf_opengles__timeline_summary.dart b/dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf_opengles__timeline_summary.dart new file mode 100644 index 0000000000000..83e4c6d0546dd --- /dev/null +++ b/dev/devicelab/bin/tasks/animated_blur_backdrop_filter_perf_opengles__timeline_summary.dart @@ -0,0 +1,15 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createAnimatedBlurBackropFilterPerfTest( + enableImpeller: true, forceOpenGLES: true)); +} diff --git a/dev/devicelab/bin/tasks/draw_atlas_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_atlas_perf__timeline_summary.dart new file mode 100644 index 0000000000000..9386d64d3b8bd --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_atlas_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createDrawAtlasPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/draw_atlas_perf_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_atlas_perf_ios__timeline_summary.dart new file mode 100644 index 0000000000000..374e5105a8341 --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_atlas_perf_ios__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createDrawAtlasPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/draw_atlas_perf_opengles__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_atlas_perf_opengles__timeline_summary.dart new file mode 100644 index 0000000000000..bbdb935fd6a99 --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_atlas_perf_opengles__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createDrawAtlasPerfTest(forceOpenGLES: true)); +} diff --git a/dev/devicelab/bin/tasks/draw_vertices_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_vertices_perf__timeline_summary.dart new file mode 100644 index 0000000000000..635e4296d285b --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_vertices_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createDrawVerticesPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/draw_vertices_perf_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_vertices_perf_ios__timeline_summary.dart new file mode 100644 index 0000000000000..96c02f1409a77 --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_vertices_perf_ios__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createDrawVerticesPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/draw_vertices_perf_opengles__timeline_summary.dart b/dev/devicelab/bin/tasks/draw_vertices_perf_opengles__timeline_summary.dart new file mode 100644 index 0000000000000..d1163ce2baba6 --- /dev/null +++ b/dev/devicelab/bin/tasks/draw_vertices_perf_opengles__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createDrawVerticesPerfTest(forceOpenGLES: true)); +} diff --git a/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf__timeline_summary.dart new file mode 100644 index 0000000000000..47ae2d1ff2817 --- /dev/null +++ b/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createPathTessellationDynamicPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf_ios__timeline_summary.dart new file mode 100644 index 0000000000000..79509a3b36d2b --- /dev/null +++ b/dev/devicelab/bin/tasks/dynamic_path_tessellation_perf_ios__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createPathTessellationDynamicPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/engine_dependency_proxy_test.dart b/dev/devicelab/bin/tasks/engine_dependency_proxy_test.dart index 43cad17440688..9443a29bb257a 100644 --- a/dev/devicelab/bin/tasks/engine_dependency_proxy_test.dart +++ b/dev/devicelab/bin/tasks/engine_dependency_proxy_test.dart @@ -30,7 +30,8 @@ Future main() async { await inDirectory(path.join(flutterProject.rootPath, 'android'), () async { section('Insert gradle testing script'); final File build = File(path.join( - flutterProject.rootPath, 'android', 'app', 'build.gradle')); + flutterProject.rootPath, 'android', 'app', 'build.gradle', + )); build.writeAsStringSync( ''' task printEngineMavenUrl() { @@ -44,6 +45,7 @@ task printEngineMavenUrl() { ); section('Checking default maven URL'); + String gradleOutput = await eval( gradlewExecutable, ['printEngineMavenUrl', '-q'], @@ -53,29 +55,39 @@ task printEngineMavenUrl() { String mavenUrl = outputLines.last; print('Returned maven url: $mavenUrl'); - if (mavenUrl != 'https://storage.googleapis.com/download.flutter.io') { - throw TaskResult.failure('Expected Android engine maven dependency URL to ' - 'resolve to https://storage.googleapis.com/download.flutter.io. Got ' - '$mavenUrl instead'); + String realm = File( + path.join(flutterDirectory.path, 'bin', 'internal', 'engine.realm'), + ).readAsStringSync().trim(); + if (realm.isNotEmpty) { + realm = '$realm/'; + } + + if (mavenUrl != 'https://storage.googleapis.com/${realm}download.flutter.io') { + throw TaskResult.failure( + 'Expected Android engine maven dependency URL to ' + 'resolve to https://storage.googleapis.com/${realm}download.flutter.io. Got ' + '$mavenUrl instead', + ); } section('Checking overridden maven URL'); gradleOutput = await eval( - gradlewExecutable, - ['printEngineMavenUrl','-q'], - environment: { - 'FLUTTER_STORAGE_BASE_URL': 'https://my.special.proxy', - } - ); + gradlewExecutable, + ['printEngineMavenUrl','-q'], + environment: { + 'FLUTTER_STORAGE_BASE_URL': 'https://my.special.proxy', + }, + ); outputLines = splitter.convert(gradleOutput); mavenUrl = outputLines.last; - if (mavenUrl != 'https://my.special.proxy/download.flutter.io') { + if (mavenUrl != 'https://my.special.proxy/${realm}download.flutter.io') { throw TaskResult.failure( - 'Expected overridden Android engine maven ' - 'dependency URL to resolve to proxy location ' - 'https://my.special.proxy/download.flutter.io. Got ' - '$mavenUrl instead'); + 'Expected overridden Android engine maven ' + 'dependency URL to resolve to proxy location ' + 'https://my.special.proxy/${realm}download.flutter.io. Got ' + '$mavenUrl instead', + ); } }); }); diff --git a/dev/devicelab/bin/tasks/flavors_test.dart b/dev/devicelab/bin/tasks/flavors_test.dart index 055f58aa29f7f..2db0956f79a62 100644 --- a/dev/devicelab/bin/tasks/flavors_test.dart +++ b/dev/devicelab/bin/tasks/flavors_test.dart @@ -7,37 +7,45 @@ import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/tasks/integration_tests.dart'; +import 'package:path/path.dart' as path; Future main() async { deviceOperatingSystem = DeviceOperatingSystem.android; await task(() async { await createFlavorsTest().call(); await createIntegrationTestFlavorsTest().call(); - // test install and uninstall of flavors app - await inDirectory('${flutterDirectory.path}/dev/integration_tests/flavors', () async { - await flutter( - 'install', - options: ['--debug', '--flavor', 'paid'], - ); - await flutter( - 'install', - options: ['--debug', '--flavor', 'paid', '--uninstall-only'], - ); - final StringBuffer stderr = StringBuffer(); - await evalFlutter( - 'install', - canFail: true, - stderr: stderr, - options: ['--flavor', 'bogus'], - ); - final String stderrString = stderr.toString(); - if (!stderrString.contains('The Xcode project defines schemes: free, paid')) { - print(stderrString); - return TaskResult.failure('Should not succeed with bogus flavor'); - } - }); + final TaskResult installTestsResult = await inDirectory( + '${flutterDirectory.path}/dev/integration_tests/flavors', + () async { + await flutter( + 'install', + options: ['--debug', '--flavor', 'paid'], + ); + await flutter( + 'install', + options: ['--debug', '--flavor', 'paid', '--uninstall-only'], + ); - return TaskResult.success(null); + final StringBuffer stderr = StringBuffer(); + await evalFlutter( + 'install', + canFail: true, + stderr: stderr, + options: ['--flavor', 'bogus'], + ); + + final String stderrString = stderr.toString(); + final String expectedApkPath = path.join('build', 'app', 'outputs', 'flutter-apk', 'app-bogus-release.apk'); + if (!stderrString.contains('"$expectedApkPath" does not exist.')) { + print(stderrString); + return TaskResult.failure('Should not succeed with bogus flavor'); + } + + return TaskResult.success(null); + }, + ); + + return installTestsResult; }); } diff --git a/dev/devicelab/bin/tasks/flavors_test_ios.dart b/dev/devicelab/bin/tasks/flavors_test_ios.dart index 2fbfcb9e0a4af..4e84ed4ec3566 100644 --- a/dev/devicelab/bin/tasks/flavors_test_ios.dart +++ b/dev/devicelab/bin/tasks/flavors_test_ios.dart @@ -14,30 +14,35 @@ Future main() async { await createFlavorsTest().call(); await createIntegrationTestFlavorsTest().call(); // test install and uninstall of flavors app - await inDirectory('${flutterDirectory.path}/dev/integration_tests/flavors', () async { - await flutter( - 'install', - options: ['--flavor', 'paid'], - ); - await flutter( - 'install', - options: ['--flavor', 'paid', '--uninstall-only'], - ); - final StringBuffer stderr = StringBuffer(); - await evalFlutter( - 'install', - canFail: true, - stderr: stderr, - options: ['--flavor', 'bogus'], - ); + final TaskResult installTestsResult = await inDirectory( + '${flutterDirectory.path}/dev/integration_tests/flavors', + () async { + await flutter( + 'install', + options: ['--flavor', 'paid'], + ); + await flutter( + 'install', + options: ['--flavor', 'paid', '--uninstall-only'], + ); + final StringBuffer stderr = StringBuffer(); + await evalFlutter( + 'install', + canFail: true, + stderr: stderr, + options: ['--flavor', 'bogus'], + ); - final String stderrString = stderr.toString(); - if (!stderrString.contains('install failed, bogus flavor not found')) { - print(stderrString); - return TaskResult.failure('Should not succeed with bogus flavor'); - } - }); + final String stderrString = stderr.toString(); + if (!stderrString.contains('The Xcode project defines schemes: free, paid')) { + print(stderrString); + return TaskResult.failure('Should not succeed with bogus flavor'); + } - return TaskResult.success(null); + return TaskResult.success(null); + }, + ); + + return installTestsResult; }); } diff --git a/dev/devicelab/bin/tasks/flavors_test_macos.dart b/dev/devicelab/bin/tasks/flavors_test_macos.dart index 346bc31a19c98..e3bb56123bc4a 100644 --- a/dev/devicelab/bin/tasks/flavors_test_macos.dart +++ b/dev/devicelab/bin/tasks/flavors_test_macos.dart @@ -14,26 +14,31 @@ Future main() async { await createFlavorsTest().call(); await createIntegrationTestFlavorsTest().call(); - await inDirectory('${flutterDirectory.path}/dev/integration_tests/flavors', () async { - final StringBuffer stderr = StringBuffer(); + final TaskResult installTestsResult = await inDirectory( + '${flutterDirectory.path}/dev/integration_tests/flavors', + () async { + final StringBuffer stderr = StringBuffer(); - await evalFlutter( - 'install', - canFail: true, - stderr: stderr, - options: [ - '--d', 'macos', - '--flavor', 'free' - ], - ); + await evalFlutter( + 'install', + canFail: true, + stderr: stderr, + options: [ + '--d', 'macos', + '--flavor', 'free' + ], + ); - final String stderrString = stderr.toString(); - if (!stderrString.contains('Host and target are the same. Nothing to install.')) { - print(stderrString); - return TaskResult.failure('Installing a macOS app on macOS should no-op'); - } - }); + final String stderrString = stderr.toString(); + if (!stderrString.contains('Host and target are the same. Nothing to install.')) { + print(stderrString); + return TaskResult.failure('Installing a macOS app on macOS should no-op'); + } - return TaskResult.success(null); + return TaskResult.success(null); + }, + ); + + return installTestsResult; }); } diff --git a/dev/devicelab/bin/tasks/flutter_engine_group_performance.dart b/dev/devicelab/bin/tasks/flutter_engine_group_performance.dart index 995b38918e66c..5ba8eb091b6ac 100644 --- a/dev/devicelab/bin/tasks/flutter_engine_group_performance.dart +++ b/dev/devicelab/bin/tasks/flutter_engine_group_performance.dart @@ -16,7 +16,7 @@ const String _activityName = 'MainActivity'; const int _numberOfIterations = 10; Future _withApkInstall( - String apkPath, String bundleName, Function(AndroidDevice) body) async { + String apkPath, String bundleName, Future Function(AndroidDevice) body) async { final DeviceDiscovery devices = DeviceDiscovery(); final AndroidDevice device = await devices.workingDevice as AndroidDevice; await device.unlock(); diff --git a/dev/devicelab/bin/tasks/hello_world_impeller.dart b/dev/devicelab/bin/tasks/hello_world_impeller.dart new file mode 100644 index 0000000000000..e67a25b48e6d3 --- /dev/null +++ b/dev/devicelab/bin/tasks/hello_world_impeller.dart @@ -0,0 +1,81 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async' show Completer, StreamSubscription; +import 'dart:io' show Directory, Process; + +import 'package:flutter_devicelab/framework/devices.dart' + show Device, DeviceOperatingSystem, deviceOperatingSystem, devices; +import 'package:flutter_devicelab/framework/framework.dart' show task; +import 'package:flutter_devicelab/framework/task_result.dart' show TaskResult; +import 'package:flutter_devicelab/framework/utils.dart' + show dir, flutter, flutterDirectory, inDirectory, startFlutter; +import 'package:path/path.dart' as path; + +Future run() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + final Device device = await devices.workingDevice; + await device.unlock(); + final Directory appDir = + dir(path.join(flutterDirectory.path, 'examples/hello_world')); + + bool isUsingValidationLayers = false; + bool hasValidationErrors = false; + int impellerBackendCount = 0; + final Completer didReceiveBackendMessage = Completer(); + + await inDirectory(appDir, () async { + await flutter('packages', options: ['get']); + + final StreamSubscription adb = device.logcat.listen( + (String data) { + if (data.contains('Using the Impeller rendering backend')) { + // Sometimes more than one of these will be printed out if there is a + // fallback. + if (!didReceiveBackendMessage.isCompleted) { + didReceiveBackendMessage.complete(); + } + impellerBackendCount += 1; + } + if (data.contains( + 'Using the Impeller rendering backend (Vulkan with Validation Layers)')) { + isUsingValidationLayers = true; + } + // "ImpellerValidationBreak" comes from the engine: + // https://github.com/flutter/engine/blob/4160ebacdae2081d6f3160432f5f0dd87dbebec1/impeller/base/validation.cc#L40 + if (data.contains('ImpellerValidationBreak')) { + hasValidationErrors = true; + } + }, + ); + + final Process process = await startFlutter( + 'run', + options: [ + '--enable-impeller', + '-d', + device.deviceId, + ], + ); + + await didReceiveBackendMessage.future; + // Since we are waiting for the lack of errors, there is no determinate + // amount of time we can wait. + await Future.delayed(const Duration(seconds: 30)); + process.stdin.write('q'); + await adb.cancel(); + }); + + if (!isUsingValidationLayers || impellerBackendCount != 1) { + return TaskResult.failure('Not using Vulkan validation layers.'); + } + if (hasValidationErrors){ + return TaskResult.failure('Impeller validation errors detected.'); + } + return TaskResult.success(null); +} + +Future main() async { + await task(run); +} diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index 86d47afe35763..cd6218e451782 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -27,8 +27,8 @@ Future main() async { }, ); - // this variable cannot be `late`, as we reference it in the `finally` block - // which may execute before this field has been initialized + // This variable cannot be `late`, as we reference it in the `finally` block + // which may execute before this field has been initialized. String? simulatorDeviceId; section('Create Flutter module project'); @@ -51,7 +51,7 @@ Future main() async { final Directory flutterModuleLibSource = Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app', 'flutterapp', 'lib')); final Directory flutterModuleLibDestination = Directory(path.join(projectDir.path, 'lib')); - // These test files don't have a .dart prefix so the analyzer will ignore them. They aren't in a + // These test files don't have a .dart extension so the analyzer will ignore them. They aren't in a // package and don't work on their own outside of the test module just created. final File main = File(path.join(flutterModuleLibSource.path, 'main')); main.copySync(path.join(flutterModuleLibDestination.path, 'main.dart')); @@ -59,6 +59,36 @@ Future main() async { final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee')); marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart')); + section('Create package with native assets'); + + await flutter( + 'config', + options: ['--enable-native-assets'], + ); + + const String ffiPackageName = 'ffi_package'; + await _createFfiPackage(ffiPackageName, tempDir); + + section('Add FFI package'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceFirst( + 'dependencies:\n', + ''' +dependencies: + $ffiPackageName: + path: ../$ffiPackageName +''', + ); + await pubspec.writeAsString(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + section('Build ephemeral host app in release mode without CocoaPods'); await inDirectory(projectDir, () async { @@ -162,10 +192,8 @@ Future main() async { section('Add plugins'); - final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); - String content = await pubspec.readAsString(); content = content.replaceFirst( - '\ndependencies:\n', + 'dependencies:\n', // One framework, one Dart-only, one that does not support iOS, and one with a resource bundle. ''' dependencies: @@ -221,6 +249,11 @@ dependencies: // Dart-only, no embedded framework. checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework')); + // Native assets embedded, no embedded framework. + const String libFfiPackageDylib = 'lib$ffiPackageName.dylib'; + checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', libFfiPackageDylib)); + checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$ffiPackageName.framework')); + section('Clean and pub get module'); await inDirectory(projectDir, () async { @@ -350,6 +383,11 @@ end 'isolate_snapshot_data', )); + checkFileExists(path.join( + hostFrameworksDirectory, + libFfiPackageDylib, + )); + section('Check the NOTICE file is correct'); final String licenseFilePath = path.join( @@ -449,6 +487,13 @@ end throw TaskResult.failure('Unexpected armv7 architecture slice in $builtAppBinary'); } + // Check native assets are bundled. + checkFileExists(path.join( + archivedAppPath, + 'Frameworks', + libFfiPackageDylib, + )); + // The host app example builds plugins statically, url_launcher_ios.framework // should not exist. checkDirectoryNotExists(path.join( @@ -685,3 +730,17 @@ class $dartPluginClass { // Remove the native plugin code. await Directory(path.join(pluginDir, 'ios')).delete(recursive: true); } + +Future _createFfiPackage(String name, Directory parent) async { + await inDirectory(parent, () async { + await flutter( + 'create', + options: [ + '--org', + 'io.flutter.devicelab', + '--template=package_ffi', + name, + ], + ); + }); +} diff --git a/dev/devicelab/bin/tasks/native_assets_ios.dart b/dev/devicelab/bin/tasks/native_assets_ios.dart new file mode 100644 index 0000000000000..9db75bf3c86e5 --- /dev/null +++ b/dev/devicelab/bin/tasks/native_assets_ios.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/native_assets_test.dart'; + +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + return createNativeAssetsTest()(); + }); +} diff --git a/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart b/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart new file mode 100644 index 0000000000000..73579452434cf --- /dev/null +++ b/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/ios.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/tasks/native_assets_test.dart'; + +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + String? simulatorDeviceId; + try { + await testWithNewIOSSimulator( + 'TestNativeAssetsSim', + (String deviceId) async { + simulatorDeviceId = deviceId; + await createNativeAssetsTest( + deviceIdOverride: deviceId, + isIosSimulator: true, + )(); + }, + ); + } finally { + await removeIOSimulator(simulatorDeviceId); + } + return TaskResult.success(null); + }); +} diff --git a/dev/devicelab/bin/tasks/new_gallery_opengles_impeller__transition_perf.dart b/dev/devicelab/bin/tasks/new_gallery_opengles_impeller__transition_perf.dart new file mode 100644 index 0000000000000..2a33ce72a8e79 --- /dev/null +++ b/dev/devicelab/bin/tasks/new_gallery_opengles_impeller__transition_perf.dart @@ -0,0 +1,24 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:flutter_devicelab/tasks/new_gallery.dart'; +import 'package:path/path.dart' as path; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + + final Directory galleryParentDir = Directory.systemTemp.createTempSync('flutter_new_gallery_test.'); + final Directory galleryDir = Directory(path.join(galleryParentDir.path, 'gallery')); + + try { + await task(NewGalleryPerfTest(galleryDir, enableImpeller: true, forceOpenGLES: true).run); + } finally { + rmTree(galleryParentDir); + } +} diff --git a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart index da776743dbb5d..bf74614c3c544 100644 --- a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart +++ b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart @@ -101,7 +101,7 @@ dependencies: sdk: flutter environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=1.5.0" ''', flush: true); diff --git a/dev/devicelab/bin/tasks/plugin_lint_mac.dart b/dev/devicelab/bin/tasks/plugin_lint_mac.dart index 79015aa39245f..f4a8f0faa8579 100644 --- a/dev/devicelab/bin/tasks/plugin_lint_mac.dart +++ b/dev/devicelab/bin/tasks/plugin_lint_mac.dart @@ -46,12 +46,9 @@ Future main() async { ); final String macosintegrationTestPodspec = path.join(integrationTestPackage, 'integration_test_macos', 'macos', 'integration_test_macos.podspec'); - await exec( - 'pod', + await _tryMacOSLint( + macosintegrationTestPodspec, [ - 'lib', - 'lint', - macosintegrationTestPodspec, '--verbose', // TODO(cyanglaz): remove allow-warnings when https://github.com/flutter/flutter/issues/125812 is fixed. // https://github.com/flutter/flutter/issues/125812 @@ -164,12 +161,9 @@ Future main() async { final String macOSPodspecPath = path.join(swiftPluginPath, 'macos', '$swiftPluginName.podspec'); await inDirectory(tempDir, () async { - await exec( - 'pod', + await _tryMacOSLint( + macOSPodspecPath, [ - 'lib', - 'lint', - macOSPodspecPath, '--allow-warnings', '--verbose', ], @@ -179,12 +173,9 @@ Future main() async { section('Lint Swift macOS podspec plugin as library'); await inDirectory(tempDir, () async { - await exec( - 'pod', + await _tryMacOSLint( + macOSPodspecPath, [ - 'lib', - 'lint', - macOSPodspecPath, '--allow-warnings', '--use-libraries', '--verbose', @@ -533,3 +524,32 @@ void _validateMacOSPodfile(String appPath) { 'macos', )); } + +Future _tryMacOSLint( + String podspecPath, + List extraArguments, +) async { + final StringBuffer lintStdout = StringBuffer(); + try { + await eval( + 'pod', + [ + 'lib', + 'lint', + podspecPath, + ...extraArguments, + ], + stdout: lintStdout, + ); + } on BuildFailedError { + // Temporarily ignore errors due to DT_TOOLCHAIN_DIR if it's the only error. + // This error was introduced with Xcode 15. Fix was made in Cocoapods, but + // is not in an official release yet. + // TODO(vashworth): Stop ignoring when https://github.com/flutter/flutter/issues/133584 is complete. + final String lintResult = lintStdout.toString(); + if (!(lintResult.contains('error: DT_TOOLCHAIN_DIR cannot be used to evaluate') && + lintResult.contains('did not pass validation, due to 1 error'))) { + rethrow; + } + } +} diff --git a/dev/devicelab/bin/tasks/static_path_tessellation_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/static_path_tessellation_perf__timeline_summary.dart new file mode 100644 index 0000000000000..10285bf9e07f3 --- /dev/null +++ b/dev/devicelab/bin/tasks/static_path_tessellation_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createPathTessellationStaticPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/static_path_tessellation_perf_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/static_path_tessellation_perf_ios__timeline_summary.dart new file mode 100644 index 0000000000000..5278dfbb2d740 --- /dev/null +++ b/dev/devicelab/bin/tasks/static_path_tessellation_perf_ios__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createPathTessellationStaticPerfTest()); +} diff --git a/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart b/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart new file mode 100644 index 0000000000000..ef738f8510a79 --- /dev/null +++ b/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createVeryLongPictureScrollingPerfE2ETest(enableImpeller: false)); +} diff --git a/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf_ios__e2e_summary.dart b/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf_ios__e2e_summary.dart new file mode 100644 index 0000000000000..99e61f877fe74 --- /dev/null +++ b/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf_ios__e2e_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/perf_tests.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createVeryLongPictureScrollingPerfE2ETest(enableImpeller: false)); +} diff --git a/dev/devicelab/lib/command/test.dart b/dev/devicelab/lib/command/test.dart index a98d2b3ecebee..c65a5da287d6f 100644 --- a/dev/devicelab/lib/command/test.dart +++ b/dev/devicelab/lib/command/test.dart @@ -13,7 +13,7 @@ class TestCommand extends Command { help: 'The name of a task listed under bin/tasks.\n' ' Example: complex_layout__start_up.\n'); argParser.addMultiOption('task-args', - help: 'The name of a task listed under bin/tasks.\n' + help: 'List of arguments to pass to the task.\n' 'For example, "--task-args build" is passed as "bin/task/task.dart --build"'); argParser.addOption( 'device-id', @@ -24,6 +24,11 @@ class TestCommand extends Command { 'settings in the test case, and will results in error if no device\n' 'with given ID/ID prefix is found.', ); + argParser.addFlag( + 'exit', + help: 'Exit on the first test failure. Currently flakes are intentionally (though ' + 'incorrectly) not considered to be failures.', + ); argParser.addOption( 'git-branch', help: '[Flutter infrastructure] Git branch of the current commit. LUCI\n' @@ -37,6 +42,15 @@ class TestCommand extends Command { 'This path is relative to --local-engine-src-path/out. This option\n' 'is required when running an A/B test (see the --ab option).', ); + argParser.addOption( + 'local-engine-host', + help: 'Name of a build output within the engine out directory, if you\n' + 'are building Flutter locally. Use this to select a specific\n' + 'version of the engine to use as the host platform if you have built ' + 'multiple engine targets.\n' + 'This path is relative to --local-engine-src-path/out. This option\n' + 'is required when running an A/B test (see the --ab option).', + ); argParser.addOption( 'local-engine-src-path', help: 'Path to your engine src directory, if you are building Flutter\n' @@ -74,12 +88,14 @@ class TestCommand extends Command { deviceId: argResults!['device-id'] as String?, gitBranch: argResults!['git-branch'] as String?, localEngine: argResults!['local-engine'] as String?, + localEngineHost: argResults!['local-engine-host'] as String?, localEngineSrcPath: argResults!['local-engine-src-path'] as String?, luciBuilder: argResults!['luci-builder'] as String?, resultsPath: argResults!['results-file'] as String?, silent: (argResults!['silent'] as bool?) ?? false, useEmulator: (argResults!['use-emulator'] as bool?) ?? false, taskArgs: taskArgs, + exitOnFirstTestFailure: argResults!['exit'] as bool, ); } } diff --git a/dev/devicelab/lib/framework/ab.dart b/dev/devicelab/lib/framework/ab.dart index e7da55855006e..0e05a130bfbc4 100644 --- a/dev/devicelab/lib/framework/ab.dart +++ b/dev/devicelab/lib/framework/ab.dart @@ -9,6 +9,7 @@ import 'task_result.dart'; const String kBenchmarkTypeKeyName = 'benchmark_type'; const String kBenchmarkVersionKeyName = 'version'; const String kLocalEngineKeyName = 'local_engine'; +const String kLocalEngineHostKeyName = 'local_engine_host'; const String kTaskNameKeyName = 'task_name'; const String kRunStartKeyName = 'run_start'; const String kRunEndKeyName = 'run_end'; @@ -24,13 +25,14 @@ enum FieldJustification { LEFT, RIGHT, CENTER } /// /// See [printSummary] for more. class ABTest { - ABTest(this.localEngine, this.taskName) + ABTest({required this.localEngine, required this.localEngineHost, required this.taskName}) : runStart = DateTime.now(), _aResults = >{}, _bResults = >{}; ABTest.fromJsonMap(Map jsonResults) : localEngine = jsonResults[kLocalEngineKeyName] as String, + localEngineHost = jsonResults[kLocalEngineHostKeyName] as String, taskName = jsonResults[kTaskNameKeyName] as String, runStart = DateTime.parse(jsonResults[kRunStartKeyName] as String), _runEnd = DateTime.parse(jsonResults[kRunEndKeyName] as String), @@ -38,6 +40,7 @@ class ABTest { _bResults = _convertFrom(jsonResults[kBResultsKeyName] as Map); final String localEngine; + final String localEngineHost; final String taskName; final DateTime runStart; DateTime? _runEnd; @@ -86,6 +89,7 @@ class ABTest { kBenchmarkTypeKeyName: kBenchmarkResultsType, kBenchmarkVersionKeyName: kBenchmarkABVersion, kLocalEngineKeyName: localEngine, + kLocalEngineHostKeyName: localEngineHost, kTaskNameKeyName: taskName, kRunStartKeyName: runStart.toIso8601String(), kRunEndKeyName: runEnd!.toIso8601String(), diff --git a/dev/devicelab/lib/framework/browser.dart b/dev/devicelab/lib/framework/browser.dart index 9d4581faec009..a35f6ac0d85d0 100644 --- a/dev/devicelab/lib/framework/browser.dart +++ b/dev/devicelab/lib/framework/browser.dart @@ -87,6 +87,10 @@ class Chrome { print('Launching Chrome...'); } + final String jsFlags = options.enableWasmGC ? [ + '--experimental-wasm-gc', + '--experimental-wasm-type-reflection', + ].join(' ') : ''; final bool withDebugging = options.debugPort != null; final List args = [ if (options.userDataDirectory != null) @@ -108,8 +112,7 @@ class Chrome { '--no-default-browser-check', '--disable-default-apps', '--disable-translate', - if (options.enableWasmGC) - '--js-flags=--experimental-wasm-gc', + if (jsFlags.isNotEmpty) '--js-flags=$jsFlags', ]; final io.Process chromeProcess = await _spawnChromiumProcess( diff --git a/dev/devicelab/lib/framework/framework.dart b/dev/devicelab/lib/framework/framework.dart index 2282907aaf8de..d64fe2e15023e 100644 --- a/dev/devicelab/lib/framework/framework.dart +++ b/dev/devicelab/lib/framework/framework.dart @@ -74,11 +74,13 @@ class _TaskRunner { final bool runFlutterConfig = parameters['runFlutterConfig'] != 'false'; // used by tests to avoid changing the configuration final bool runProcessCleanup = parameters['runProcessCleanup'] != 'false'; final String? localEngine = parameters['localEngine']; + final String? localEngineHost = parameters['localEngineHost']; final TaskResult result = await run( taskTimeout, runProcessCleanup: runProcessCleanup, runFlutterConfig: runFlutterConfig, localEngine: localEngine, + localEngineHost: localEngineHost, ); return ServiceExtensionResponse.result(json.encode(result.toJson())); }); @@ -115,6 +117,7 @@ class _TaskRunner { bool runFlutterConfig = true, bool runProcessCleanup = true, required String? localEngine, + required String? localEngineHost, }) async { try { _taskStarted = true; @@ -144,6 +147,7 @@ class _TaskRunner { '--enable-macos-desktop', '--enable-linux-desktop', if (localEngine != null) ...['--local-engine', localEngine], + if (localEngineHost != null) ...['--local-engine-host', localEngineHost], ], canFail: true); if (configResult != 0) { print('Failed to enable configuration, tasks may not run.'); diff --git a/dev/devicelab/lib/framework/runner.dart b/dev/devicelab/lib/framework/runner.dart index 88a81d81ba934..3696a9ce86c08 100644 --- a/dev/devicelab/lib/framework/runner.dart +++ b/dev/devicelab/lib/framework/runner.dart @@ -35,23 +35,25 @@ Future runTasks( String? deviceId, String? gitBranch, String? localEngine, + String? localEngineHost, String? localEngineSrcPath, String? luciBuilder, String? resultsPath, List? taskArgs, bool useEmulator = false, @visibleForTesting Map? isolateParams, - @visibleForTesting Function(String) print = print, + @visibleForTesting void Function(String) print = print, @visibleForTesting List? logs, }) async { for (final String taskName in taskNames) { TaskResult result = TaskResult.success(null); - int retry = 0; - while (retry <= Cocoon.retryNumber) { + int failureCount = 0; + while (failureCount <= Cocoon.retryNumber) { result = await rerunTask( taskName, deviceId: deviceId, localEngine: localEngine, + localEngineHost: localEngineHost, localEngineSrcPath: localEngineSrcPath, terminateStrayDartProcesses: terminateStrayDartProcesses, silent: silent, @@ -64,11 +66,14 @@ Future runTasks( ); if (!result.succeeded) { - retry += 1; + failureCount += 1; + if (exitOnFirstTestFailure) { + break; + } } else { section('Flaky status for "$taskName"'); - if (retry > 0) { - print('Total ${retry+1} executions: $retry failures and 1 false positive.'); + if (failureCount > 0) { + print('Total ${failureCount+1} executions: $failureCount failures and 1 false positive.'); print('flaky: true'); // TODO(ianh): stop ignoring this failure. We should set exitCode=1, and quit // if exitOnFirstTestFailure is true. @@ -82,7 +87,7 @@ Future runTasks( if (!result.succeeded) { section('Flaky status for "$taskName"'); - print('Consistently failed across all $retry executions.'); + print('Consistently failed across all $failureCount executions.'); print('flaky: false'); exitCode = 1; if (exitOnFirstTestFailure) { @@ -99,6 +104,7 @@ Future rerunTask( String taskName, { String? deviceId, String? localEngine, + String? localEngineHost, String? localEngineSrcPath, bool terminateStrayDartProcesses = false, bool silent = false, @@ -114,6 +120,7 @@ Future rerunTask( taskName, deviceId: deviceId, localEngine: localEngine, + localEngineHost: localEngineHost, localEngineSrcPath: localEngineSrcPath, terminateStrayDartProcesses: terminateStrayDartProcesses, silent: silent, @@ -153,6 +160,7 @@ Future runTask( bool terminateStrayDartProcesses = false, bool silent = false, String? localEngine, + String? localEngineHost, String? localWebSdk, String? localEngineSrcPath, String? deviceId, @@ -182,6 +190,7 @@ Future runTask( '--enable-vm-service=0', // zero causes the system to choose a free port '--no-pause-isolates-on-exit', if (localEngine != null) '-DlocalEngine=$localEngine', + if (localEngineHost != null) '-DlocalEngineHost=$localEngineHost', if (localWebSdk != null) '-DlocalWebSdk=$localWebSdk', if (localEngineSrcPath != null) '-DlocalEngineSrcPath=$localEngineSrcPath', taskExecutable, diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 20301e51eebd0..40b5820c21ea8 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -26,6 +26,14 @@ String? get localEngineFromEnv { return isDefined ? const String.fromEnvironment('localEngine') : null; } +/// The local engine host to use for [flutter] and [evalFlutter], if any. +/// +/// This is set as an environment variable when running the task, see runTask in runner.dart. +String? get localEngineHostFromEnv { + const bool isDefined = bool.hasEnvironment('localEngineHost'); + return isDefined ? const String.fromEnvironment('localEngineHost') : null; +} + /// The local engine source path to use if a local engine is used for [flutter] /// and [evalFlutter]. /// @@ -422,11 +430,12 @@ Future eval( Map? environment, bool canFail = false, // as in, whether failures are ok. False means that they are fatal. String? workingDirectory, + StringBuffer? stdout, // if not null, the stdout will be written here StringBuffer? stderr, // if not null, the stderr will be written here bool printStdout = true, bool printStderr = true, }) async { - final StringBuffer output = StringBuffer(); + final StringBuffer output = stdout ?? StringBuffer(); await _execute( executable, arguments, @@ -453,6 +462,7 @@ List _flutterCommandArgs(String command, List options) { 'screenshot', }; final String? localEngine = localEngineFromEnv; + final String? localEngineHost = localEngineHostFromEnv; final String? localEngineSrcPath = localEngineSrcPathFromEnv; final String? localWebSdk = localWebSdkFromEnv; return [ @@ -468,6 +478,7 @@ List _flutterCommandArgs(String command, List options) { hostAgent.dumpDirectory!.path, ], if (localEngine != null) ...['--local-engine', localEngine], + if (localEngineHost != null) ...['--local-engine-host', localEngineHost], if (localEngineSrcPath != null) ...['--local-engine-src-path', localEngineSrcPath], if (localWebSdk != null) ...['--local-web-sdk', localWebSdk], ...options, diff --git a/dev/devicelab/lib/tasks/gallery.dart b/dev/devicelab/lib/tasks/gallery.dart index 887d960d3f907..f9ae9a058c481 100644 --- a/dev/devicelab/lib/tasks/gallery.dart +++ b/dev/devicelab/lib/tasks/gallery.dart @@ -189,6 +189,7 @@ class GalleryTransitionTest { '90th_percentile_frame_build_time_millis', '99th_percentile_frame_build_time_millis', 'average_frame_rasterizer_time_millis', + 'stddev_frame_rasterizer_time_millis', 'worst_frame_rasterizer_time_millis', '90th_percentile_frame_rasterizer_time_millis', '99th_percentile_frame_rasterizer_time_millis', diff --git a/dev/devicelab/lib/tasks/native_assets_test.dart b/dev/devicelab/lib/tasks/native_assets_test.dart new file mode 100644 index 0000000000000..7d93272e7eb4b --- /dev/null +++ b/dev/devicelab/lib/tasks/native_assets_test.dart @@ -0,0 +1,191 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../framework/devices.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +const String _packageName = 'package_with_native_assets'; + +const List _buildModes = [ + 'debug', + 'profile', + 'release', +]; + +TaskFunction createNativeAssetsTest({ + String? deviceIdOverride, + bool checkAppRunningOnLocalDevice = true, + bool isIosSimulator = false, +}) { + return () async { + if (deviceIdOverride == null) { + final Device device = await devices.workingDevice; + await device.unlock(); + deviceIdOverride = device.deviceId; + } + + await enableNativeAssets(); + + for (final String buildMode in _buildModes) { + if (buildMode != 'debug' && isIosSimulator) { + continue; + } + final TaskResult buildModeResult = await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(_packageName, tempDirectory); + final Directory exampleDirectory = dir(packageDirectory.uri.resolve('example/').toFilePath()); + + final List options = [ + '-d', + deviceIdOverride!, + '--no-android-gradle-daemon', + '--no-publish-port', + '--verbose', + '--uninstall-first', + '--$buildMode', + ]; + int transitionCount = 0; + bool done = false; + + await inDirectory(exampleDirectory, () async { + final int runFlutterResult = await runFlutter( + options: options, + onLine: (String line, Process process) { + if (done) { + return; + } + switch (transitionCount) { + case 0: + if (!line.contains('Flutter run key commands.')) { + return; + } + if (buildMode == 'debug') { + // Do a hot reload diff on the initial dill file. + process.stdin.writeln('r'); + } else { + done = true; + process.stdin.writeln('q'); + } + case 1: + if (!line.contains('Reloaded')) { + return; + } + process.stdin.writeln('R'); + case 2: + // Do a hot restart, pushing a new complete dill file. + if (!line.contains('Restarted application')) { + return; + } + // Do another hot reload, pushing a diff to the second dill file. + process.stdin.writeln('r'); + case 3: + if (!line.contains('Reloaded')) { + return; + } + done = true; + process.stdin.writeln('q'); + } + transitionCount += 1; + }, + ); + if (runFlutterResult != 0) { + print('Flutter run returned non-zero exit code: $runFlutterResult.'); + } + }); + + final int expectedNumberOfTransitions = buildMode == 'debug' ? 4 : 1; + if (transitionCount != expectedNumberOfTransitions) { + return TaskResult.failure( + 'Did not get expected number of transitions: $transitionCount ' + '(expected $expectedNumberOfTransitions)', + ); + } + return TaskResult.success(null); + }); + if (buildModeResult.failed) { + return buildModeResult; + } + } + return TaskResult.success(null); + }; +} + +Future runFlutter({ + required List options, + required void Function(String, Process) onLine, +}) async { + final Process process = await startFlutter( + 'run', + options: options, + ); + + final Completer stdoutDone = Completer(); + final Completer stderrDone = Completer(); + process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) { + onLine(line, process); + print('stdout: $line'); + }, onDone: stdoutDone.complete); + + process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen( + (String line) => print('stderr: $line'), + onDone: stderrDone.complete, + ); + + await Future.wait(>[stdoutDone.future, stderrDone.future]); + final int exitCode = await process.exitCode; + return exitCode; +} + +final String _flutterBin = path.join(flutterDirectory.path, 'bin', 'flutter'); + +Future enableNativeAssets() async { + print('Enabling configs for native assets...'); + final int configResult = await exec( + _flutterBin, + [ + 'config', + '-v', + '--enable-native-assets', + ], + canFail: true); + if (configResult != 0) { + print('Failed to enable configuration, tasks may not run.'); + } +} + +Future createTestProject( + String packageName, + Directory tempDirectory, +) async { + final int createResult = await exec( + _flutterBin, + [ + 'create', + '--template=package_ffi', + packageName, + ], + workingDirectory: tempDirectory.path, + canFail: true, + ); + assert(createResult == 0); + + final Directory packageDirectory = Directory.fromUri(tempDirectory.uri.resolve('$packageName/')); + return packageDirectory; +} + +Future inTempDir(Future Function(Directory tempDirectory) fun) async { + final Directory tempDirectory = dir(Directory.systemTemp.createTempSync().resolveSymbolicLinksSync()); + try { + return await fun(tempDirectory); + } finally { + tempDirectory.deleteSync(recursive: true); + } +} diff --git a/dev/devicelab/lib/tasks/new_gallery.dart b/dev/devicelab/lib/tasks/new_gallery.dart index dcc52ff56a268..6f8b998b4be75 100644 --- a/dev/devicelab/lib/tasks/new_gallery.dart +++ b/dev/devicelab/lib/tasks/new_gallery.dart @@ -16,6 +16,7 @@ class NewGalleryPerfTest extends PerfTest { String dartDefine = '', super.enableImpeller, super.timeoutSeconds, + super.forceOpenGLES, }) : super( galleryDir.path, 'test_driver/transitions_perf.dart', diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 193e4e57fba3f..1dc73382bfab7 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -9,6 +9,7 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; +import 'package:xml/xml.dart'; import '../framework/devices.dart'; import '../framework/framework.dart'; @@ -307,6 +308,13 @@ TaskFunction createTextfieldPerfE2ETest() { ).run; } +TaskFunction createVeryLongPictureScrollingPerfE2ETest({required bool enableImpeller}) { + return PerfTest.e2e( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test/very_long_picture_scrolling_perf_e2e.dart', + enableImpeller: enableImpeller, + ).run; +} TaskFunction createSlidersPerfTest() { return PerfTest( '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', @@ -628,14 +636,31 @@ TaskFunction createGradientStaticPerfE2ETest() { ).run; } +TaskFunction createAnimatedAdvancedBlendPerfTest({ + bool? enableImpeller, + bool? forceOpenGLES, +}) { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'animated_advanced_blend_perf', + enableImpeller: enableImpeller, + forceOpenGLES: forceOpenGLES, + testDriver: 'test_driver/animated_advanced_blend_perf_test.dart', + saveTraceFile: true, + ).run; +} + TaskFunction createAnimatedBlurBackropFilterPerfTest({ bool? enableImpeller, + bool? forceOpenGLES, }) { return PerfTest( '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', 'test_driver/run_app.dart', 'animated_blur_backdrop_filter_perf', enableImpeller: enableImpeller, + forceOpenGLES: forceOpenGLES, testDriver: 'test_driver/animated_blur_backdrop_filter_perf_test.dart', saveTraceFile: true, ).run; @@ -654,6 +679,56 @@ TaskFunction createDrawPointsPerfTest({ ).run; } +TaskFunction createDrawAtlasPerfTest({ + bool? forceOpenGLES, +}) { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'draw_atlas_perf', + enableImpeller: true, + testDriver: 'test_driver/draw_atlas_perf_test.dart', + saveTraceFile: true, + forceOpenGLES: forceOpenGLES, + ).run; +} + +TaskFunction createDrawVerticesPerfTest({ + bool? forceOpenGLES, +}) { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'draw_vertices_perf', + enableImpeller: true, + testDriver: 'test_driver/draw_vertices_perf_test.dart', + saveTraceFile: true, + forceOpenGLES: forceOpenGLES, + ).run; +} + +TaskFunction createPathTessellationStaticPerfTest() { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'tessellation_perf_static', + enableImpeller: true, + testDriver: 'test_driver/path_tessellation_static_perf_test.dart', + saveTraceFile: true, + ).run; +} + +TaskFunction createPathTessellationDynamicPerfTest() { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/run_app.dart', + 'tessellation_perf_dynamic', + enableImpeller: true, + testDriver: 'test_driver/path_tessellation_dynamic_perf_test.dart', + saveTraceFile: true, + ).run; +} + TaskFunction createAnimatedComplexOpacityPerfE2ETest({ bool? enableImpeller, }) { @@ -692,6 +767,63 @@ Map _average(List> results, int iterations return tally; } +/// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml' +/// and adds the following entry to the application. +/// +void _addOpenGLESToManifest(String testDirectory) { + final String manifestPath = path.join( + testDirectory, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); + final File file = File(manifestPath); + + if (!file.existsSync()) { + throw Exception('AndroidManifest.xml not found at $manifestPath'); + } + + final String xmlStr = file.readAsStringSync(); + final XmlDocument xmlDoc = XmlDocument.parse(xmlStr); + const String key = 'io.flutter.embedding.android.ImpellerBackend'; + const String value = 'opengles'; + + final XmlElement applicationNode = + xmlDoc.findAllElements('application').first; + + // Check if the meta-data node already exists. + final Iterable existingMetaData = applicationNode + .findAllElements('meta-data') + .where((XmlElement node) => node.getAttribute('android:name') == key); + + if (existingMetaData.isNotEmpty) { + final XmlElement existingEntry = existingMetaData.first; + existingEntry.setAttribute('android:value', value); + } else { + final XmlElement metaData = XmlElement( + XmlName('meta-data'), + [ + XmlAttribute(XmlName('android:name'), key), + XmlAttribute(XmlName('android:value'), value) + ], + ); + + applicationNode.children.add(metaData); + } + + file.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' ')); +} + +Future _resetManifest(String testDirectory) async { + final String manifestPath = path.join( + testDirectory, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); + final File file = File(manifestPath); + + if (!file.existsSync()) { + throw Exception('AndroidManifest.xml not found at $manifestPath'); + } + + await exec('git', ['checkout', file.path]); +} + /// Measure application startup performance. class StartupTest { const StartupTest( @@ -769,6 +901,7 @@ class StartupTest { testDirectory, 'build', 'windows', + 'x64', 'runner', 'Profile', '$basename.exe' @@ -778,6 +911,15 @@ class StartupTest { const int maxFailures = 3; int currentFailures = 0; for (int i = 0; i < iterations; i += 1) { + // Startup should not take more than a few minutes. After 10 minutes, + // take a screenshot to help debug. + final Timer timer = Timer(const Duration(minutes: 10), () async { + print('Startup not completed within 10 minutes. Taking a screenshot...'); + await _flutterScreenshot( + device.deviceId, + 'screenshot_startup_${DateTime.now().toLocal().toIso8601String()}.png', + ); + }); final int result = await flutter( 'run', options: [ @@ -786,6 +928,9 @@ class StartupTest { '--verbose', '--profile', '--trace-startup', + // TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836 + if (device is IosDevice) + '--verbose-system-logs', '--target=$target', '-d', device.deviceId, @@ -795,6 +940,7 @@ class StartupTest { environment: runEnvironment, canFail: true, ); + timer.cancel(); if (result == 0) { final Map data = json.decode( file('${_testOutputDirectory(testDirectory)}/start_up_info.json').readAsStringSync(), @@ -802,20 +948,10 @@ class StartupTest { results.add(data); } else { currentFailures += 1; - if (hostAgent.dumpDirectory != null) { - await flutter( - 'screenshot', - options: [ - '-d', - device.deviceId, - '--out', - hostAgent.dumpDirectory! - .childFile('screenshot_startup_failure_$currentFailures.png') - .path, - ], - canFail: true, - ); - } + await _flutterScreenshot( + device.deviceId, + 'screenshot_startup_failure_$currentFailures.png', + ); i -= 1; if (currentFailures == maxFailures) { return TaskResult.failure('Application failed to start $maxFailures times'); @@ -841,6 +977,23 @@ class StartupTest { ]); }); } + + Future _flutterScreenshot(String deviceId, String screenshotName) async { + if (hostAgent.dumpDirectory != null) { + await flutter( + 'screenshot', + options: [ + '-d', + deviceId, + '--out', + hostAgent.dumpDirectory! + .childFile(screenshotName) + .path, + ], + canFail: true, + ); + } + } } /// A one-off test to verify that devtools starts in profile mode. @@ -970,6 +1123,7 @@ class PerfTest { this.flutterDriveCallback, this.timeoutSeconds, this.enableImpeller, + this.forceOpenGLES, }): _resultFilename = resultFilename; const PerfTest.e2e( @@ -987,6 +1141,7 @@ class PerfTest { this.flutterDriveCallback, this.timeoutSeconds, this.enableImpeller, + this.forceOpenGLES, }) : saveTraceFile = false, timelineFileName = null, _resultFilename = resultFilename; /// The directory where the app under test is defined. @@ -1023,6 +1178,9 @@ class PerfTest { /// Whether the perf test should enable Impeller. final bool? enableImpeller; + /// Whether the perf test force Impeller's OpenGLES backend. + final bool? forceOpenGLES; + /// Number of seconds to time out the test after, allowing debug callbacks to run. final int? timeoutSeconds; @@ -1069,42 +1227,62 @@ class PerfTest { await selectedDevice.unlock(); final String deviceId = selectedDevice.deviceId; final String? localEngine = localEngineFromEnv; + final String? localEngineHost = localEngineHostFromEnv; final String? localEngineSrcPath = localEngineSrcPathFromEnv; - final List options = [ - if (localEngine != null) - ...['--local-engine', localEngine], - if (localEngineSrcPath != null) - ...['--local-engine-src-path', localEngineSrcPath], - '--no-dds', - '--no-android-gradle-daemon', - '-v', - '--verbose-system-logs', - '--profile', - if (timeoutSeconds != null) - ...[ + Future Function()? manifestReset; + if (forceOpenGLES ?? false) { + assert(enableImpeller!); + _addOpenGLESToManifest(testDirectory); + manifestReset = () => _resetManifest(testDirectory); + } + + try { + final List options = [ + if (localEngine != null) ...['--local-engine', localEngine], + if (localEngineHost != null) ...[ + '--local-engine-host', + localEngineHost + ], + if (localEngineSrcPath != null) ...[ + '--local-engine-src-path', + localEngineSrcPath + ], + '--no-dds', + '--no-android-gradle-daemon', + '-v', + '--verbose-system-logs', + '--profile', + if (timeoutSeconds != null) ...[ '--timeout', timeoutSeconds.toString(), ], - if (needsFullTimeline) - '--trace-startup', // Enables "endless" timeline event buffering. - '-t', testTarget, - if (testDriver != null) - ...['--driver', testDriver!], - if (existingApp != null) - ...['--use-existing-app', existingApp], - if (dartDefine.isNotEmpty) - ...['--dart-define', dartDefine], - if (enableImpeller != null && enableImpeller!) '--enable-impeller', - if (enableImpeller != null && !enableImpeller!) '--no-enable-impeller', - '-d', - deviceId, - ]; - if (flutterDriveCallback != null) { - flutterDriveCallback!(options); - } else { - await flutter('drive', options: options); + if (needsFullTimeline) + '--trace-startup', // Enables "endless" timeline event buffering. + '-t', testTarget, + if (testDriver != null) ...['--driver', testDriver!], + if (existingApp != null) ...[ + '--use-existing-app', + existingApp + ], + if (dartDefine.isNotEmpty) ...['--dart-define', dartDefine], + if (enableImpeller != null && enableImpeller!) '--enable-impeller', + if (enableImpeller != null && !enableImpeller!) + '--no-enable-impeller', + '-d', + deviceId, + ]; + if (flutterDriveCallback != null) { + flutterDriveCallback!(options); + } else { + await flutter('drive', options: options); + } + } finally { + if (manifestReset != null) { + await manifestReset(); + } } + final Map data = json.decode( file('${_testOutputDirectory(testDirectory)}/$resultFilename.json').readAsStringSync(), ) as Map; @@ -1179,7 +1357,6 @@ const List _kCommonScoreKeys = [ '90th_percentile_picture_cache_memory', '99th_percentile_picture_cache_memory', 'worst_picture_cache_memory', - 'new_gen_gc_count', 'old_gen_gc_count', ]; @@ -1475,6 +1652,7 @@ class CompileTest { cwd, 'build', 'windows', + 'x64', 'runner', 'release', '$basename.exe'); diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index 4ef09183a8b11..5b0aeae53db99 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -294,7 +294,7 @@ public class $pluginClass: NSObject, FlutterPlugin { } case 'windows': if (await exec( - path.join(rootPath, 'build', 'windows', 'plugins', 'plugintest', 'Release', 'plugintest_test.exe'), + path.join(rootPath, 'build', 'windows', 'x64', 'plugins', 'plugintest', 'Release', 'plugintest_test.exe'), [], canFail: true, ) != 0) { diff --git a/dev/devicelab/lib/tasks/run_tests.dart b/dev/devicelab/lib/tasks/run_tests.dart index f7a41169e3004..a5c6f8c7547b3 100644 --- a/dev/devicelab/lib/tasks/run_tests.dart +++ b/dev/devicelab/lib/tasks/run_tests.dart @@ -178,7 +178,7 @@ class WindowsRunOutputTest extends DesktopRunOutputTest { multiLine: true, ); static final RegExp _builtOutput = RegExp( - r'Built build\\windows\\runner\\(Debug|Release)\\\w+\.exe( \(\d+(\.\d+)?MB\))?\.', + r'Built build\\windows\\x64\\runner\\(Debug|Release)\\\w+\.exe( \(\d+(\.\d+)?MB\))?\.', ); @override @@ -205,7 +205,7 @@ class WindowsRunOutputTest extends DesktopRunOutputTest { return true; }, - 'Built build\\windows\\runner\\$buildMode\\app.exe', + 'Built build\\windows\\x64\\runner\\$buildMode\\app.exe', ); } } diff --git a/dev/devicelab/lib/tasks/web_benchmarks.dart b/dev/devicelab/lib/tasks/web_benchmarks.dart index 051133c70db7b..a4748b8f9f85e 100644 --- a/dev/devicelab/lib/tasks/web_benchmarks.dart +++ b/dev/devicelab/lib/tasks/web_benchmarks.dart @@ -36,6 +36,7 @@ Future runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async { if (benchmarkOptions.useWasm) ...[ '--wasm', '--wasm-opt=debug', + '--omit-type-checks', ], '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true', '--web-renderer=${benchmarkOptions.webRenderer}', diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml index a74f303ceb275..90b762ac6afbf 100644 --- a/dev/devicelab/pubspec.yaml +++ b/dev/devicelab/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter continuous integration performance and correctness tests. homepage: https://github.com/flutter/flutter environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: archive: 3.3.2 @@ -11,46 +11,48 @@ dependencies: file: 6.1.4 http: 0.13.6 logging: 1.2.0 - meta: 1.9.1 - metrics_center: 1.0.9 + meta: 1.10.0 + metrics_center: 1.0.12 path: 1.8.3 - platform: 3.1.0 + platform: 3.1.2 process: 4.2.4 pubspec_parse: 1.2.3 shelf: 1.4.1 shelf_static: 1.1.2 - stack_trace: 1.11.0 - vm_service: 11.7.1 - web: 0.1.4-beta - webkit_inspection_protocol: 1.2.0 + stack_trace: 1.11.1 + vm_service: 11.10.0 + web: 0.3.0 + webkit_inspection_protocol: 1.2.1 + xml: 6.4.2 _discoveryapis_commons: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" checked_yaml: 2.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - equatable: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - gcloud: 0.8.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + gcloud: 0.8.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" googleapis: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" googleapis_auth: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_annotation: 4.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 6.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + retry: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -65,9 +67,9 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a090 +# PUBSPEC CHECKSUM: a06f diff --git a/dev/devicelab/test/ab_test.dart b/dev/devicelab/test/ab_test.dart index 27e92fd687662..b6ff15b1c2269 100644 --- a/dev/devicelab/test/ab_test.dart +++ b/dev/devicelab/test/ab_test.dart @@ -9,7 +9,7 @@ import 'common.dart'; void main() { test('ABTest', () { - final ABTest ab = ABTest('engine', 'test'); + final ABTest ab = ABTest(localEngine: 'engine', localEngineHost: 'engine', taskName: 'test'); for (int i = 0; i < 5; i++) { final TaskResult aResult = TaskResult.fromJson({ diff --git a/dev/devicelab/test/run_test.dart b/dev/devicelab/test/run_test.dart index a5332dd53c7ad..933c7391a32dd 100644 --- a/dev/devicelab/test/run_test.dart +++ b/dev/devicelab/test/run_test.dart @@ -96,7 +96,7 @@ void main() { final ProcessResult result = await runScript( ['smoke_test_success'], - ['--ab=2', '--local-engine=host_debug_unopt', '--ab-result-file', abResultsFile.path], + ['--ab=2', '--local-engine=host_debug_unopt', '--local-engine-host=host_debug_unopt', '--ab-result-file', abResultsFile.path], ); expect(result.exitCode, 0); diff --git a/dev/devicelab/test/utils_test.dart b/dev/devicelab/test/utils_test.dart index c51ac5afa99dc..f137140abed4b 100644 --- a/dev/devicelab/test/utils_test.dart +++ b/dev/devicelab/test/utils_test.dart @@ -37,6 +37,7 @@ void main() { group('engine environment declarations', () { test('localEngine', () { expect(localEngineFromEnv, null); + expect(localEngineHostFromEnv, null); expect(localEngineSrcPathFromEnv, null); }); }); diff --git a/dev/docs/README.md b/dev/docs/README.md index 2c20f3d199fdb..e8ac002be6631 100644 --- a/dev/docs/README.md +++ b/dev/docs/README.md @@ -13,7 +13,16 @@ SDK. This site hosts Flutter's API documentation. Other documentation can be found at the following locations: -* [flutter.dev](https://flutter.dev) (main site) +* [flutter.dev](https://flutter.dev) (main Flutter site) +* [Stable channel API Docs](https://api.flutter.dev) +* [Main channel API Docs](https://master-api.flutter.dev) +* Engine Embedder API documentation: + * [Android Embedder](../javadoc/index.html) + * [iOS Embedder](../ios-embedder/index.html) + * [macOS Embedder](../macos-embedder/index.html) + * [Linux Embedder](../linux-embedder/index.html) + * [Windows Embedder](../windows-embedder/index.html) + * [Web Embedder](dart-ui_web/dart-ui_web-library.html) * [Installation](https://flutter.dev/docs/get-started/install) * [Codelabs](https://flutter.dev/docs/codelabs) * [Contributing to Flutter](https://github.com/flutter/flutter/blob/master/CONTRIBUTING.md) diff --git a/dev/docs/analytics-footer.html b/dev/docs/analytics-footer.html new file mode 100644 index 0000000000000..133ecf3bd3e73 --- /dev/null +++ b/dev/docs/analytics-footer.html @@ -0,0 +1,4 @@ + + + diff --git a/dev/docs/analytics-header.html b/dev/docs/analytics-header.html new file mode 100644 index 0000000000000..bed0852361406 --- /dev/null +++ b/dev/docs/analytics-header.html @@ -0,0 +1,7 @@ + + + diff --git a/dev/docs/analytics.html b/dev/docs/analytics.html deleted file mode 100644 index ee11d34b06003..0000000000000 --- a/dev/docs/analytics.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/dev/docs/assets/overrides.css b/dev/docs/assets/overrides.css index 14758845f9e9f..c4e441bf8cb01 100644 --- a/dev/docs/assets/overrides.css +++ b/dev/docs/assets/overrides.css @@ -30,6 +30,15 @@ section.summary h2 { border-bottom: none; } +section.summary .name { + font-size: 1.5em; + margin-right: 0.2em; +} + +section.summary .returntype { + font-style:italic; +} + pre { margin: 0 0 15px 0; padding: 8px 12px; diff --git a/dev/docs/dashing_postprocess.dart b/dev/docs/dashing_postprocess.dart deleted file mode 100644 index 9f186e4f2c315..0000000000000 --- a/dev/docs/dashing_postprocess.dart +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -/// This changes the DocSetPlatformFamily key to be "dartlang" instead of the -/// name of the package (usually "flutter"). -/// -/// This is so that the IntelliJ plugin for Dash will be able to go directly to -/// the docs for a symbol from a keystroke. Without this, flutter isn't part -/// of the list of package names it searches. After this, it finds the flutter -/// docs because they're declared here to be part of the "dartlang" family of -/// docs. -/// -/// Dashing doesn't have a way to configure this, so we modify the Info.plist -/// directly to make the change. -void main(List args) { - final File infoPlist = File('flutter.docset/Contents/Info.plist'); - String contents = infoPlist.readAsStringSync(); - - // Since I didn't want to add the XML package as a dependency just for this, - // I just used a regular expression to make this simple change. - final RegExp findRe = RegExp(r'(\s*DocSetPlatformFamily\s*)[^<]+()', multiLine: true); - contents = contents.replaceAllMapped(findRe, (Match match) { - return '${match.group(1)}dartlang${match.group(2)}'; - }); - infoPlist.writeAsStringSync(contents); -} diff --git a/dev/docs/platform_integration/lib/ios.dart b/dev/docs/platform_integration/lib/ios.dart index 9645423e36fb8..92f8d0fc9629a 100644 --- a/dev/docs/platform_integration/lib/ios.dart +++ b/dev/docs/platform_integration/lib/ios.dart @@ -2,5 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// [Flutter platform integration APIs for iOS.](https://api.flutter.dev/objcdoc/) +/// [Flutter platform integration APIs for iOS.](https://api.flutter.dev/ios-embedder/) library iOS; diff --git a/dev/docs/platform_integration/lib/linux.dart b/dev/docs/platform_integration/lib/linux.dart new file mode 100644 index 0000000000000..97512455eca6f --- /dev/null +++ b/dev/docs/platform_integration/lib/linux.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// [Flutter platform integration APIs for Linux.](https://api.flutter.dev/linux-embedder/) +library Linux; diff --git a/dev/docs/platform_integration/lib/macos.dart b/dev/docs/platform_integration/lib/macos.dart new file mode 100644 index 0000000000000..1d701ea9d17ea --- /dev/null +++ b/dev/docs/platform_integration/lib/macos.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// [Flutter platform integration APIs for macOS.](https://api.flutter.dev/macos-embedder/) +library macOS; diff --git a/dev/docs/platform_integration/lib/windows.dart b/dev/docs/platform_integration/lib/windows.dart new file mode 100644 index 0000000000000..f92522f6d0110 --- /dev/null +++ b/dev/docs/platform_integration/lib/windows.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// [Flutter platform integration APIs for Windows.](https://api.flutter.dev/windows-embedder/) +library Windows; diff --git a/dev/docs/platform_integration/pubspec.yaml b/dev/docs/platform_integration/pubspec.yaml index 62caada10c640..ebe28969f054a 100644 --- a/dev/docs/platform_integration/pubspec.yaml +++ b/dev/docs/platform_integration/pubspec.yaml @@ -1,4 +1,4 @@ name: platform_integration environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' diff --git a/dev/docs/renderers/lib/impeller.dart b/dev/docs/renderers/lib/impeller.dart new file mode 100644 index 0000000000000..86f04ee65c4ea --- /dev/null +++ b/dev/docs/renderers/lib/impeller.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// [Flutter APIs for the Impeller renderer.](https://api.flutter.dev/impeller/) +library Impeller; diff --git a/dev/docs/renderers/pubspec.yaml b/dev/docs/renderers/pubspec.yaml new file mode 100644 index 0000000000000..76bdb8ce03b0d --- /dev/null +++ b/dev/docs/renderers/pubspec.yaml @@ -0,0 +1,4 @@ +name: renderers + +environment: + sdk: '>=3.2.0-0 <4.0.0' diff --git a/dev/forbidden_from_release_tests/pubspec.yaml b/dev/forbidden_from_release_tests/pubspec.yaml index 38a767f90b6d1..cc3e527d1fd21 100644 --- a/dev/forbidden_from_release_tests/pubspec.yaml +++ b/dev/forbidden_from_release_tests/pubspec.yaml @@ -2,7 +2,7 @@ name: forbidden_from_release_tests publish_to: 'none' environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: args: 2.4.2 @@ -12,8 +12,8 @@ dependencies: process: 4.2.4 vm_snapshot_analysis: 0.7.6 - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 5e6b +# PUBSPEC CHECKSUM: 4c93 diff --git a/dev/integration_tests/abstract_method_smoke_test/android/build.gradle b/dev/integration_tests/abstract_method_smoke_test/android/build.gradle index a2a8866952986..20b59171a855c 100644 --- a/dev/integration_tests/abstract_method_smoke_test/android/build.gradle +++ b/dev/integration_tests/abstract_method_smoke_test/android/build.gradle @@ -14,7 +14,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/dev/integration_tests/abstract_method_smoke_test/android/buildscript-gradle.lockfile b/dev/integration_tests/abstract_method_smoke_test/android/buildscript-gradle.lockfile index eb605802d98f5..135d58bac49a3 100644 --- a/dev/integration_tests/abstract_method_smoke_test/android/buildscript-gradle.lockfile +++ b/dev/integration_tests/abstract_method_smoke_test/android/buildscript-gradle.lockfile @@ -1,45 +1,46 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -androidx.databinding:databinding-common:7.3.0=classpath -androidx.databinding:databinding-compiler-common:7.3.0=classpath -com.android.databinding:baseLibrary:7.3.0=classpath -com.android.tools.analytics-library:crash:30.3.0=classpath -com.android.tools.analytics-library:protos:30.3.0=classpath -com.android.tools.analytics-library:shared:30.3.0=classpath -com.android.tools.analytics-library:tracker:30.3.0=classpath +androidx.databinding:databinding-common:7.4.2=classpath +androidx.databinding:databinding-compiler-common:7.4.2=classpath +com.android.databinding:baseLibrary:7.4.2=classpath +com.android.tools.analytics-library:crash:30.4.2=classpath +com.android.tools.analytics-library:protos:30.4.2=classpath +com.android.tools.analytics-library:shared:30.4.2=classpath +com.android.tools.analytics-library:tracker:30.4.2=classpath com.android.tools.build.jetifier:jetifier-core:1.0.0-beta10=classpath com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta10=classpath -com.android.tools.build:aapt2-proto:7.3.0-8691043=classpath -com.android.tools.build:aaptcompiler:7.3.0=classpath -com.android.tools.build:apksig:7.3.0=classpath -com.android.tools.build:apkzlib:7.3.0=classpath -com.android.tools.build:builder-model:7.3.0=classpath -com.android.tools.build:builder-test-api:7.3.0=classpath -com.android.tools.build:builder:7.3.0=classpath -com.android.tools.build:bundletool:1.9.0=classpath -com.android.tools.build:gradle-api:7.3.0=classpath -com.android.tools.build:gradle:7.3.0=classpath -com.android.tools.build:manifest-merger:30.3.0=classpath +com.android.tools.build:aapt2-proto:7.4.2-8841542=classpath +com.android.tools.build:aaptcompiler:7.4.2=classpath +com.android.tools.build:apksig:7.4.2=classpath +com.android.tools.build:apkzlib:7.4.2=classpath +com.android.tools.build:builder-model:7.4.2=classpath +com.android.tools.build:builder-test-api:7.4.2=classpath +com.android.tools.build:builder:7.4.2=classpath +com.android.tools.build:bundletool:1.11.4=classpath +com.android.tools.build:gradle-api:7.4.2=classpath +com.android.tools.build:gradle-settings-api:7.4.2=classpath +com.android.tools.build:gradle:7.4.2=classpath +com.android.tools.build:manifest-merger:30.4.2=classpath com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api=classpath -com.android.tools.ddms:ddmlib:30.3.0=classpath -com.android.tools.layoutlib:layoutlib-api:30.3.0=classpath -com.android.tools.lint:lint-model:30.3.0=classpath -com.android.tools.lint:lint-typedef-remover:30.3.0=classpath -com.android.tools.utp:android-device-provider-ddmlib-proto:30.3.0=classpath -com.android.tools.utp:android-device-provider-gradle-proto:30.3.0=classpath -com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.3.0=classpath -com.android.tools.utp:android-test-plugin-host-coverage-proto:30.3.0=classpath -com.android.tools.utp:android-test-plugin-host-retention-proto:30.3.0=classpath -com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.3.0=classpath -com.android.tools:annotations:30.3.0=classpath -com.android.tools:common:30.3.0=classpath -com.android.tools:dvlib:30.3.0=classpath -com.android.tools:repository:30.3.0=classpath -com.android.tools:sdk-common:30.3.0=classpath -com.android.tools:sdklib:30.3.0=classpath -com.android:signflinger:7.3.0=classpath -com.android:zipflinger:7.3.0=classpath +com.android.tools.ddms:ddmlib:30.4.2=classpath +com.android.tools.layoutlib:layoutlib-api:30.4.2=classpath +com.android.tools.lint:lint-model:30.4.2=classpath +com.android.tools.lint:lint-typedef-remover:30.4.2=classpath +com.android.tools.utp:android-device-provider-ddmlib-proto:30.4.2=classpath +com.android.tools.utp:android-device-provider-gradle-proto:30.4.2=classpath +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.4.2=classpath +com.android.tools.utp:android-test-plugin-host-coverage-proto:30.4.2=classpath +com.android.tools.utp:android-test-plugin-host-retention-proto:30.4.2=classpath +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.4.2=classpath +com.android.tools:annotations:30.4.2=classpath +com.android.tools:common:30.4.2=classpath +com.android.tools:dvlib:30.4.2=classpath +com.android.tools:repository:30.4.2=classpath +com.android.tools:sdk-common:30.4.2=classpath +com.android.tools:sdklib:30.4.2=classpath +com.android:signflinger:7.4.2=classpath +com.android:zipflinger:7.4.2=classpath com.github.gundy:semver4j:0.16.4=classpath com.google.android:annotations:4.1.1.4=classpath com.google.api.grpc:proto-google-common-protos:2.0.1=classpath @@ -57,8 +58,7 @@ com.google.j2objc:j2objc-annotations:1.3=classpath com.google.jimfs:jimfs:1.1=classpath com.google.protobuf:protobuf-java-util:3.17.2=classpath com.google.protobuf:protobuf-java:3.17.2=classpath -com.google.testing.platform:core-proto:0.0.8-alpha07=classpath -com.googlecode.json-simple:json-simple:1.1=classpath +com.google.testing.platform:core-proto:0.0.8-alpha08=classpath com.googlecode.juniversalchardet:juniversalchardet:1.0.3=classpath com.squareup:javapoet:1.10.0=classpath com.squareup:javawriter:2.5.0=classpath @@ -87,7 +87,6 @@ io.netty:netty-handler:4.1.52.Final=classpath io.netty:netty-resolver:4.1.52.Final=classpath io.netty:netty-transport:4.1.52.Final=classpath io.perfmark:perfmark-api:0.23.0=classpath -it.unimi.dsi:fastutil:8.4.0=classpath jakarta.activation:jakarta.activation-api:1.2.1=classpath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=classpath javax.annotation:javax.annotation-api:1.3.2=classpath @@ -123,15 +122,15 @@ org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10=classpath org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.7.10=classpath org.jetbrains.kotlin:kotlin-native-utils:1.7.10=classpath org.jetbrains.kotlin:kotlin-project-model:1.7.10=classpath -org.jetbrains.kotlin:kotlin-reflect:1.5.31=classpath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=classpath org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=classpath org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=classpath org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=classpath org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=classpath -org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib:1.5.31=classpath +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=classpath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=classpath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=classpath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=classpath org.jetbrains.kotlin:kotlin-tooling-core:1.7.10=classpath org.jetbrains.kotlin:kotlin-tooling-metadata:1.7.10=classpath org.jetbrains.kotlin:kotlin-util-io:1.7.10=classpath @@ -140,11 +139,11 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath org.jetbrains:annotations:13.0=classpath org.json:json:20180813=classpath org.jvnet.staxex:stax-ex:1.8.1=classpath -org.ow2.asm:asm-analysis:9.1=classpath -org.ow2.asm:asm-commons:9.1=classpath -org.ow2.asm:asm-tree:9.1=classpath -org.ow2.asm:asm-util:9.1=classpath -org.ow2.asm:asm:9.1=classpath +org.ow2.asm:asm-analysis:9.2=classpath +org.ow2.asm:asm-commons:9.2=classpath +org.ow2.asm:asm-tree:9.2=classpath +org.ow2.asm:asm-util:9.2=classpath +org.ow2.asm:asm:9.2=classpath org.slf4j:slf4j-api:1.7.30=classpath org.tensorflow:tensorflow-lite-metadata:0.1.0-rc2=classpath xerces:xercesImpl:2.12.0=classpath diff --git a/dev/integration_tests/abstract_method_smoke_test/android/gradle.properties b/dev/integration_tests/abstract_method_smoke_test/android/gradle.properties index 08f2b5f91bff6..f17eebabc3990 100644 --- a/dev/integration_tests/abstract_method_smoke_test/android/gradle.properties +++ b/dev/integration_tests/abstract_method_smoke_test/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableJetifier=true android.useAndroidX=true diff --git a/dev/integration_tests/abstract_method_smoke_test/android/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/abstract_method_smoke_test/android/gradle/wrapper/gradle-wrapper.properties index cb24abda10ae7..3c472b99c6f35 100644 --- a/dev/integration_tests/abstract_method_smoke_test/android/gradle/wrapper/gradle-wrapper.properties +++ b/dev/integration_tests/abstract_method_smoke_test/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/dev/integration_tests/abstract_method_smoke_test/pubspec.yaml b/dev/integration_tests/abstract_method_smoke_test/pubspec.yaml index 737b9e663c40c..60ef22f5567c6 100644 --- a/dev/integration_tests/abstract_method_smoke_test/pubspec.yaml +++ b/dev/integration_tests/abstract_method_smoke_test/pubspec.yaml @@ -4,20 +4,20 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: a9c0 +# PUBSPEC CHECKSUM: 081a diff --git a/dev/integration_tests/android_custom_host_app/SampleApp/build.gradle b/dev/integration_tests/android_custom_host_app/SampleApp/build.gradle index 9d8b6ebcd18ca..a05c46a0a3483 100644 --- a/dev/integration_tests/android_custom_host_app/SampleApp/build.gradle +++ b/dev/integration_tests/android_custom_host_app/SampleApp/build.gradle @@ -15,7 +15,7 @@ android { defaultConfig { applicationId "io.flutter.add2app" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 1 versionName "1.0" } diff --git a/dev/integration_tests/android_custom_host_app/gradle.properties b/dev/integration_tests/android_custom_host_app/gradle.properties index 759a1767410a2..7413f6ce06495 100644 --- a/dev/integration_tests/android_custom_host_app/gradle.properties +++ b/dev/integration_tests/android_custom_host_app/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true flutter.hostAppProjectName=SampleApp diff --git a/dev/integration_tests/android_embedding_v2_smoke_test/android/gradle.properties b/dev/integration_tests/android_embedding_v2_smoke_test/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/android_embedding_v2_smoke_test/android/gradle.properties +++ b/dev/integration_tests/android_embedding_v2_smoke_test/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/android_embedding_v2_smoke_test/pubspec.yaml b/dev/integration_tests/android_embedding_v2_smoke_test/pubspec.yaml index 5b52b760c400d..ef0bf621f8f40 100644 --- a/dev/integration_tests/android_embedding_v2_smoke_test/pubspec.yaml +++ b/dev/integration_tests/android_embedding_v2_smoke_test/pubspec.yaml @@ -14,7 +14,7 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -25,16 +25,16 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 battery_platform_interface: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -52,11 +52,11 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -96,4 +96,4 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -# PUBSPEC CHECKSUM: d514 +# PUBSPEC CHECKSUM: 9073 diff --git a/dev/integration_tests/android_host_app_v2_embedding/app/build.gradle b/dev/integration_tests/android_host_app_v2_embedding/app/build.gradle index f653bc3792fca..c23b1b6db927c 100644 --- a/dev/integration_tests/android_host_app_v2_embedding/app/build.gradle +++ b/dev/integration_tests/android_host_app_v2_embedding/app/build.gradle @@ -15,7 +15,7 @@ android { defaultConfig { applicationId "io.flutter.add2app" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 1 versionName "1.0" } diff --git a/dev/integration_tests/android_host_app_v2_embedding/gradle.properties b/dev/integration_tests/android_host_app_v2_embedding/gradle.properties index 47a56de84bd00..598d13fee4463 100644 --- a/dev/integration_tests/android_host_app_v2_embedding/gradle.properties +++ b/dev/integration_tests/android_host_app_v2_embedding/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/android_semantics_testing/android/gradle.properties b/dev/integration_tests/android_semantics_testing/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/android_semantics_testing/android/gradle.properties +++ b/dev/integration_tests/android_semantics_testing/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/android_semantics_testing/pubspec.yaml b/dev/integration_tests/android_semantics_testing/pubspec.yaml index f50c94031bd94..193980c8007cb 100644 --- a/dev/integration_tests/android_semantics_testing/pubspec.yaml +++ b/dev/integration_tests/android_semantics_testing/pubspec.yaml @@ -1,7 +1,7 @@ name: android_semantics_testing description: Integration testing library for Android semantics environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -11,16 +11,16 @@ dependencies: flutter_test: sdk: flutter pub_semver: 2.1.4 - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -35,7 +35,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -48,22 +48,22 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3549 +# PUBSPEC CHECKSUM: aaa7 diff --git a/dev/integration_tests/android_views/android/gradle.properties b/dev/integration_tests/android_views/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/android_views/android/gradle.properties +++ b/dev/integration_tests/android_views/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/android_views/lib/motion_events_page.dart b/dev/integration_tests/android_views/lib/motion_events_page.dart index 1b4f6cc6a2cbf..740aa6041f017 100644 --- a/dev/integration_tests/android_views/lib/motion_events_page.dart +++ b/dev/integration_tests/android_views/lib/motion_events_page.dart @@ -236,7 +236,6 @@ class MotionEventsBodyState extends State { flutterViewEvents.removeLast(); } setState(() {}); - break; } return Future.value(); } @@ -250,7 +249,6 @@ class MotionEventsBodyState extends State { embeddedViewEvents.removeLast(); } setState(() {}); - break; } return Future.value(); } diff --git a/dev/integration_tests/android_views/pubspec.yaml b/dev/integration_tests/android_views/pubspec.yaml index 3d17c7d390563..891258bf5eae1 100644 --- a/dev/integration_tests/android_views/pubspec.yaml +++ b/dev/integration_tests/android_views/pubspec.yaml @@ -4,18 +4,18 @@ publish_to: none description: An integration test for embedded platform views version: 1.0.0+1 environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter flutter_driver: sdk: flutter - path_provider: 2.0.15 + path_provider: 2.1.1 # This made non-transitive to allow exact pinning # https://github.com/flutter/flutter/issues/116376 - path_provider_android: 2.0.27 - collection: 1.17.2 + path_provider_android: 2.2.0 + collection: 1.18.0 assets_for_android_views: git: url: https://github.com/flutter/goldens.git @@ -25,40 +25,39 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - ffi: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_foundation: 2.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_linux: 2.1.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_platform_interface: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_windows: 2.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_foundation: 2.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_linux: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_platform_interface: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_windows: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - win32: 5.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - xdg_directories: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + win32: 5.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xdg_directories: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -83,14 +82,14 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 6876 +# PUBSPEC CHECKSUM: 15e5 diff --git a/dev/integration_tests/channels/android/gradle.properties b/dev/integration_tests/channels/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/channels/android/gradle.properties +++ b/dev/integration_tests/channels/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/channels/integration_test/main_test.dart b/dev/integration_tests/channels/integration_test/main_test.dart index 2ec03ce92ee18..438ad8aa235ca 100644 --- a/dev/integration_tests/channels/integration_test/main_test.dart +++ b/dev/integration_tests/channels/integration_test/main_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:channels/main.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -14,9 +13,6 @@ String getStatus(WidgetTester tester) => tester.widget(statusField).data!; void main() { testWidgets('step through', (WidgetTester tester) async { - // TODO(goderbauer): Remove this once https://github.com/flutter/flutter/issues/116663 is diagnosed. - debugPrintHitTestResults = true; - await tester.pumpWidget(const TestApp()); await tester.pumpAndSettle(); @@ -37,9 +33,6 @@ void main() { } } - // TODO(goderbauer): Remove this once https://github.com/flutter/flutter/issues/116663 is diagnosed. - debugPrintHitTestResults = false; - final String status = getStatus(tester); if (status != 'complete') { fail('Failed at step $step with status $status'); diff --git a/dev/integration_tests/channels/lib/src/test_step.dart b/dev/integration_tests/channels/lib/src/test_step.dart index 7c34e9622e9f9..443db5a6fa669 100644 --- a/dev/integration_tests/channels/lib/src/test_step.dart +++ b/dev/integration_tests/channels/lib/src/test_step.dart @@ -69,8 +69,7 @@ class TestStepResult { ); Widget asWidget(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ListView( children: [ Text('Step: $name', style: bold), Text(description), diff --git a/dev/integration_tests/channels/pubspec.yaml b/dev/integration_tests/channels/pubspec.yaml index dbaf46734172a..0b4e90a193f94 100644 --- a/dev/integration_tests/channels/pubspec.yaml +++ b/dev/integration_tests/channels/pubspec.yaml @@ -2,18 +2,18 @@ name: channels description: Integration test for platform channels. environment: - sdk: '>=2.19.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: integration_test: @@ -31,16 +31,16 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3234 +# PUBSPEC CHECKSUM: 53b9 diff --git a/dev/integration_tests/deferred_components_test/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/deferred_components_test/android/app/src/main/AndroidManifest.xml index 7ba934d1e32da..c43b90e80fc8f 100644 --- a/dev/integration_tests/deferred_components_test/android/app/src/main/AndroidManifest.xml +++ b/dev/integration_tests/deferred_components_test/android/app/src/main/AndroidManifest.xml @@ -28,15 +28,6 @@ found in the LICENSE file. --> android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> - - diff --git a/dev/integration_tests/deferred_components_test/android/component1/build.gradle b/dev/integration_tests/deferred_components_test/android/component1/build.gradle index 728ff955c1aa0..3cf18c577423a 100644 --- a/dev/integration_tests/deferred_components_test/android/component1/build.gradle +++ b/dev/integration_tests/deferred_components_test/android/component1/build.gradle @@ -24,7 +24,7 @@ apply plugin: "com.android.dynamic-feature" android { namespace "io.flutter.integration.deferred_components_test.component1" - compileSdkVersion 31 + compileSdkVersion 33 sourceSets { applicationVariants.all { variant -> @@ -35,7 +35,7 @@ android { defaultConfig { minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/dev/integration_tests/deferred_components_test/android/gradle.properties b/dev/integration_tests/deferred_components_test/android/gradle.properties index 507f4a3fcd52a..e5a6f71ad43f8 100644 --- a/dev/integration_tests/deferred_components_test/android/gradle.properties +++ b/dev/integration_tests/deferred_components_test/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/dev/integration_tests/deferred_components_test/pubspec.yaml b/dev/integration_tests/deferred_components_test/pubspec.yaml index fa45b4baee41e..37a3d05d33e02 100644 --- a/dev/integration_tests/deferred_components_test/pubspec.yaml +++ b/dev/integration_tests/deferred_components_test/pubspec.yaml @@ -3,7 +3,7 @@ description: Integration test application for basic deferred components function publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -14,31 +14,31 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -63,11 +63,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -80,4 +80,4 @@ flutter: assets: - customassets/flutter_logo.png -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/dev/integration_tests/external_ui/android/gradle.properties b/dev/integration_tests/external_ui/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/external_ui/android/gradle.properties +++ b/dev/integration_tests/external_ui/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/external_ui/pubspec.yaml b/dev/integration_tests/external_ui/pubspec.yaml index 63f9692033a55..19f981355470b 100644 --- a/dev/integration_tests/external_ui/pubspec.yaml +++ b/dev/integration_tests/external_ui/pubspec.yaml @@ -2,22 +2,22 @@ name: external_ui description: A test of Flutter integrating external UIs. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -31,7 +31,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -45,24 +45,24 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3dd1 +# PUBSPEC CHECKSUM: 4930 diff --git a/dev/integration_tests/flavors/android/gradle.properties b/dev/integration_tests/flavors/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/flavors/android/gradle.properties +++ b/dev/integration_tests/flavors/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/flavors/integration_test/integration_test.dart b/dev/integration_tests/flavors/integration_test/integration_test.dart index d2cdd10dd4ec9..4fda3ce79bb4c 100644 --- a/dev/integration_tests/flavors/integration_test/integration_test.dart +++ b/dev/integration_tests/flavors/integration_test/integration_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flavors/main.dart' as app; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -16,6 +17,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('paid'), findsOneWidget); + expect(appFlavor, 'paid'); }); }); } diff --git a/dev/integration_tests/flavors/pubspec.yaml b/dev/integration_tests/flavors/pubspec.yaml index 7e6e37466ed81..dcb4d71a0bb52 100644 --- a/dev/integration_tests/flavors/pubspec.yaml +++ b/dev/integration_tests/flavors/pubspec.yaml @@ -2,7 +2,7 @@ name: flavors description: Integration test for build flavors. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -11,15 +11,15 @@ dependencies: sdk: flutter integration_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -33,7 +33,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -47,21 +47,21 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: @@ -74,4 +74,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/dev/integration_tests/flutter_gallery/.gitignore b/dev/integration_tests/flutter_gallery/.gitignore index a61bb46ed60fe..d8571249c6b57 100644 --- a/dev/integration_tests/flutter_gallery/.gitignore +++ b/dev/integration_tests/flutter_gallery/.gitignore @@ -1,3 +1,2 @@ -lib/generated_plugin_registrant.dart vmservice.out *.sksl.json diff --git a/dev/integration_tests/flutter_gallery/android/gradle.properties b/dev/integration_tests/flutter_gallery/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/flutter_gallery/android/gradle.properties +++ b/dev/integration_tests/flutter_gallery/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/flutter_gallery/android/project-app.lockfile b/dev/integration_tests/flutter_gallery/android/project-app.lockfile index 01747922649a5..c5040659c70d5 100644 --- a/dev/integration_tests/flutter_gallery/android/project-app.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-app.lockfile @@ -2,19 +2,28 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. androidx.activity:activity:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.annotation:annotation-experimental:1.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.annotation:annotation:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-experimental:1.1.0=debugAndroidTestCompileClasspath +androidx.annotation:annotation-experimental:1.3.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-jvm:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.arch.core:core-common:2.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.arch.core:core-runtime:2.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.arch.core:core-runtime:2.0.0=debugAndroidTestCompileClasspath +androidx.arch.core:core-runtime:2.1.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.browser:browser:1.5.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection:1.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.core:core:1.6.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.concurrent:concurrent-futures:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.core:core:1.10.1=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.core:core:1.6.0=debugAndroidTestCompileClasspath androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.fragment:fragment:1.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-common-java8:2.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-common:2.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.2.0=debugAndroidTestCompileClasspath +androidx.lifecycle:lifecycle-common:2.3.1=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-livedata-core:2.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-livedata:2.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath +androidx.lifecycle:lifecycle-runtime:2.3.1=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.multidex:multidex-instrumentation:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath @@ -56,14 +65,17 @@ org.jacoco:org.jacoco.agent:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.ant:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.core:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.report:0.8.7=androidJacocoAnt -org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib:1.5.31=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-bom:1.8.22=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30=debugAndroidTestCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30=debugAndroidTestCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.1=androidJacocoAnt org.ow2.asm:asm-commons:9.1=androidJacocoAnt org.ow2.asm:asm-tree:9.1=androidJacocoAnt diff --git a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile index f45da3d417cf0..1326b8773fd76 100644 --- a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile @@ -37,6 +37,8 @@ com.google.j2objc:j2objc-annotations:1.3=debugAndroidTestCompileClasspath,debugA com.squareup:javawriter:2.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath javax.inject:javax.inject:1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath junit:junit:4.12=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath net.sf.kxml:kxml2:2.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.checkerframework:checker-compat-qual:2.5.5=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.codehaus.mojo:animal-sniffer-annotations:1.18=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -55,6 +57,9 @@ org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2=debugAndroidTestCompileCl org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.mockito:mockito-core:5.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.mockito:mockito-inline:5.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.objenesis:objenesis:3.3=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.1=androidJacocoAnt org.ow2.asm:asm-commons:9.1=androidJacocoAnt org.ow2.asm:asm-tree:9.1=androidJacocoAnt diff --git a/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile b/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile index 6831d08f57f4a..796a30231af53 100644 --- a/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile @@ -2,104 +2,89 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. androidx.activity:activity:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.annotation:annotation-experimental:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.annotation:annotation:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-experimental:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-jvm:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.arch.core:core-common:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.arch.core:core-runtime:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.arch.core:core-runtime:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.browser:browser:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.core:core:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.concurrent:concurrent-futures:1.0.0=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.core:core:1.10.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.fragment:fragment:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-common-java8:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.3.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-livedata-core:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-livedata:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.3.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.5.1=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath +androidx.test:annotation:1.0.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:core:1.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.6.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.window:window-java:1.0.0-beta04=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.window:window:1.0.0-beta04=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -backport-util-concurrent:backport-util-concurrent:3.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -classworlds:classworlds:1.1-alpha-2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.almworks.sqlite4java:sqlite4java:0.282=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.auto.service:auto-service:1.0-rc4=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.auto:auto-common:0.8=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.almworks.sqlite4java:sqlite4java:1.0.392=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.auto.value:auto-value-annotations:1.10.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.errorprone:error_prone_annotations:2.2.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath com.google.guava:failureaccess:1.0.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.guava:guava:27.0.1-jre=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:guava:31.1-jre=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.j2objc:j2objc-annotations:1.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.protobuf:protobuf-java:2.6.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.ibm.icu:icu4j:53.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.j2objc:j2objc-annotations:1.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.ibm.icu:icu4j:72.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath javax.annotation:javax.annotation-api:1.3.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath javax.inject:javax.inject:1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -junit:junit:4.12=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -nekohtml:nekohtml:1.9.6.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -nekohtml:xercesMinimal:1.9.6.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.ant:ant-launcher:1.8.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.ant:ant:1.8.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven.wagon:wagon-file:1.0-beta-6=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven.wagon:wagon-http-lightweight:1.0-beta-6=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven.wagon:wagon-http-shared:1.0-beta-6=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven.wagon:wagon-provider-api:1.0-beta-6=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-ant-tasks:2.1.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-artifact-manager:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-artifact:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-error-diagnostics:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-model:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-plugin-registry:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-profile:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-project:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-repository-metadata:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.apache.maven:maven-settings:2.2.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.bouncycastle:bcprov-jdk15on:1.52=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.checkerframework:checker-qual:2.5.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.codehaus.mojo:animal-sniffer-annotations:1.17=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.codehaus.plexus:plexus-container-default:1.0-alpha-9-stable-1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.codehaus.plexus:plexus-interpolation:1.11=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.codehaus.plexus:plexus-utils:1.5.15=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.72=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.checkerframework:checker-qual:3.12.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.conscrypt:conscrypt-openjdk-uber:2.5.2=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.hamcrest:hamcrest-library:1.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jacoco:org.jacoco.agent:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.ant:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.core:0.8.7=androidJacocoAnt org.jacoco:org.jacoco.report:0.8.7=androidJacocoAnt -org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib:1.5.31=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-bom:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.8.22=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.mockito:mockito-core:1.10.19=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.objenesis:objenesis:2.1=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath -org.ow2.asm:asm-analysis:7.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.mockito:mockito-core:5.1.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.objenesis:objenesis:3.3=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.1=androidJacocoAnt -org.ow2.asm:asm-commons:7.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.ow2.asm:asm-analysis:9.5=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-commons:9.1=androidJacocoAnt -org.ow2.asm:asm-tree:7.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.ow2.asm:asm-commons:9.5=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-tree:9.1=androidJacocoAnt -org.ow2.asm:asm-util:7.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.ow2.asm:asm:7.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.ow2.asm:asm-tree:9.5=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.ow2.asm:asm-util:9.5=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm:9.1=androidJacocoAnt -org.robolectric:annotations:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:junit:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:pluginapi:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:plugins-maven-dependency-resolver:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:resources:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:robolectric:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:sandbox:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:shadowapi:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:shadows-framework:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:utils-reflector:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.robolectric:utils:4.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.ow2.asm:asm:9.5=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:annotations:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:junit:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:nativeruntime-dist-compat:1.0.1=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:nativeruntime:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:pluginapi:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:plugins-maven-dependency-resolver:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:resources:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:robolectric:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:sandbox:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:shadowapi:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:shadows-framework:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:utils-reflector:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.robolectric:utils:4.10.3=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAnnotationProcessorClasspath,debugUnitTestAnnotationProcessorClasspath,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileUnitTestAnnotationProcessorClasspath,releaseAnnotationProcessorClasspath,releaseUnitTestAnnotationProcessorClasspath diff --git a/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart index f6626b85e20dd..a68c2a194c9b7 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart @@ -48,9 +48,9 @@ class CupertinoNavigationDemo extends StatelessWidget { @override Widget build(BuildContext context) { - return WillPopScope( + return PopScope( // Prevent swipe popping of this page. Use explicit exit buttons only. - onWillPop: () => Future.value(true), + canPop: false, child: DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, child: CupertinoTabScaffold( diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart index fa7554e27a833..c22519d647cf3 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/date_and_time_picker_demo.dart @@ -62,7 +62,7 @@ class _DateTimePicker extends StatelessWidget { Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, - initialDate: selectedDate!, + initialDate: selectedDate, firstDate: DateTime(2015, 8), lastDate: DateTime(2101), ); @@ -120,9 +120,9 @@ class DateAndTimePickerDemo extends StatefulWidget { } class _DateAndTimePickerDemoState extends State { - DateTime _fromDate = DateTime.now(); + DateTime? _fromDate = DateTime.now(); TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28); - DateTime _toDate = DateTime.now(); + DateTime? _toDate = DateTime.now(); TimeOfDay _toTime = const TimeOfDay(hour: 8, minute: 28); final List _allActivities = ['hiking', 'swimming', 'boating', 'fishing']; String? _activity = 'fishing'; diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart index b6f7bbe6b6afb..81ee4dacd8643 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; // This demo is based on @@ -109,16 +110,15 @@ class FullScreenDialogDemoState extends State { bool _hasName = false; late String _eventName; - Future _onWillPop() async { - _saveNeeded = _hasLocation || _hasName || _saveNeeded; - if (!_saveNeeded) { - return true; + Future _handlePopInvoked(bool didPop) async { + if (didPop) { + return; } final ThemeData theme = Theme.of(context); final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color); - return showDialog( + final bool? shouldDiscard = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( @@ -130,19 +130,31 @@ class FullScreenDialogDemoState extends State { TextButton( child: const Text('CANCEL'), onPressed: () { - Navigator.of(context).pop(false); // Pops the confirmation dialog but not the page. + // Pop the confirmation dialog and indicate that the page should + // not be popped. + Navigator.of(context).pop(false); }, ), TextButton( child: const Text('DISCARD'), onPressed: () { - Navigator.of(context).pop(true); // Returning true to _onWillPop will pop again. + // Pop the confirmation dialog and indicate that the page should + // be popped, too. + Navigator.of(context).pop(true); }, ), ], ); }, - ) as Future; + ); + + if (shouldDiscard ?? false) { + // Since this is the root route, quit the app where possible by invoking + // the SystemNavigator. If this wasn't the root route, then + // Navigator.maybePop could be used instead. + // See https://github.com/flutter/flutter/issues/11490 + SystemNavigator.pop(); + } } @override @@ -162,7 +174,8 @@ class FullScreenDialogDemoState extends State { ], ), body: Form( - onWillPop: _onWillPop, + canPop: !_saveNeeded && !_hasLocation && !_hasName, + onPopInvoked: _handlePopInvoked, child: Scrollbar( child: ListView( primary: true, diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart index 5d3fee8d60f0c..c6f644ee74cd3 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart @@ -143,10 +143,9 @@ class TextFormFieldDemoState extends State { return null; } - Future _warnUserAboutInvalidData() async { - final FormState? form = _formKey.currentState; - if (form == null || !_formWasEdited || form.validate()) { - return true; + Future _handlePopInvoked(bool didPop) async { + if (didPop) { + return; } final bool? result = await showDialog( @@ -168,7 +167,14 @@ class TextFormFieldDemoState extends State { ); }, ); - return result!; + + if (result ?? false) { + // Since this is the root route, quit the app where possible by invoking + // the SystemNavigator. If this wasn't the root route, then + // Navigator.maybePop could be used instead. + // See https://github.com/flutter/flutter/issues/11490 + SystemNavigator.pop(); + } } @override @@ -185,7 +191,8 @@ class TextFormFieldDemoState extends State { child: Form( key: _formKey, autovalidateMode: _autovalidateMode, - onWillPop: _warnUserAboutInvalidData, + canPop: _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(), + onPopInvoked: _handlePopInvoked, child: Scrollbar( child: SingleChildScrollView( primary: true, diff --git a/dev/integration_tests/flutter_gallery/lib/demo/pesto_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/pesto_demo.dart index fd36fe6c6917c..5d40dbeaf8228 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/pesto_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/pesto_demo.dart @@ -23,8 +23,9 @@ const double _kRecipePageMaxWidth = 500.0; final Set _favoriteRecipes = {}; final ThemeData _kTheme = ThemeData( + appBarTheme: const AppBarTheme(foregroundColor: Colors.white, backgroundColor: Colors.teal), brightness: Brightness.light, - primarySwatch: Colors.teal, + floatingActionButtonTheme: const FloatingActionButtonThemeData(foregroundColor: Colors.white), ); class PestoHome extends StatelessWidget { diff --git a/dev/integration_tests/flutter_gallery/lib/demo/shrine/app.dart b/dev/integration_tests/flutter_gallery/lib/demo/shrine/app.dart index dd9db3b926ecd..0b4ff755f49a7 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/shrine/app.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/shrine/app.dart @@ -92,6 +92,7 @@ ThemeData _buildShrineTheme() { textTheme: _buildShrineTextTheme(base.textTheme), primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme), iconTheme: _customIconTheme(base.iconTheme), + appBarTheme: const AppBarTheme(backgroundColor: kShrinePink100), ); } diff --git a/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart b/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart index 0391ef95bee47..de2cfa43ef714 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:scoped_model/scoped_model.dart'; import 'colors.dart'; @@ -361,14 +360,12 @@ class ExpandingBottomSheetState extends State with TickerP // Closes the cart if the cart is open, otherwise exits the app (this should // only be relevant for Android). - Future _onWillPop() async { - if (!_isOpen) { - await SystemNavigator.pop(); - return true; + void _handlePopInvoked(bool didPop) { + if (didPop) { + return; } close(); - return true; } @override @@ -378,8 +375,9 @@ class ExpandingBottomSheetState extends State with TickerP duration: const Duration(milliseconds: 225), curve: Curves.easeInOut, alignment: FractionalOffset.topLeft, - child: WillPopScope( - onWillPop: _onWillPop, + child: PopScope( + canPop: !_isOpen, + onPopInvoked: _handlePopInvoked, child: AnimatedBuilder( animation: widget.hideController, builder: _buildSlideAnimation, diff --git a/dev/integration_tests/flutter_gallery/lib/demo/shrine/login.dart b/dev/integration_tests/flutter_gallery/lib/demo/shrine/login.dart index ba11ceaa0bc65..c78279068a169 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/shrine/login.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/shrine/login.dart @@ -99,7 +99,7 @@ class _LoginPageState extends State { // of Shrine completely. Navigator.of(context, rootNavigator: true).pop(); }, - child: const Text('CANCEL'), + child: Text('CANCEL', style: Theme.of(context).textTheme.bodySmall), ), ElevatedButton( style: ElevatedButton.styleFrom( @@ -111,7 +111,7 @@ class _LoginPageState extends State { onPressed: () { Navigator.pop(context); }, - child: const Text('NEXT'), + child: Text('NEXT', style: Theme.of(context).textTheme.bodySmall), ), ], ), diff --git a/dev/integration_tests/flutter_gallery/lib/demo/shrine/supplemental/product_card.dart b/dev/integration_tests/flutter_gallery/lib/demo/shrine/supplemental/product_card.dart index b5d4083d9cbda..5a32001fed47a 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/shrine/supplemental/product_card.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/shrine/supplemental/product_card.dart @@ -52,7 +52,8 @@ class ProductCard extends StatelessWidget { child: imageWidget, ), SizedBox( - height: kTextBoxHeight * MediaQuery.of(context).textScaleFactor, + // ignore: deprecated_member_use, https://github.com/flutter/flutter/issues/128825 + height: kTextBoxHeight * MediaQuery.textScalerOf(context).textScaleFactor, width: 121.0, child: Column( mainAxisAlignment: MainAxisAlignment.end, diff --git a/dev/integration_tests/flutter_gallery/lib/gallery/app.dart b/dev/integration_tests/flutter_gallery/lib/gallery/app.dart index 788c48d7a170d..e2d8f0fb34a78 100644 --- a/dev/integration_tests/flutter_gallery/lib/gallery/app.dart +++ b/dev/integration_tests/flutter_gallery/lib/gallery/app.dart @@ -104,10 +104,10 @@ class _GalleryAppState extends State { Widget _applyTextScaleFactor(Widget child) { return Builder( builder: (BuildContext context) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: _options!.textScaleFactor!.scale, - ), + final double? textScaleFactor = _options!.textScaleFactor!.scale; + return MediaQuery.withClampedTextScaling( + minScaleFactor: textScaleFactor ?? 0.0, + maxScaleFactor: textScaleFactor ?? double.infinity, child: child, ); }, diff --git a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart index ebbce5404ecba..4a5f01fede8de 100644 --- a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart +++ b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart @@ -178,7 +178,8 @@ class _DemoItem extends StatelessWidget { Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final bool isDark = theme.brightness == Brightness.dark; - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); + // ignore: deprecated_member_use, https://github.com/flutter/flutter/issues/128825 + final double textScaleFactor = MediaQuery.textScalerOf(context).textScaleFactor; return RawMaterialButton( splashColor: theme.primaryColor.withOpacity(0.12), highlightColor: Colors.transparent, @@ -324,14 +325,14 @@ class _GalleryHomeState extends State with SingleTickerProviderStat backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, body: SafeArea( bottom: false, - child: WillPopScope( - onWillPop: () { - // Pop the category page if Android back button is pressed. - if (_category != null) { - setState(() => _category = null); - return Future.value(false); + child: PopScope( + canPop: _category == null, + onPopInvoked: (bool didPop) { + if (didPop) { + return; } - return Future.value(true); + // Pop the category page if Android back button is pressed. + setState(() => _category = null); }, child: Backdrop( backTitle: const Text('Options'), diff --git a/dev/integration_tests/flutter_gallery/lib/gallery/options.dart b/dev/integration_tests/flutter_gallery/lib/gallery/options.dart index c1c4b8314e5fe..53b542ff4eb85 100644 --- a/dev/integration_tests/flutter_gallery/lib/gallery/options.dart +++ b/dev/integration_tests/flutter_gallery/lib/gallery/options.dart @@ -100,7 +100,8 @@ class _OptionsItem extends StatelessWidget { @override Widget build(BuildContext context) { - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); + // ignore: deprecated_member_use, https://github.com/flutter/flutter/issues/128825 + final double textScaleFactor = MediaQuery.textScalerOf(context).textScaleFactor; return MergeSemantics( child: Container( diff --git a/dev/integration_tests/flutter_gallery/macos/Podfile.lock b/dev/integration_tests/flutter_gallery/macos/Podfile.lock index 7fc925bb07325..04d3145211d20 100644 --- a/dev/integration_tests/flutter_gallery/macos/Podfile.lock +++ b/dev/integration_tests/flutter_gallery/macos/Podfile.lock @@ -28,8 +28,8 @@ SPEC CHECKSUMS: connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 PODFILE CHECKSUM: 2e6060c123c393d6beb3ee5b7beaf789de4d2e47 -COCOAPODS: 1.12.0 +COCOAPODS: 1.12.1 diff --git a/dev/integration_tests/flutter_gallery/pubspec.yaml b/dev/integration_tests/flutter_gallery/pubspec.yaml index 7be18d52b30eb..e853f48a7d43e 100644 --- a/dev/integration_tests/flutter_gallery/pubspec.yaml +++ b/dev/integration_tests/flutter_gallery/pubspec.yaml @@ -1,20 +1,20 @@ name: flutter_gallery environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter - collection: 1.17.2 + collection: 1.18.0 device_info: 2.0.3 intl: 0.18.1 connectivity: 3.0.6 string_scanner: 1.2.0 - url_launcher: 6.1.11 + url_launcher: 6.1.14 # This is listed as direct so it can be manually pinned - url_launcher_android: 6.0.17 - cupertino_icons: 1.0.5 + url_launcher_android: 6.1.0 + cupertino_icons: 1.0.6 video_player: 2.2.11 scoped_model: git: @@ -35,21 +35,21 @@ dependencies: device_info_platform_interface: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" html: 0.15.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_ios: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_linux: 3.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_macos: 3.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_platform_interface: 2.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_web: 2.0.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - url_launcher_windows: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_ios: 6.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_linux: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_macos: 3.0.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_platform_interface: 2.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_web: 2.0.20 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_windows: 3.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" video_player_platform_interface: 5.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" video_player_web: 2.0.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -58,12 +58,12 @@ dev_dependencies: sdk: flutter flutter_goldens: sdk: flutter - test: 1.24.3 + test: 1.24.6 integration_test: sdk: flutter - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -83,7 +83,7 @@ dev_dependencies: mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -93,17 +93,17 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -276,4 +276,4 @@ flutter: - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf -# PUBSPEC CHECKSUM: bd6f +# PUBSPEC CHECKSUM: f6a0 diff --git a/dev/integration_tests/flutter_gallery/test/drawer_test.dart b/dev/integration_tests/flutter_gallery/test/drawer_test.dart index 3dc6e93675ab5..2341180fb64c3 100644 --- a/dev/integration_tests/flutter_gallery/test/drawer_test.dart +++ b/dev/integration_tests/flutter_gallery/test/drawer_test.dart @@ -105,7 +105,7 @@ void main() { await tester.tap(find.text('Small')); await tester.pumpAndSettle(); Size textSize = tester.getSize(find.text('Text size')); - expect(textSize, equals(const Size(116.0, 13.0))); + expect(textSize, equals(within(distance: 0.05, from: const Size(115.2, 13.0)))); // Set font scale back to the default. await tester.tap(find.byIcon(Icons.arrow_drop_down).at(1)); diff --git a/dev/integration_tests/flutter_gallery/test/live_smoketest.dart b/dev/integration_tests/flutter_gallery/test/live_smoketest.dart index 26aae16dec966..44afcc9b0d0f2 100644 --- a/dev/integration_tests/flutter_gallery/test/live_smoketest.dart +++ b/dev/integration_tests/flutter_gallery/test/live_smoketest.dart @@ -126,11 +126,11 @@ class _LiveWidgetController extends LiveWidgetController { } /// Runs `finder` repeatedly until it finds one or more [Element]s. - Future _waitForElement(Finder finder) async { + Future> _waitForElement(FinderBase finder) async { if (frameSync) { await _waitUntilFrame(() => binding.transientCallbackCount == 0); } - await _waitUntilFrame(() => finder.precache()); + await _waitUntilFrame(() => finder.tryEvaluate()); if (frameSync) { await _waitUntilFrame(() => binding.transientCallbackCount == 0); } @@ -138,12 +138,12 @@ class _LiveWidgetController extends LiveWidgetController { } @override - Future tap(Finder finder, { int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true }) async { + Future tap(FinderBase finder, { int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true }) async { await super.tap(await _waitForElement(finder), pointer: pointer, buttons: buttons, warnIfMissed: warnIfMissed); } - Future scrollIntoView(Finder finder, {required double alignment}) async { - final Finder target = await _waitForElement(finder); + Future scrollIntoView(FinderBase finder, {required double alignment}) async { + final FinderBase target = await _waitForElement(finder); await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment); } } diff --git a/dev/integration_tests/flutter_gallery/test/smoke_test.dart b/dev/integration_tests/flutter_gallery/test/smoke_test.dart index 98f473880dd5f..cd49804f804b9 100644 --- a/dev/integration_tests/flutter_gallery/test/smoke_test.dart +++ b/dev/integration_tests/flutter_gallery/test/smoke_test.dart @@ -79,8 +79,8 @@ Future smokeDemo(WidgetTester tester, GalleryDemo demo) async { // Verify that the dumps are pretty. final String routeName = demo.routeName; verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep()); - verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep()); - verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? ''); + verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderViews.single.toStringDeep()); + verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderViews.single.debugLayer?.toStringDeep() ?? ''); verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep()); // Scroll the demo around a bit more. diff --git a/dev/integration_tests/flutter_gallery/test_memory/image_cache_memory.dart b/dev/integration_tests/flutter_gallery/test_memory/image_cache_memory.dart index 0533a5fca342c..ccac85a2ce1ba 100644 --- a/dev/integration_tests/flutter_gallery/test_memory/image_cache_memory.dart +++ b/dev/integration_tests/flutter_gallery/test_memory/image_cache_memory.dart @@ -56,7 +56,7 @@ Future main() async { do { await controller.drag(list, const Offset(0.0, -30.0)); await Future.delayed(const Duration(milliseconds: 20)); - } while (!lastItem.precache()); + } while (!lastItem.tryEvaluate()); debugPrint('==== MEMORY BENCHMARK ==== DONE ===='); } diff --git a/dev/integration_tests/flutter_gallery/test_memory/memory_nav.dart b/dev/integration_tests/flutter_gallery/test_memory/memory_nav.dart index d83f61f9d3dd1..5992b64b1aa60 100644 --- a/dev/integration_tests/flutter_gallery/test_memory/memory_nav.dart +++ b/dev/integration_tests/flutter_gallery/test_memory/memory_nav.dart @@ -60,7 +60,7 @@ Future main() async { do { await controller.drag(demoList, const Offset(0.0, -300.0)); await Future.delayed(const Duration(milliseconds: 20)); - } while (!demoItem.precache()); + } while (!demoItem.tryEvaluate()); // Ensure that the center of the "Text fields" item is visible // because that's where we're going to tap diff --git a/dev/integration_tests/flutter_gallery/windows/flutter/CMakeLists.txt b/dev/integration_tests/flutter_gallery/windows/flutter/CMakeLists.txt index b2e4bd8d658b2..903f4899d6fce 100644 --- a/dev/integration_tests/flutter_gallery/windows/flutter/CMakeLists.txt +++ b/dev/integration_tests/flutter_gallery/windows/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/dev/integration_tests/gradle_deprecated_settings/android/gradle.properties b/dev/integration_tests/gradle_deprecated_settings/android/gradle.properties index 4d3226abc21bb..598d13fee4463 100644 --- a/dev/integration_tests/gradle_deprecated_settings/android/gradle.properties +++ b/dev/integration_tests/gradle_deprecated_settings/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml b/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml index 70db19b854d14..3abbe1967f77d 100644 --- a/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml +++ b/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml @@ -2,41 +2,41 @@ name: gradle_deprecated_settings description: Integration test for the current settings.gradle. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter - camera: 0.10.5+2 + camera: 0.10.5+4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - camera_android: 0.10.8+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - camera_avfoundation: 0.9.13+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - camera_platform_interface: 2.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - camera_web: 0.3.1+4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + camera_android: 0.10.8+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + camera_avfoundation: 0.9.13+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + camera_platform_interface: 2.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + camera_web: 0.3.2+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - cross_file: 0.3.3+4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - flutter_plugin_android_lifecycle: 2.0.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + cross_file: 0.3.3+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + flutter_plugin_android_lifecycle: 2.0.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" quiver: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_transform: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: f3fe +# PUBSPEC CHECKSUM: 5b94 diff --git a/dev/integration_tests/hybrid_android_views/android/gradle.properties b/dev/integration_tests/hybrid_android_views/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/hybrid_android_views/android/gradle.properties +++ b/dev/integration_tests/hybrid_android_views/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart index bdcb3dd90668d..b58580c40727c 100644 --- a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart +++ b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart @@ -51,13 +51,30 @@ class NestedViewEventBodyState extends State { children: [ SizedBox( height: 300, - child: showPlatformView ? - AndroidPlatformView( - key: const ValueKey('PlatformView'), - viewType: 'simple_view', - onPlatformViewCreated: onPlatformViewCreated, - useHybridComposition: useHybridComposition, - ) : null, + child: Stack( + alignment: Alignment.topCenter, + children: [ + if (showPlatformView) + AndroidPlatformView( + key: const ValueKey('PlatformView'), + viewType: 'simple_view', + onPlatformViewCreated: onPlatformViewCreated, + useHybridComposition: useHybridComposition, + ), + // The overlapping widget stabilizes the view tree by ensuring + // that there is widget content on top of the platform view. + // Without this widget, we rely on the UI elements down below + // to "incidentally" draw on top of the PlatformView which + // is not a reliable behavior as we eliminate non-visible + // rendering operations throughout the framework and engine. + const Positioned( + top: 50, + child: Text('overlapping widget', + style: TextStyle(color: Colors.yellow), + ), + ), + ], + ), ), if (_lastTestStatus != _LastTestStatus.pending) _statusWidget(), if (viewChannel != null) ... [ diff --git a/dev/integration_tests/hybrid_android_views/pubspec.yaml b/dev/integration_tests/hybrid_android_views/pubspec.yaml index 460a189ce623d..2b26016fbe905 100644 --- a/dev/integration_tests/hybrid_android_views/pubspec.yaml +++ b/dev/integration_tests/hybrid_android_views/pubspec.yaml @@ -4,15 +4,15 @@ publish_to: none description: An integration test for hybrid composition on Android version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter flutter_driver: sdk: flutter - path_provider: 2.0.15 - collection: 1.17.2 + path_provider: 2.1.1 + collection: 1.18.0 assets_for_android_views: git: url: https://github.com/flutter/goldens.git @@ -22,41 +22,40 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - ffi: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_android: 2.0.27 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_foundation: 2.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_linux: 2.1.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_platform_interface: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - path_provider_windows: 2.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_android: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_foundation: 2.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_linux: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_platform_interface: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_windows: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - win32: 5.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - xdg_directories: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + win32: 5.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xdg_directories: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -81,14 +80,14 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 6876 +# PUBSPEC CHECKSUM: 15e5 diff --git a/dev/integration_tests/ios_add2app_life_cycle/Podfile b/dev/integration_tests/ios_add2app_life_cycle/Podfile index c844f4ea5ae48..d3b1bf4ef7fcb 100644 --- a/dev/integration_tests/ios_add2app_life_cycle/Podfile +++ b/dev/integration_tests/ios_add2app_life_cycle/Podfile @@ -10,7 +10,7 @@ target 'ios_add2app' do end target 'ios_add2appTests' do - install_flutter_engine_pod + install_flutter_engine_pod(flutter_application_path) pod 'EarlGreyTest' end diff --git a/dev/integration_tests/ios_add2app_life_cycle/flutterapp/pubspec.yaml b/dev/integration_tests/ios_add2app_life_cycle/flutterapp/pubspec.yaml index ab6b4f34a0f8f..2f9a8e9595246 100644 --- a/dev/integration_tests/ios_add2app_life_cycle/flutterapp/pubspec.yaml +++ b/dev/integration_tests/ios_add2app_life_cycle/flutterapp/pubspec.yaml @@ -14,7 +14,7 @@ description: A new flutter module project. version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -22,14 +22,14 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -45,11 +45,11 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: # The following line ensures that the Material Icons font is @@ -99,4 +99,4 @@ flutter: androidPackage: com.example.iosadd2appflutter iosBundleIdentifier: com.example.iosAdd2appFlutter -# PUBSPEC CHECKSUM: f5e9 +# PUBSPEC CHECKSUM: 7b47 diff --git a/dev/integration_tests/ios_app_with_extensions/pubspec.yaml b/dev/integration_tests/ios_app_with_extensions/pubspec.yaml index 2631ecbf165af..3d614796d5eee 100644 --- a/dev/integration_tests/ios_app_with_extensions/pubspec.yaml +++ b/dev/integration_tests/ios_app_with_extensions/pubspec.yaml @@ -13,7 +13,7 @@ name: ios_app_with_extensions version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -23,13 +23,13 @@ dependencies: device_info: 2.0.3 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" device_info_platform_interface: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - plugin_platform_interface: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -47,11 +47,11 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -91,4 +91,4 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -# PUBSPEC CHECKSUM: e86f +# PUBSPEC CHECKSUM: 7dcd diff --git a/dev/integration_tests/ios_host_app/flutterapp/lib/main b/dev/integration_tests/ios_host_app/flutterapp/lib/main index 2f5c09d11b734..78ea99b18b875 100644 --- a/dev/integration_tests/ios_host_app/flutterapp/lib/main +++ b/dev/integration_tests/ios_host_app/flutterapp/lib/main @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:ffi_package/ffi_package.dart'; import 'marquee.dart'; @@ -116,10 +117,15 @@ class _MyHomePageState extends State { // button on the Flutter page has been tapped. int _counter = 0; + late int sumResult; + late Future sumAsyncResult; + @override void initState() { super.initState(); _platform.setMessageHandler(_handlePlatformIncrement); + sumResult = sum(1, 2); + sumAsyncResult = sumAsync(3, 4); } /// Directly increments our internal counter and rebuilds the UI. diff --git a/dev/integration_tests/ios_platform_view_tests/lib/main.dart b/dev/integration_tests/ios_platform_view_tests/lib/main.dart index 661e32d458514..22025f261e863 100644 --- a/dev/integration_tests/ios_platform_view_tests/lib/main.dart +++ b/dev/integration_tests/ios_platform_view_tests/lib/main.dart @@ -201,7 +201,7 @@ class _ZOrderTestPageState extends State { )), TextButton( onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return const SizedBox( diff --git a/dev/integration_tests/ios_platform_view_tests/pubspec.yaml b/dev/integration_tests/ios_platform_view_tests/pubspec.yaml index 3d03abe456cd1..b415fb1ec3315 100644 --- a/dev/integration_tests/ios_platform_view_tests/pubspec.yaml +++ b/dev/integration_tests/ios_platform_view_tests/pubspec.yaml @@ -3,7 +3,7 @@ name: ios_platform_view_tests version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -14,31 +14,31 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -63,11 +63,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -77,4 +77,4 @@ flutter: # the material Icons class. uses-material-design: true -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/dev/integration_tests/module_host_with_custom_build_v2_embedding/app/build.gradle b/dev/integration_tests/module_host_with_custom_build_v2_embedding/app/build.gradle index 4cf5470e824c6..2ca993f2d7cae 100644 --- a/dev/integration_tests/module_host_with_custom_build_v2_embedding/app/build.gradle +++ b/dev/integration_tests/module_host_with_custom_build_v2_embedding/app/build.gradle @@ -15,7 +15,7 @@ android { defaultConfig { applicationId "io.flutter.addtoapp" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 1 versionName "1.0" } diff --git a/dev/integration_tests/module_host_with_custom_build_v2_embedding/gradle.properties b/dev/integration_tests/module_host_with_custom_build_v2_embedding/gradle.properties index 47a56de84bd00..598d13fee4463 100644 --- a/dev/integration_tests/module_host_with_custom_build_v2_embedding/gradle.properties +++ b/dev/integration_tests/module_host_with_custom_build_v2_embedding/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/non_nullable/android/gradle.properties b/dev/integration_tests/non_nullable/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/non_nullable/android/gradle.properties +++ b/dev/integration_tests/non_nullable/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/non_nullable/pubspec.yaml b/dev/integration_tests/non_nullable/pubspec.yaml index b1d61f8cec408..58dcb97e808b6 100644 --- a/dev/integration_tests/non_nullable/pubspec.yaml +++ b/dev/integration_tests/non_nullable/pubspec.yaml @@ -5,19 +5,19 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -30,13 +30,13 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: f5e9 +# PUBSPEC CHECKSUM: 7b47 diff --git a/dev/integration_tests/platform_interaction/android/gradle.properties b/dev/integration_tests/platform_interaction/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/platform_interaction/android/gradle.properties +++ b/dev/integration_tests/platform_interaction/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/platform_interaction/lib/src/test_step.dart b/dev/integration_tests/platform_interaction/lib/src/test_step.dart index a75fd70197955..4bd004b5feb34 100644 --- a/dev/integration_tests/platform_interaction/lib/src/test_step.dart +++ b/dev/integration_tests/platform_interaction/lib/src/test_step.dart @@ -37,7 +37,8 @@ class TestStepResult { final String description; final TestStatus status; - static const TextStyle bold = TextStyle(fontWeight: FontWeight.bold); + static const TextStyle normal = TextStyle(height: 1.0); + static const TextStyle bold = TextStyle(fontWeight: FontWeight.bold, height: 1.0); static const TestStepResult complete = TestStepResult( 'Test complete', nothing, @@ -49,8 +50,8 @@ class TestStepResult { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Step: $name', style: bold), - Text(description), - const Text(' '), + Text(description, style: normal), + const Text(' ', style: normal), Text( status.toString().substring('TestStatus.'.length), key: ValueKey( diff --git a/dev/integration_tests/platform_interaction/pubspec.yaml b/dev/integration_tests/platform_interaction/pubspec.yaml index 44875f9bbdad5..2112c89e5a39b 100644 --- a/dev/integration_tests/platform_interaction/pubspec.yaml +++ b/dev/integration_tests/platform_interaction/pubspec.yaml @@ -2,22 +2,22 @@ name: platform_interaction description: Integration test for platform interactions. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -31,7 +31,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -45,24 +45,24 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 3dd1 +# PUBSPEC CHECKSUM: 4930 diff --git a/dev/integration_tests/release_smoke_test/android/gradle.properties b/dev/integration_tests/release_smoke_test/android/gradle.properties index d1ab454e543a7..4512f01215d27 100644 --- a/dev/integration_tests/release_smoke_test/android/gradle.properties +++ b/dev/integration_tests/release_smoke_test/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.useAndroidX=true diff --git a/dev/integration_tests/release_smoke_test/pubspec.yaml b/dev/integration_tests/release_smoke_test/pubspec.yaml index 7426fb46cd40e..f2b1ffea44aa7 100644 --- a/dev/integration_tests/release_smoke_test/pubspec.yaml +++ b/dev/integration_tests/release_smoke_test/pubspec.yaml @@ -1,18 +1,18 @@ name: release_smoke_test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -27,11 +27,11 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: b2fa +# PUBSPEC CHECKSUM: 4580 diff --git a/dev/integration_tests/spell_check/android/gradle.properties b/dev/integration_tests/spell_check/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/spell_check/android/gradle.properties +++ b/dev/integration_tests/spell_check/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/spell_check/pubspec.yaml b/dev/integration_tests/spell_check/pubspec.yaml index 91be571793695..ca226f3063a04 100644 --- a/dev/integration_tests/spell_check/pubspec.yaml +++ b/dev/integration_tests/spell_check/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,14 +32,14 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -61,12 +61,12 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -106,4 +106,4 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -# PUBSPEC CHECKSUM: 85a2 +# PUBSPEC CHECKSUM: ec29 diff --git a/dev/integration_tests/ui/android/gradle.properties b/dev/integration_tests/ui/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/integration_tests/ui/android/gradle.properties +++ b/dev/integration_tests/ui/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/integration_tests/ui/pubspec.yaml b/dev/integration_tests/ui/pubspec.yaml index 4613a4e7090c8..53d503489830f 100644 --- a/dev/integration_tests/ui/pubspec.yaml +++ b/dev/integration_tests/ui/pubspec.yaml @@ -2,7 +2,7 @@ name: integration_ui description: Flutter non-plugin UI integration tests. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -11,15 +11,15 @@ dependencies: sdk: flutter integration_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -33,7 +33,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -47,26 +47,26 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test_api: 0.6.0 + test_api: 0.6.1 clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -76,4 +76,4 @@ flutter: assets: - assets/foo.png -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/dev/integration_tests/ui/windows/flutter/CMakeLists.txt b/dev/integration_tests/ui/windows/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/dev/integration_tests/ui/windows/flutter/CMakeLists.txt +++ b/dev/integration_tests/ui/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/dev/integration_tests/web/pubspec.yaml b/dev/integration_tests/web/pubspec.yaml index 71008e7ea2266..82206b225ab8a 100644 --- a/dev/integration_tests/web/pubspec.yaml +++ b/dev/integration_tests/web/pubspec.yaml @@ -2,7 +2,7 @@ name: web_integration description: Integration test for web compilation. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: assets: @@ -15,10 +15,10 @@ dependencies: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a9c0 +# PUBSPEC CHECKSUM: 081a diff --git a/dev/integration_tests/web/web/index_with_flutterjs_custom_sw_version.html b/dev/integration_tests/web/web/index_with_flutterjs_custom_sw_version.html new file mode 100644 index 0000000000000..886afa4df72cd --- /dev/null +++ b/dev/integration_tests/web/web/index_with_flutterjs_custom_sw_version.html @@ -0,0 +1,39 @@ + + + + + + + + Integration test. App load with flutter.js. Custom serviceWorkerVersion provided. + + + + + + + + + + + + + + diff --git a/dev/integration_tests/web_compile_tests/pubspec.yaml b/dev/integration_tests/web_compile_tests/pubspec.yaml index 9dc5925ff4257..c8212be4e4691 100644 --- a/dev/integration_tests/web_compile_tests/pubspec.yaml +++ b/dev/integration_tests/web_compile_tests/pubspec.yaml @@ -1,16 +1,16 @@ name: web_compile_tests environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a9c0 +# PUBSPEC CHECKSUM: 081a diff --git a/dev/integration_tests/web_e2e_tests/pubspec.yaml b/dev/integration_tests/web_e2e_tests/pubspec.yaml index 37e371439dec2..da6afe4a57125 100644 --- a/dev/integration_tests/web_e2e_tests/pubspec.yaml +++ b/dev/integration_tests/web_e2e_tests/pubspec.yaml @@ -2,7 +2,7 @@ name: web_e2e_tests publish_to: none environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: assets: @@ -26,33 +26,33 @@ dependencies: boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_goldens: sdk: flutter http: 0.13.6 - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -67,7 +67,7 @@ dev_dependencies: mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -77,11 +77,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: ab73 +# PUBSPEC CHECKSUM: 16d3 diff --git a/dev/integration_tests/wide_gamut_test/pubspec.yaml b/dev/integration_tests/wide_gamut_test/pubspec.yaml index e7eeb65b71a05..2ff9b6fb8376e 100644 --- a/dev/integration_tests/wide_gamut_test/pubspec.yaml +++ b/dev/integration_tests/wide_gamut_test/pubspec.yaml @@ -5,18 +5,18 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -31,14 +31,14 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: b2fa +# PUBSPEC CHECKSUM: 4580 diff --git a/dev/integration_tests/windows_startup_test/lib/main.dart b/dev/integration_tests/windows_startup_test/lib/main.dart index 6917653db72e9..f79473238a15b 100644 --- a/dev/integration_tests/windows_startup_test/lib/main.dart +++ b/dev/integration_tests/windows_startup_test/lib/main.dart @@ -31,6 +31,25 @@ void drawHelloWorld(ui.FlutterView view) { view.render(sceneBuilder.build()); } +Future _waitUntilWindowVisible() async { + while (!await isWindowVisible()) { + await Future.delayed(const Duration(milliseconds: 100)); + } +} + +void _expectVisible(bool current, bool expect, Completer completer, int frameCount) { + if (current != expect) { + try { + throw 'Window should be ${expect ? 'visible' : 'hidden'} on frame $frameCount'; + } catch (e) { + if (!completer.isCompleted) { + completer.completeError(e); + } + rethrow; + } + } +} + void main() async { // TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into. assert(ui.PlatformDispatcher.instance.implicitView != null); @@ -70,27 +89,39 @@ void main() async { throw 'Window should be hidden at startup'; } - bool firstFrame = true; - ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) async { - if (await isWindowVisible()) { - if (firstFrame) { - throw 'Window should be hidden on first frame'; - } - - if (!visibilityCompleter.isCompleted) { - visibilityCompleter.complete('success'); - } + int frameCount = 0; + ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) { + // Our goal is to verify that it's `drawHelloWorld` that makes the window + // appear, not anything else. This requires checking the visibility right + // before drawing, but since `isWindowVisible` has to be async, and + // `FlutterView.render` (in `drawHelloWorld`) forbids async before it, + // this can not be done during a single onBeginFrame. However, we can + // verify in separate frames to indirectly prove it, by ensuring that + // no other mechanism can affect isWindowVisible in the first frame at all. + frameCount += 1; + switch (frameCount) { + // The 1st frame: render nothing, just verify that the window is hidden. + case 1: + isWindowVisible().then((bool visible) { + _expectVisible(visible, false, visibilityCompleter, frameCount); + ui.PlatformDispatcher.instance.scheduleFrame(); + }); + // The 2nd frame: render, which makes the window appear. + case 2: + drawHelloWorld(view); + _waitUntilWindowVisible().then((_) { + if (!visibilityCompleter.isCompleted) { + visibilityCompleter.complete('success'); + } + }); + // Others, in case requested to render. + default: + drawHelloWorld(view); } - - // Draw something to trigger the first frame callback that displays the - // window. - drawHelloWorld(view); - firstFrame = false; }; - - ui.PlatformDispatcher.instance.scheduleFrame(); } catch (e) { visibilityCompleter.completeError(e); rethrow; } + ui.PlatformDispatcher.instance.scheduleFrame(); } diff --git a/dev/integration_tests/windows_startup_test/pubspec.yaml b/dev/integration_tests/windows_startup_test/pubspec.yaml index e786d59a3176c..aadd2358a713d 100644 --- a/dev/integration_tests/windows_startup_test/pubspec.yaml +++ b/dev/integration_tests/windows_startup_test/pubspec.yaml @@ -2,22 +2,22 @@ name: windows_startup_test description: Integration test for Windows app's startup. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -31,7 +31,7 @@ dependencies: logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -45,21 +45,21 @@ dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 3dd1 +# PUBSPEC CHECKSUM: 4930 diff --git a/dev/integration_tests/windows_startup_test/windows/flutter/CMakeLists.txt b/dev/integration_tests/windows_startup_test/windows/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/dev/integration_tests/windows_startup_test/windows/flutter/CMakeLists.txt +++ b/dev/integration_tests/windows_startup_test/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/dev/manual_tests/android/gradle.properties b/dev/manual_tests/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/manual_tests/android/gradle.properties +++ b/dev/manual_tests/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/manual_tests/lib/density.dart b/dev/manual_tests/lib/density.dart index 02e252e4da4be..783b7d0108adb 100644 --- a/dev/manual_tests/lib/density.dart +++ b/dev/manual_tests/lib/density.dart @@ -621,14 +621,17 @@ class _MyHomePageState extends State { data: Theme.of(context).copyWith(visualDensity: _model.density), child: Directionality( textDirection: _model.rtl ? TextDirection.rtl : TextDirection.ltr, - child: MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: _model.size), - child: SizedBox.expand( - child: ListView( - children: tiles, + child: Builder(builder: (BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return MediaQuery( + data: mediaQueryData.copyWith(textScaler: TextScaler.linear(_model.size)), + child: SizedBox.expand( + child: ListView( + children: tiles, + ), ), - ), - ), + ); + }), ), ), ), diff --git a/dev/manual_tests/lib/menu_anchor.dart b/dev/manual_tests/lib/menu_anchor.dart index b6f98d34b7e07..11a5ebd84eed5 100644 --- a/dev/manual_tests/lib/menu_anchor.dart +++ b/dev/manual_tests/lib/menu_anchor.dart @@ -706,6 +706,12 @@ List createTestMenus({ TestMenu.mainMenu3, menuChildren: [ menuItemButton(TestMenu.subMenu8), + MenuItemButton( + onPressed: () { + debugPrint('Focused Item: $primaryFocus'); + }, + child: const Text('Print Focused Item'), + ) ], ), submenuButton( @@ -734,7 +740,11 @@ List createTestMenus({ submenuButton( TestMenu.subSubMenu3, menuChildren: [ - menuItemButton(TestMenu.subSubSubMenu1), + for (int i=0; i < 100; ++i) + MenuItemButton( + onPressed: () {}, + child: Text('Menu Item $i'), + ), ], ), ], diff --git a/dev/manual_tests/pubspec.yaml b/dev/manual_tests/pubspec.yaml index ce15e76cabb4e..66566439b726a 100644 --- a/dev/manual_tests/pubspec.yaml +++ b/dev/manual_tests/pubspec.yaml @@ -1,18 +1,18 @@ name: manual_tests environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -25,13 +25,13 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: b642 +# PUBSPEC CHECKSUM: dc9e diff --git a/dev/manual_tests/windows/flutter/CMakeLists.txt b/dev/manual_tests/windows/flutter/CMakeLists.txt index b2e4bd8d658b2..903f4899d6fce 100644 --- a/dev/manual_tests/windows/flutter/CMakeLists.txt +++ b/dev/manual_tests/windows/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/dev/missing_dependency_tests/pubspec.yaml b/dev/missing_dependency_tests/pubspec.yaml index a322c188f3ee5..6499f49c5f8a1 100644 --- a/dev/missing_dependency_tests/pubspec.yaml +++ b/dev/missing_dependency_tests/pubspec.yaml @@ -1,7 +1,7 @@ name: missing_dependency_tests environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/dev/tools/bin/generate_gradle_lockfiles.dart b/dev/tools/bin/generate_gradle_lockfiles.dart index 8daafdf7bfea4..e039e9f7577d2 100644 --- a/dev/tools/bin/generate_gradle_lockfiles.dart +++ b/dev/tools/bin/generate_gradle_lockfiles.dart @@ -86,13 +86,11 @@ void main(List arguments) { // Verify that the Gradlew wrapper exists. final File gradleWrapper = androidDirectory.childFile('gradlew'); - // Generate Gradle wrapper if it doesn't exists. - // This logic is embedded within the Flutter tool. - // To generate the wrapper, build a flavor that doesn't exist. + // Generate Gradle wrapper if it doesn't exist. if (!gradleWrapper.existsSync()) { Process.runSync( 'flutter', - ['build', 'apk', '--debug', '--flavor=does-not-exist'], + ['build', 'apk', '--config-only'], workingDirectory: appDirectory, ); } diff --git a/dev/tools/create_api_docs.dart b/dev/tools/create_api_docs.dart new file mode 100644 index 0000000000000..56f6c32695a23 --- /dev/null +++ b/dev/tools/create_api_docs.dart @@ -0,0 +1,1219 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:archive/archive_io.dart'; +import 'package:args/args.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import 'dartdoc_checker.dart'; + +const String kDummyPackageName = 'Flutter'; +const String kPlatformIntegrationPackageName = 'platform_integration'; + +class PlatformDocsSection { + const PlatformDocsSection({ + required this.zipName, + required this.sectionName, + required this.checkFile, + required this.subdir, + }); + final String zipName; + final String sectionName; + final String checkFile; + final String subdir; +} + +const Map kPlatformDocs = { + 'android': PlatformDocsSection( + zipName: 'android-javadoc.zip', + sectionName: 'Android', + checkFile: 'io/flutter/view/FlutterView.html', + subdir: 'javadoc', + ), + 'ios': PlatformDocsSection( + zipName: 'ios-docs.zip', + sectionName: 'iOS', + checkFile: 'interface_flutter_view.html', + subdir: 'ios-embedder', + ), + 'macos': PlatformDocsSection( + zipName: 'macos-docs.zip', + sectionName: 'macOS', + checkFile: 'interface_flutter_view.html', + subdir: 'macos-embedder', + ), + 'linux': PlatformDocsSection( + zipName: 'linux-docs.zip', + sectionName: 'Linux', + checkFile: 'struct___fl_view.html', + subdir: 'linux-embedder', + ), + 'windows': PlatformDocsSection( + zipName: 'windows-docs.zip', + sectionName: 'Windows', + checkFile: 'classflutter_1_1_flutter_view.html', + subdir: 'windows-embedder', + ), + 'impeller': PlatformDocsSection( + zipName: 'impeller-docs.zip', + sectionName: 'Impeller', + checkFile: 'classimpeller_1_1_canvas.html', + subdir: 'impeller', + ), +}; + +/// This script will generate documentation for the packages in `packages/` and +/// write the documentation to the output directory specified on the command +/// line. +/// +/// This script also updates the index.html file so that it can be placed at the +/// root of api.flutter.dev. The files are kept inside of +/// api.flutter.dev/flutter, so we need to manipulate paths a bit. See +/// https://github.com/flutter/flutter/issues/3900 for more info. +/// +/// This will only work on UNIX systems, not Windows. It requires that 'git', +/// 'zip', and 'tar' be in the PATH. It requires that 'flutter' has been run +/// previously. It uses the version of Dart downloaded by the 'flutter' tool in +/// this repository and will fail if that is absent. +Future main(List arguments) async { + const FileSystem filesystem = LocalFileSystem(); + const ProcessManager processManager = LocalProcessManager(); + const Platform platform = LocalPlatform(); + + // The place to find customization files and configuration files for docs + // generation. + final Directory docsRoot = + FlutterInformation.instance.getFlutterRoot().childDirectory('dev').childDirectory('docs').absolute; + final ArgParser argParser = _createArgsParser( + publishDefault: docsRoot.childDirectory('doc').path, + ); + final ArgResults args = argParser.parse(arguments); + if (args['help'] as bool) { + print('Usage:'); + print(argParser.usage); + exit(0); + } + + final Directory publishRoot = filesystem.directory(args['output-dir']! as String).absolute; + final Directory packageRoot = publishRoot.parent; + if (!filesystem.directory(packageRoot).existsSync()) { + filesystem.directory(packageRoot).createSync(recursive: true); + } + + if (!filesystem.directory(publishRoot).existsSync()) { + filesystem.directory(publishRoot).createSync(recursive: true); + } + + final Configurator configurator = Configurator( + publishRoot: publishRoot, + packageRoot: packageRoot, + docsRoot: docsRoot, + filesystem: filesystem, + processManager: processManager, + platform: platform, + ); + configurator.generateConfiguration(); + + final PlatformDocGenerator platformGenerator = PlatformDocGenerator(outputDir: publishRoot, filesystem: filesystem); + await platformGenerator.generatePlatformDocs(); + + final DartdocGenerator dartdocGenerator = DartdocGenerator( + publishRoot: publishRoot, + packageRoot: packageRoot, + docsRoot: docsRoot, + filesystem: filesystem, + processManager: processManager, + useJson: args['json'] as bool? ?? true, + validateLinks: args['validate-links']! as bool, + verbose: args['verbose'] as bool? ?? false, + ); + + await dartdocGenerator.generateDartdoc(); + await configurator.generateOfflineAssetsIfNeeded(); +} + +ArgParser _createArgsParser({required String publishDefault}) { + final ArgParser parser = ArgParser(); + parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show command help.'); + parser.addFlag('verbose', + defaultsTo: true, + help: 'Whether to report all error messages (on) or attempt to ' + 'filter out some known false positives (off). Shut this off ' + 'locally if you want to address Flutter-specific issues.'); + parser.addFlag('json', help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.'); + parser.addFlag('validate-links', help: 'Display warnings for broken links generated by dartdoc (slow)'); + parser.addOption('output-dir', defaultsTo: publishDefault, help: 'Sets the output directory for the documentation.'); + return parser; +} + +/// A class used to configure the staging area for building the docs in. +/// +/// The [generateConfiguration] function generates a dummy package with a +/// pubspec. It copies any assets and customization files from the framework +/// repo. It creates a metadata file for searches. +/// +/// Once the docs have been generated, [generateOfflineAssetsIfNeeded] will +/// create offline assets like Dash/Zeal docsets and an offline ZIP file of the +/// site if the build is a CI build that is not a presubmit build. +class Configurator { + Configurator({ + required this.docsRoot, + required this.publishRoot, + required this.packageRoot, + required this.filesystem, + required this.processManager, + required this.platform, + }); + + /// The root of the directory in the Flutter repo where configuration data is + /// stored. + final Directory docsRoot; + + /// The root of the output area for the dartdoc docs. + /// + /// Typically this is a "doc" subdirectory under the [packageRoot]. + final Directory publishRoot; + + /// The root of the staging area for creating docs. + final Directory packageRoot; + + /// The [FileSystem] object used to create [File] and [Directory] objects. + final FileSystem filesystem; + + /// The [ProcessManager] object used to invoke external processes. + /// + /// Can be replaced by tests to have a fake process manager. + final ProcessManager processManager; + + /// The [Platform] to use for this run. + /// + /// Can be replaced by tests to test behavior on different plaforms. + final Platform platform; + + void generateConfiguration() { + final Version version = FlutterInformation.instance.getFlutterVersion(); + _createDummyPubspec(); + _createDummyLibrary(); + _createPageFooter(packageRoot, version); + _copyCustomizations(); + _createSearchMetadata( + docsRoot.childDirectory('lib').childFile('opensearch.xml'), publishRoot.childFile('opensearch.xml')); + } + + Future generateOfflineAssetsIfNeeded() async { + // Only create the offline docs if we're running in a non-presubmit build: + // it takes too long otherwise. + if (platform.environment.containsKey('LUCI_CI') && (platform.environment['LUCI_PR'] ?? '').isEmpty) { + _createOfflineZipFile(); + await _createDocset(); + _moveOfflineIntoPlace(); + _createRobotsTxt(); + } + } + + /// Returns import or on-disk paths for all libraries in the Flutter SDK. + Iterable _libraryRefs() sync* { + for (final Directory dir in findPackages(filesystem)) { + final String dirName = dir.basename; + for (final FileSystemEntity file in dir.childDirectory('lib').listSync()) { + if (file is File && file.path.endsWith('.dart')) { + yield '$dirName/${file.basename}'; + } + } + } + + // Add a fake package for platform integration APIs. + yield '$kPlatformIntegrationPackageName/android.dart'; + yield '$kPlatformIntegrationPackageName/ios.dart'; + yield '$kPlatformIntegrationPackageName/macos.dart'; + yield '$kPlatformIntegrationPackageName/linux.dart'; + yield '$kPlatformIntegrationPackageName/windows.dart'; + } + + void _createDummyPubspec() { + // Create the pubspec.yaml file. + final List pubspec = [ + 'name: $kDummyPackageName', + 'homepage: https://flutter.dev', + 'version: 0.0.0', + 'environment:', + " sdk: '>=3.2.0-0 <4.0.0'", + 'dependencies:', + for (final String package in findPackageNames(filesystem)) ' $package:\n sdk: flutter', + ' $kPlatformIntegrationPackageName: 0.0.1', + 'dependency_overrides:', + ' $kPlatformIntegrationPackageName:', + ' path: ${docsRoot.childDirectory(kPlatformIntegrationPackageName).path}', + ]; + + packageRoot.childFile('pubspec.yaml').writeAsStringSync(pubspec.join('\n')); + } + + void _createDummyLibrary() { + final Directory libDir = packageRoot.childDirectory('lib'); + libDir.createSync(); + + final StringBuffer contents = StringBuffer('library temp_doc;\n\n'); + for (final String libraryRef in _libraryRefs()) { + contents.writeln("import 'package:$libraryRef';"); + } + packageRoot.childDirectory('lib') + ..createSync(recursive: true) + ..childFile('temp_doc.dart').writeAsStringSync(contents.toString()); + } + + void _createPageFooter(Directory footerPath, Version version) { + final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); + final String gitBranch = FlutterInformation.instance.getBranchName(); + final String gitRevision = FlutterInformation.instance.getFlutterRevision(); + final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; + footerPath.childFile('footer.html').writeAsStringSync(''); + publishRoot.childDirectory('flutter').childFile('footer.js') + ..createSync(recursive: true) + ..writeAsStringSync(''' +(function() { + var span = document.querySelector('footer>span'); + if (span) { + span.innerText = 'Flutter $version • $timestamp • $gitRevision $gitBranchOut'; + } + var sourceLink = document.querySelector('a.source-link'); + if (sourceLink) { + sourceLink.href = sourceLink.href.replace('/master/', '/$gitRevision/'); + } +})(); +'''); + } + + void _copyCustomizations() { + final List files = [ + 'README.md', + 'analysis_options.yaml', + 'dartdoc_options.yaml', + ]; + for (final String file in files) { + final File source = docsRoot.childFile(file); + final File destination = packageRoot.childFile(file); + // Have to canonicalize because otherwise things like /foo/bar/baz and + // /foo/../foo/bar/baz won't compare as identical. + if (path.canonicalize(source.absolute.path) != path.canonicalize(destination.absolute.path)) { + source.copySync(destination.path); + print('Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}'); + } + } + final Directory assetsDir = filesystem.directory(publishRoot.childDirectory('assets')); + final Directory assetSource = docsRoot.childDirectory('assets'); + if (path.canonicalize(assetSource.absolute.path) == path.canonicalize(assetsDir.absolute.path)) { + // Don't try and copy the directory over itself. + return; + } + if (assetsDir.existsSync()) { + assetsDir.deleteSync(recursive: true); + } + copyDirectorySync( + docsRoot.childDirectory('assets'), + assetsDir, + onFileCopied: (File src, File dest) { + print('Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}'); + }, + filesystem: filesystem, + ); + } + + /// Generates an OpenSearch XML description that can be used to add a custom + /// search for Flutter API docs to the browser. Unfortunately, it has to know + /// the URL to which site to search, so we customize it here based upon the + /// branch name. + void _createSearchMetadata(File templatePath, File metadataPath) { + final String template = templatePath.readAsStringSync(); + final String branch = FlutterInformation.instance.getBranchName(); + final String metadata = template.replaceAll( + '{SITE_URL}', + branch == 'stable' ? 'https://api.flutter.dev/' : 'https://master-api.flutter.dev/', + ); + metadataPath.parent.create(recursive: true); + metadataPath.writeAsStringSync(metadata); + } + + Future _createDocset() async { + // Must have dashing installed: go get -u github.com/technosophos/dashing + // Dashing produces a LOT of log output (~30MB), so we collect it, and just + // show the end of it if there was a problem. + print('${DateTime.now().toUtc()}: Building Flutter docset.'); + + // If dashing gets stuck, Cirrus will time out the build after an hour, and we + // never get to see the logs. Thus, we run it in the background and tail the + // logs only if it fails. + final ProcessWrapper result = ProcessWrapper( + await processManager.start( + [ + 'dashing', + 'build', + '--source', + publishRoot.path, + '--config', + docsRoot.childFile('dashing.json').path, + ], + workingDirectory: packageRoot.path, + ), + ); + final List buffer = []; + result.stdout.listen(buffer.addAll); + result.stderr.listen(buffer.addAll); + // If the dashing process exited with an error, print the last 200 lines of stderr and exit. + final int exitCode = await result.done; + if (exitCode != 0) { + print('Dashing docset generation failed with code $exitCode'); + final List output = systemEncoding.decode(buffer).split('\n'); + print(output.sublist(math.max(output.length - 200, 0)).join('\n')); + exit(exitCode); + } + buffer.clear(); + + // Copy the favicon file to the output directory. + final File faviconFile = + publishRoot.childDirectory('flutter').childDirectory('static-assets').childFile('favicon.png'); + final File iconFile = packageRoot.childDirectory('flutter.docset').childFile('icon.png'); + faviconFile + ..createSync(recursive: true) + ..copySync(iconFile.path); + + // Post-process the dashing output. + final File infoPlist = + packageRoot.childDirectory('flutter.docset').childDirectory('Contents').childFile('Info.plist'); + String contents = infoPlist.readAsStringSync(); + + // Since I didn't want to add the XML package as a dependency just for this, + // I just used a regular expression to make this simple change. + final RegExp findRe = RegExp(r'(\s*DocSetPlatformFamily\s*)[^<]+()', multiLine: true); + contents = contents.replaceAllMapped(findRe, (Match match) { + return '${match.group(1)}dartlang${match.group(2)}'; + }); + infoPlist.writeAsStringSync(contents); + final Directory offlineDir = publishRoot.childDirectory('offline'); + if (!offlineDir.existsSync()) { + offlineDir.createSync(recursive: true); + } + tarDirectory(packageRoot, offlineDir.childFile('flutter.docset.tar.gz'), processManager: processManager); + + // Write the Dash/Zeal XML feed file. + final bool isStable = platform.environment['LUCI_BRANCH'] == 'stable'; + offlineDir.childFile('flutter.xml').writeAsStringSync('\n' + ' ${FlutterInformation.instance.getFlutterVersion()}\n' + ' https://${isStable ? '' : 'master-'}api.flutter.dev/offline/flutter.docset.tar.gz\n' + '\n'); + } + + // Creates the offline ZIP file containing all of the website HTML files. + void _createOfflineZipFile() { + print('${DateTime.now().toLocal()}: Creating offline docs archive.'); + zipDirectory(publishRoot, packageRoot.childFile('flutter.docs.zip'), processManager: processManager); + } + + // Moves the generated offline archives into the publish directory so that + // they can be included in the output ZIP file. + void _moveOfflineIntoPlace() { + print('${DateTime.now().toUtc()}: Moving offline docs into place.'); + final Directory offlineDir = publishRoot.childDirectory('offline')..createSync(recursive: true); + packageRoot.childFile('flutter.docs.zip').renameSync(offlineDir.childFile('flutter.docs.zip').path); + } + + // Creates a robots.txt file that disallows indexing unless the branch is the + // stable branch. + void _createRobotsTxt() { + final File robotsTxt = publishRoot.childFile('robots.txt'); + if (FlutterInformation.instance.getBranchName() == 'stable') { + robotsTxt.writeAsStringSync('# All robots welcome!'); + } else { + robotsTxt.writeAsStringSync('User-agent: *\nDisallow: /'); + } + } +} + +/// Runs Dartdoc inside of the given pre-prepared staging area, prepared by +/// [Configurator.generateConfiguration]. +/// +/// Performs a sanity check of the output once the generation is complete. +class DartdocGenerator { + DartdocGenerator({ + required this.docsRoot, + required this.publishRoot, + required this.packageRoot, + required this.filesystem, + required this.processManager, + this.useJson = true, + this.validateLinks = true, + this.verbose = false, + }); + + /// The root of the directory in the Flutter repo where configuration data is + /// stored. + final Directory docsRoot; + + /// The root of the output area for the dartdoc docs. + /// + /// Typically this is a "doc" subdirectory under the [packageRoot]. + final Directory publishRoot; + + /// The root of the staging area for creating docs. + final Directory packageRoot; + + /// The [FileSystem] object used to create [File] and [Directory] objects. + final FileSystem filesystem; + + /// The [ProcessManager] object used to invoke external processes. + /// + /// Can be replaced by tests to have a fake process manager. + final ProcessManager processManager; + + /// Whether or not dartdoc should output an index.json file of the + /// documentation. + final bool useJson; + + // Whether or not to have dartdoc validate its own links. + final bool validateLinks; + + /// Whether or not to filter overly verbose log output from dartdoc. + final bool verbose; + + Future generateDartdoc() async { + final Directory flutterRoot = FlutterInformation.instance.getFlutterRoot(); + final Map pubEnvironment = { + 'FLUTTER_ROOT': flutterRoot.absolute.path, + }; + + // If there's a .pub-cache dir in the Flutter root, use that. + final File pubCache = flutterRoot.childFile('.pub-cache'); + if (pubCache.existsSync()) { + pubEnvironment['PUB_CACHE'] = pubCache.path; + } + + // Run pub. + ProcessWrapper process = ProcessWrapper(await runPubProcess( + arguments: ['get'], + workingDirectory: packageRoot, + environment: pubEnvironment, + filesystem: filesystem, + processManager: processManager, + )); + printStream(process.stdout, prefix: 'pub:stdout: '); + printStream(process.stderr, prefix: 'pub:stderr: '); + final int code = await process.done; + if (code != 0) { + exit(code); + } + + final Version version = FlutterInformation.instance.getFlutterVersion(); + + // Verify which version of snippets and dartdoc we're using. + final ProcessResult snippetsResult = processManager.runSync( + [ + FlutterInformation.instance.getFlutterBinaryPath().path, + 'pub', + 'global', + 'list', + ], + workingDirectory: packageRoot.path, + environment: pubEnvironment, + stdoutEncoding: utf8, + ); + print(''); + final Iterable versionMatches = + RegExp(r'^(?snippets|dartdoc) (?[^\s]+)', multiLine: true) + .allMatches(snippetsResult.stdout as String); + for (final RegExpMatch match in versionMatches) { + print('${match.namedGroup('name')} version: ${match.namedGroup('version')}'); + } + + print('flutter version: $version\n'); + + // Dartdoc warnings and errors in these packages are considered fatal. + // All packages owned by flutter should be in the list. + final List flutterPackages = [ + kDummyPackageName, + kPlatformIntegrationPackageName, + ...findPackageNames(filesystem), + // TODO(goderbauer): Figure out how to only include `dart:ui` of + // `sky_engine` below, https://github.com/dart-lang/dartdoc/issues/2278. + // 'sky_engine', + ]; + + // Generate the documentation. We don't need to exclude flutter_tools in + // this list because it's not in the recursive dependencies of the package + // defined at packageRoot + final List dartdocArgs = [ + 'global', + 'run', + '--enable-asserts', + 'dartdoc', + '--output', + publishRoot.childDirectory('flutter').path, + '--allow-tools', + if (useJson) '--json', + if (validateLinks) '--validate-links' else '--no-validate-links', + '--link-to-source-excludes', + flutterRoot.childDirectory('bin').childDirectory('cache').path, + '--link-to-source-root', + flutterRoot.path, + '--link-to-source-uri-template', + 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', + docsRoot.childFile('styles.html').path, + '--header', + docsRoot.childFile('analytics-header.html').path, + '--header', + docsRoot.childFile('survey.html').path, + '--header', + docsRoot.childFile('snippets.html').path, + '--header', + docsRoot.childFile('opensearch.html').path, + '--footer', + docsRoot.childFile('analytics-footer.html').path, + '--footer-text', + packageRoot.childFile('footer.html').path, + '--allow-warnings-in-packages', + flutterPackages.join(','), + '--exclude-packages', + [ + 'analyzer', + 'args', + 'barback', + 'csslib', + 'flutter_goldens', + 'flutter_goldens_client', + 'front_end', + 'fuchsia_remote_debug_protocol', + 'glob', + 'html', + 'http_multi_server', + 'io', + 'isolate', + 'js', + 'kernel', + 'logging', + 'mime', + 'mockito', + 'node_preamble', + 'plugin', + 'shelf', + 'shelf_packages_handler', + 'shelf_static', + 'shelf_web_socket', + 'utf', + 'watcher', + 'yaml', + ].join(','), + '--exclude', + [ + 'dart:io/network_policy.dart', // dart-lang/dartdoc#2437 + 'package:Flutter/temp_doc.dart', + 'package:http/browser_client.dart', + 'package:intl/intl_browser.dart', + 'package:matcher/mirror_matchers.dart', + 'package:quiver/io.dart', + 'package:quiver/mirrors.dart', + 'package:vm_service_client/vm_service_client.dart', + 'package:web_socket_channel/html.dart', + ].join(','), + '--favicon', + docsRoot.childFile('favicon.ico').absolute.path, + '--package-order', + 'flutter,Dart,$kPlatformIntegrationPackageName,flutter_test,flutter_driver', + '--auto-include-dependencies', + ]; + + String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg; + print('Executing: (cd "${packageRoot.path}" ; ' + '${FlutterInformation.instance.getDartBinaryPath().path} ' + '${dartdocArgs.map(quote).join(' ')})'); + + process = ProcessWrapper(await runPubProcess( + arguments: dartdocArgs, + workingDirectory: packageRoot, + environment: pubEnvironment, + processManager: processManager, + )); + printStream( + process.stdout, + prefix: useJson ? '' : 'dartdoc:stdout: ', + filter: [ + if (!verbose) RegExp(r'^Generating docs for library '), // Unnecessary verbosity + ], + ); + printStream( + process.stderr, + prefix: useJson ? '' : 'dartdoc:stderr: ', + filter: [ + if (!verbose) + RegExp( + // Remove warnings from packages outside our control + r'^ warning: .+: \(.+[\\/]\.pub-cache[\\/]hosted[\\/]pub.dartlang.org[\\/].+\)', + ), + ], + ); + final int exitCode = await process.done; + + if (exitCode != 0) { + exit(exitCode); + } + + _sanityCheckDocs(); + checkForUnresolvedDirectives(publishRoot.childDirectory('flutter')); + + _createIndexAndCleanup(); + + print('Documentation written to ${publishRoot.path}'); + } + + void _sanityCheckExample(String fileString, String regExpString) { + final File file = filesystem.file(fileString); + if (file.existsSync()) { + final RegExp regExp = RegExp(regExpString, dotAll: true); + final String contents = file.readAsStringSync(); + if (!regExp.hasMatch(contents)) { + throw Exception("Missing example code matching '$regExpString' in ${file.path}."); + } + } else { + throw Exception( + "Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file."); + } + } + + /// A subset of all generated doc files for [_sanityCheckDocs]. + @visibleForTesting + List get canaries { + final Directory flutterDirectory = publishRoot.childDirectory('flutter'); + final Directory widgetsDirectory = flutterDirectory.childDirectory('widgets'); + + return [ + publishRoot.childDirectory('assets').childFile('overrides.css'), + flutterDirectory.childDirectory('dart-io').childFile('File-class.html'), + flutterDirectory.childDirectory('dart-ui').childFile('Canvas-class.html'), + flutterDirectory.childDirectory('dart-ui').childDirectory('Canvas').childFile('drawRect.html'), + flutterDirectory + .childDirectory('flutter_driver') + .childDirectory('FlutterDriver') + .childFile('FlutterDriver.connectedTo.html'), + flutterDirectory.childDirectory('flutter_test').childDirectory('WidgetTester').childFile('pumpWidget.html'), + flutterDirectory.childDirectory('material').childFile('Material-class.html'), + flutterDirectory.childDirectory('material').childFile('Tooltip-class.html'), + widgetsDirectory.childFile('Widget-class.html'), + widgetsDirectory.childFile('Listener-class.html'), + ]; + } + + /// Runs a sanity check by running a test. + void _sanityCheckDocs([Platform platform = const LocalPlatform()]) { + for (final File canary in canaries) { + if (!canary.existsSync()) { + throw Exception('Missing "${canary.path}", which probably means the documentation failed to build correctly.'); + } + } + // Make sure at least one example of each kind includes source code. + final Directory widgetsDirectory = publishRoot + .childDirectory('flutter') + .childDirectory('widgets'); + + // Check a "sample" example, any one will do. + _sanityCheckExample( + widgetsDirectory.childFile('showGeneralDialog.html').path, + r'\s*\s*import 'package:flutter/material.dart';', + ); + + // Check a "snippet" example, any one will do. + _sanityCheckExample( + widgetsDirectory.childDirectory('ModalRoute').childFile('barrierColor.html').path, + r'\s*.*Color\s+get\s+barrierColor.*', + ); + + // Check a "dartpad" example, any one will do, and check for the correct URL + // arguments. + // Just use "master" for any branch other than the LUCI_BRANCH. + final String? luciBranch = platform.environment['LUCI_BRANCH']?.trim(); + final String expectedBranch = luciBranch != null && luciBranch.isNotEmpty ? luciBranch : 'master'; + final List argumentRegExps = [ + r'split=\d+', + r'run=true', + r'sample_id=widgets\.Listener\.\d+', + 'sample_channel=$expectedBranch', + 'channel=$expectedBranch', + ]; + for (final String argumentRegExp in argumentRegExps) { + _sanityCheckExample( + widgetsDirectory.childFile('Listener-class.html').path, + r'\s*\s*<\/iframe>', + ); + } + } + + /// Creates a custom index.html because we try to maintain old + /// paths. Cleanup unused index.html files no longer needed. + void _createIndexAndCleanup() { + print('\nCreating a custom index.html in ${publishRoot.childFile('index.html').path}'); + _copyIndexToRootOfDocs(); + _addHtmlBaseToIndex(); + _changePackageToSdkInTitlebar(); + _putRedirectInOldIndexLocation(); + _writeSnippetsIndexFile(); + print('\nDocs ready to go!'); + } + + void _copyIndexToRootOfDocs() { + publishRoot.childDirectory('flutter').childFile('index.html').copySync(publishRoot.childFile('index.html').path); + } + + void _changePackageToSdkInTitlebar() { + final File indexFile = publishRoot.childFile('index.html'); + String indexContents = indexFile.readAsStringSync(); + indexContents = indexContents.replaceFirst( + '
  • Flutter package
  • ', + '
  • Flutter SDK
  • ', + ); + + indexFile.writeAsStringSync(indexContents); + } + + void _addHtmlBaseToIndex() { + final File indexFile = publishRoot.childFile('index.html'); + String indexContents = indexFile.readAsStringSync(); + indexContents = indexContents.replaceFirst( + '\n', + '\n \n', + ); + + for (final String platform in kPlatformDocs.keys) { + final String sectionName = kPlatformDocs[platform]!.sectionName; + final String subdir = kPlatformDocs[platform]!.subdir; + indexContents = indexContents.replaceAll( + 'href="$sectionName/$sectionName-library.html"', + 'href="../$subdir/index.html"', + ); + } + + indexFile.writeAsStringSync(indexContents); + } + + void _putRedirectInOldIndexLocation() { + const String metaTag = ''; + publishRoot.childDirectory('flutter').childFile('index.html').writeAsStringSync(metaTag); + } + + void _writeSnippetsIndexFile() { + final Directory snippetsDir = publishRoot.childDirectory('snippets'); + if (snippetsDir.existsSync()) { + const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); + final Iterable files = + snippetsDir.listSync().whereType().where((File file) => path.extension(file.path) == '.json'); + // Combine all the metadata into a single JSON array. + final Iterable fileContents = files.map((File file) => file.readAsStringSync()); + final List metadataObjects = fileContents.map(json.decode).toList(); + final String jsonArray = jsonEncoder.convert(metadataObjects); + snippetsDir.childFile('index.json').writeAsStringSync(jsonArray); + } + } +} + +/// Downloads and unpacks the platform specific documentation generated by the +/// engine build. +/// +/// Unpacks and massages the data so that it can be properly included in the +/// output archive. +class PlatformDocGenerator { + PlatformDocGenerator({required this.outputDir, required this.filesystem}); + + final FileSystem filesystem; + final Directory outputDir; + final String engineRevision = FlutterInformation.instance.getEngineRevision(); + final String engineRealm = FlutterInformation.instance.getEngineRealm(); + + /// This downloads an archive of platform docs for the engine from the artifact + /// store and extracts them to the location used for Dartdoc. + Future generatePlatformDocs() async { + final String realm = engineRealm.isNotEmpty ? '$engineRealm/' : ''; + + for (final String platform in kPlatformDocs.keys) { + final String zipFile = kPlatformDocs[platform]!.zipName; + final String url = + 'https://storage.googleapis.com/${realm}flutter_infra_release/flutter/$engineRevision/$zipFile'; + await _extractDocs(url, platform, kPlatformDocs[platform]!, outputDir); + } + } + + /// Fetches the zip archive at the specified url. + /// + /// Returns null if the archive fails to download after [maxTries] attempts. + Future _fetchArchive(String url, int maxTries) async { + List? responseBytes; + for (int i = 0; i < maxTries; i++) { + final http.Response response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + responseBytes = response.bodyBytes; + break; + } + stderr.writeln('Failed attempt ${i + 1} to fetch $url.'); + + // On failure print a short snipped from the body in case it's helpful. + final int bodyLength = math.min(1024, response.body.length); + stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}'); + sleep(const Duration(seconds: 1)); + } + return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes); + } + + Future _extractDocs(String url, String name, PlatformDocsSection platform, Directory outputDir) async { + const int maxTries = 5; + final Archive? archive = await _fetchArchive(url, maxTries); + if (archive == null) { + stderr.writeln('Failed to fetch zip archive from: $url after $maxTries attempts. Giving up.'); + exit(1); + } + + final Directory output = outputDir.childDirectory(platform.subdir); + print('Extracting ${platform.zipName} to ${output.path}'); + output.createSync(recursive: true); + + for (final ArchiveFile af in archive) { + if (!af.name.endsWith('/')) { + final File file = filesystem.file('${output.path}/${af.name}'); + file.createSync(recursive: true); + file.writeAsBytesSync(af.content as List); + } + } + + final File testFile = output.childFile(platform.checkFile); + if (!testFile.existsSync()) { + print('Expected file ${testFile.path} not found'); + exit(1); + } + print('${platform.sectionName} ready to go!'); + } +} + +/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if +/// specified, for each source/destination file pair. +/// +/// Creates `destDir` if needed. +void copyDirectorySync(Directory srcDir, Directory destDir, + {void Function(File srcFile, File destFile)? onFileCopied, required FileSystem filesystem}) { + if (!srcDir.existsSync()) { + throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); + } + + if (!destDir.existsSync()) { + destDir.createSync(recursive: true); + } + + for (final FileSystemEntity entity in srcDir.listSync()) { + final String newPath = path.join(destDir.path, path.basename(entity.path)); + if (entity is File) { + final File newFile = filesystem.file(newPath); + entity.copySync(newPath); + onFileCopied?.call(entity, newFile); + } else if (entity is Directory) { + copyDirectorySync(entity, filesystem.directory(newPath), filesystem: filesystem); + } else { + throw Exception('${entity.path} is neither File nor Directory'); + } + } +} + +void printStream(Stream> stream, {String prefix = '', List filter = const []}) { + stream.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) { + if (!filter.any((Pattern pattern) => line.contains(pattern))) { + print('$prefix$line'.trim()); + } + }); +} + +void zipDirectory(Directory src, File output, {required ProcessManager processManager}) { + // We would use the archive package to do this in one line, but it + // is a lot slower, and doesn't do compression nearly as well. + final ProcessResult zipProcess = processManager.runSync( + [ + 'zip', + '-r', + '-9', + '-q', + output.path, + '.', + ], + workingDirectory: src.path, + ); + + if (zipProcess.exitCode != 0) { + print('Creating offline ZIP archive ${output.path} failed:'); + print(zipProcess.stderr); + exit(1); + } +} + +void tarDirectory(Directory src, File output, {required ProcessManager processManager}) { + // We would use the archive package to do this in one line, but it + // is a lot slower, and doesn't do compression nearly as well. + final ProcessResult tarProcess = processManager.runSync( + [ + 'tar', + 'cf', + output.path, + '--use-compress-program', + 'gzip --best', + 'flutter.docset', + ], + workingDirectory: src.path, + ); + + if (tarProcess.exitCode != 0) { + print('Creating a tarball ${output.path} failed:'); + print(tarProcess.stderr); + exit(1); + } +} + +Future runPubProcess({ + required List arguments, + Directory? workingDirectory, + Map? environment, + @visibleForTesting ProcessManager processManager = const LocalProcessManager(), + @visibleForTesting FileSystem filesystem = const LocalFileSystem(), +}) { + return processManager.start( + [FlutterInformation.instance.getFlutterBinaryPath().path, 'pub', ...arguments], + workingDirectory: (workingDirectory ?? filesystem.currentDirectory).path, + environment: environment, + ); +} + +List findPackageNames(FileSystem filesystem) { + return findPackages(filesystem).map((FileSystemEntity file) => path.basename(file.path)).toList(); +} + +/// Finds all packages in the Flutter SDK +List findPackages(FileSystem filesystem) { + return FlutterInformation.instance + .getFlutterRoot() + .childDirectory('packages') + .listSync() + .where((FileSystemEntity entity) { + if (entity is! Directory) { + return false; + } + final File pubspec = entity.childFile('pubspec.yaml'); + if (!pubspec.existsSync()) { + print("Unexpected package '${entity.path}' found in packages directory"); + return false; + } + // Would be nice to use a real YAML parser here, but we don't want to + // depend on a whole package for it, and this is sufficient. + return !pubspec.readAsStringSync().contains('nodoc: true'); + }) + .cast() + .toList(); +} + +/// An exception class used to indicate problems when collecting information. +class FlutterInformationException implements Exception { + FlutterInformationException(this.message); + final String message; + + @override + String toString() { + return '$runtimeType: $message'; + } +} + +/// A singleton used to consolidate the way in which information about the +/// Flutter repo and environment is collected. +/// +/// Collects the information once, and caches it for any later access. +/// +/// The singleton instance can be overridden by tests by setting [instance]. +class FlutterInformation { + FlutterInformation({ + this.platform = const LocalPlatform(), + this.processManager = const LocalProcessManager(), + this.filesystem = const LocalFileSystem(), + }); + + final Platform platform; + final ProcessManager processManager; + final FileSystem filesystem; + + static FlutterInformation? _instance; + + static FlutterInformation get instance => _instance ??= FlutterInformation(); + + @visibleForTesting + static set instance(FlutterInformation? value) => _instance = value; + + /// The path to the Dart binary in the Flutter repo. + /// + /// This is probably a shell script. + File getDartBinaryPath() { + return getFlutterRoot().childDirectory('bin').childFile('dart'); + } + + /// The path to the Dart binary in the Flutter repo. + /// + /// This is probably a shell script. + File getFlutterBinaryPath() { + return getFlutterRoot().childDirectory('bin').childFile('flutter'); + } + + /// The path to the Flutter repo root directory. + /// + /// If the environment variable `FLUTTER_ROOT` is set, will use that instead + /// of looking for it. + /// + /// Otherwise, uses the output of `flutter --version --machine` to find the + /// Flutter root. + Directory getFlutterRoot() { + if (platform.environment['FLUTTER_ROOT'] != null) { + return filesystem.directory(platform.environment['FLUTTER_ROOT']); + } + return getFlutterInformation()['flutterRoot']! as Directory; + } + + /// Gets the semver version of the Flutter framework in the repo. + Version getFlutterVersion() => getFlutterInformation()['frameworkVersion']! as Version; + + /// Gets the git hash of the engine used by the Flutter framework in the repo. + String getEngineRevision() => getFlutterInformation()['engineRevision']! as String; + + /// Gets the value stored in bin/internal/engine.realm used by the Flutter + /// framework repo. + String getEngineRealm() => getFlutterInformation()['engineRealm']! as String; + + /// Gets the git hash of the Flutter framework in the repo. + String getFlutterRevision() => getFlutterInformation()['flutterGitRevision']! as String; + + /// Gets the name of the current branch in the Flutter framework in the repo. + String getBranchName() => getFlutterInformation()['branchName']! as String; + + Map? _cachedFlutterInformation; + + /// Gets a Map of various kinds of information about the Flutter repo. + Map getFlutterInformation() { + if (_cachedFlutterInformation != null) { + return _cachedFlutterInformation!; + } + + String flutterVersionJson; + if (platform.environment['FLUTTER_VERSION'] != null) { + flutterVersionJson = platform.environment['FLUTTER_VERSION']!; + } else { + // Determine which flutter command to run, which will determine which + // flutter root is eventually used. If the FLUTTER_ROOT is set, then use + // that flutter command, otherwise use the first one in the PATH. + String flutterCommand; + if (platform.environment['FLUTTER_ROOT'] != null) { + flutterCommand = filesystem + .directory(platform.environment['FLUTTER_ROOT']) + .childDirectory('bin') + .childFile('flutter') + .absolute + .path; + } else { + flutterCommand = 'flutter'; + } + ProcessResult result; + try { + result = processManager.runSync( + [flutterCommand, '--version', '--machine'], + stdoutEncoding: utf8, + ); + } on ProcessException catch (e) { + throw FlutterInformationException( + 'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place the ' + 'flutter command in your PATH.\n$e'); + } + if (result.exitCode != 0) { + throw FlutterInformationException( + 'Unable to determine Flutter information, because of abnormal exit of flutter command.'); + } + // Strip out any non-JSON that might be printed along with the command + // output. + flutterVersionJson = (result.stdout as String) + .replaceAll('Waiting for another flutter command to release the startup lock...', ''); + } + + final Map flutterVersion = json.decode(flutterVersionJson) as Map; + if (flutterVersion['flutterRoot'] == null || + flutterVersion['frameworkVersion'] == null || + flutterVersion['dartSdkVersion'] == null) { + throw FlutterInformationException( + 'Flutter command output has unexpected format, unable to determine flutter root location.'); + } + + final Map info = {}; + final Directory flutterRoot = filesystem.directory(flutterVersion['flutterRoot']! as String); + info['flutterRoot'] = flutterRoot; + info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String); + info['engineRevision'] = flutterVersion['engineRevision'] as String; + final File engineRealm = flutterRoot.childDirectory('bin').childDirectory('internal').childFile('engine.realm'); + info['engineRealm'] = engineRealm.existsSync() ? engineRealm.readAsStringSync().trim() : ''; + + final RegExpMatch? dartVersionRegex = RegExp(r'(?[\d.]+)(?:\s+\(build (?[-.\w]+)\))?') + .firstMatch(flutterVersion['dartSdkVersion'] as String); + if (dartVersionRegex == null) { + throw FlutterInformationException( + 'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.'); + } + info['dartSdkVersion'] = + Version.parse(dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!); + + info['branchName'] = _getBranchName(); + info['flutterGitRevision'] = _getFlutterGitRevision(); + _cachedFlutterInformation = info; + + return info; + } + + // Get the name of the release branch. + // + // On LUCI builds, the git HEAD is detached, so first check for the env + // variable "LUCI_BRANCH"; if it is not set, fall back to calling git. + String _getBranchName() { + final String? luciBranch = platform.environment['LUCI_BRANCH']; + if (luciBranch != null && luciBranch.trim().isNotEmpty) { + return luciBranch.trim(); + } + final ProcessResult gitResult = processManager.runSync(['git', 'status', '-b', '--porcelain']); + if (gitResult.exitCode != 0) { + throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; + } + final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); + final RegExpMatch? gitBranchMatch = + gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first); + return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; + } + + // Get the git revision for the repo. + String _getFlutterGitRevision() { + const int kGitRevisionLength = 10; + + final ProcessResult gitResult = processManager.runSync(['git', 'rev-parse', 'HEAD']); + if (gitResult.exitCode != 0) { + throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; + } + final String gitRevision = (gitResult.stdout as String).trim(); + + return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; + } +} diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart deleted file mode 100644 index b60c96133cf92..0000000000000 --- a/dev/tools/dartdoc.dart +++ /dev/null @@ -1,607 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:intl/intl.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as path; -import 'package:platform/platform.dart'; -import 'package:process/process.dart'; - -import 'dartdoc_checker.dart'; - -const String kDocsRoot = 'dev/docs'; -const String kPublishRoot = '$kDocsRoot/doc'; - -const String kDummyPackageName = 'Flutter'; -const String kPlatformIntegrationPackageName = 'platform_integration'; - -/// This script expects to run with the cwd as the root of the flutter repo. It -/// will generate documentation for the packages in `//packages/` and write the -/// documentation to `//dev/docs/doc/api/`. -/// -/// This script also updates the index.html file so that it can be placed -/// at the root of api.flutter.dev. We are keeping the files inside of -/// api.flutter.dev/flutter for now, so we need to manipulate paths -/// a bit. See https://github.com/flutter/flutter/issues/3900 for more info. -/// -/// This will only work on UNIX systems, not Windows. It requires that 'git' be -/// in your path. It requires that 'flutter' has been run previously. It uses -/// the version of Dart downloaded by the 'flutter' tool in this repository and -/// will crash if that is absent. -Future main(List arguments) async { - final ArgParser argParser = _createArgsParser(); - final ArgResults args = argParser.parse(arguments); - if (args['help'] as bool) { - print ('Usage:'); - print (argParser.usage); - exit(0); - } - // If we're run from the `tools` dir, set the cwd to the repo root. - if (path.basename(Directory.current.path) == 'tools') { - Directory.current = Directory.current.parent.parent; - } - - final ProcessResult flutter = Process.runSync('flutter', []); - final File versionFile = File('version'); - if (flutter.exitCode != 0 || !versionFile.existsSync()) { - throw Exception('Failed to determine Flutter version.'); - } - final String version = versionFile.readAsStringSync(); - - // Create the pubspec.yaml file. - final StringBuffer buf = StringBuffer(); - buf.writeln('name: $kDummyPackageName'); - buf.writeln('homepage: https://flutter.dev'); - buf.writeln('version: 0.0.0'); - buf.writeln('environment:'); - buf.writeln(" sdk: '>=3.0.0-0 <4.0.0'"); - buf.writeln('dependencies:'); - for (final String package in findPackageNames()) { - buf.writeln(' $package:'); - buf.writeln(' sdk: flutter'); - } - buf.writeln(' $kPlatformIntegrationPackageName: 0.0.1'); - buf.writeln('dependency_overrides:'); - buf.writeln(' $kPlatformIntegrationPackageName:'); - buf.writeln(' path: $kPlatformIntegrationPackageName'); - File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString()); - - // Create the library file. - final Directory libDir = Directory('$kDocsRoot/lib'); - libDir.createSync(); - - final StringBuffer contents = StringBuffer('library temp_doc;\n\n'); - for (final String libraryRef in libraryRefs()) { - contents.writeln("import 'package:$libraryRef';"); - } - File('$kDocsRoot/lib/temp_doc.dart').writeAsStringSync(contents.toString()); - - final String flutterRoot = Directory.current.path; - final Map pubEnvironment = { - 'FLUTTER_ROOT': flutterRoot, - }; - - // If there's a .pub-cache dir in the flutter root, use that. - final String pubCachePath = '$flutterRoot/.pub-cache'; - if (Directory(pubCachePath).existsSync()) { - pubEnvironment['PUB_CACHE'] = pubCachePath; - } - - final String dartExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/dart'; - - // Run pub. - ProcessWrapper process = ProcessWrapper(await runPubProcess( - dartBinaryPath: dartExecutable, - arguments: ['get'], - workingDirectory: kDocsRoot, - environment: pubEnvironment, - )); - printStream(process.stdout, prefix: 'pub:stdout: '); - printStream(process.stderr, prefix: 'pub:stderr: '); - final int code = await process.done; - if (code != 0) { - exit(code); - } - - createFooter('$kDocsRoot/lib/', version); - copyAssets(); - createSearchMetadata('$kDocsRoot/lib/opensearch.xml', '$kDocsRoot/doc/opensearch.xml'); - cleanOutSnippets(); - - final List dartdocBaseArgs = [ - 'global', - 'run', - if (args['checked'] as bool) '-c', - 'dartdoc', - ]; - - // Verify which version of snippets and dartdoc we're using. - final ProcessResult snippetsResult = Process.runSync( - dartExecutable, - [ - 'pub', - 'global', - 'list', - ], - workingDirectory: kDocsRoot, - environment: pubEnvironment, - stdoutEncoding: utf8, - ); - print(''); - final Iterable versionMatches = RegExp(r'^(?snippets|dartdoc) (?[^\s]+)', multiLine: true) - .allMatches(snippetsResult.stdout as String); - for (final RegExpMatch match in versionMatches) { - print('${match.namedGroup('name')} version: ${match.namedGroup('version')}'); - } - - print('flutter version: $version\n'); - - // Dartdoc warnings and errors in these packages are considered fatal. - // All packages owned by flutter should be in the list. - final List flutterPackages = [ - kDummyPackageName, - kPlatformIntegrationPackageName, - ...findPackageNames(), - // TODO(goderbauer): Figure out how to only include `dart:ui` of `sky_engine` below, https://github.com/dart-lang/dartdoc/issues/2278. - // 'sky_engine', - ]; - - // Generate the documentation. - // We don't need to exclude flutter_tools in this list because it's not in the - // recursive dependencies of the package defined at dev/docs/pubspec.yaml - final List dartdocArgs = [ - ...dartdocBaseArgs, - '--allow-tools', - if (args['json'] as bool) '--json', - if (args['validate-links'] as bool) '--validate-links' else '--no-validate-links', - '--link-to-source-excludes', '../../bin/cache', - '--link-to-source-root', '../..', - '--link-to-source-uri-template', 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', - '--inject-html', - '--use-base-href', - '--header', 'styles.html', - '--header', 'analytics.html', - '--header', 'survey.html', - '--header', 'snippets.html', - '--header', 'opensearch.html', - '--footer-text', 'lib/footer.html', - '--allow-warnings-in-packages', flutterPackages.join(','), - '--exclude-packages', - [ - 'analyzer', - 'args', - 'barback', - 'csslib', - 'flutter_goldens', - 'flutter_goldens_client', - 'front_end', - 'fuchsia_remote_debug_protocol', - 'glob', - 'html', - 'http_multi_server', - 'io', - 'isolate', - 'js', - 'kernel', - 'logging', - 'mime', - 'mockito', - 'node_preamble', - 'plugin', - 'shelf', - 'shelf_packages_handler', - 'shelf_static', - 'shelf_web_socket', - 'utf', - 'watcher', - 'yaml', - ].join(','), - '--exclude', - [ - 'dart:io/network_policy.dart', // dart-lang/dartdoc#2437 - 'package:Flutter/temp_doc.dart', - 'package:http/browser_client.dart', - 'package:intl/intl_browser.dart', - 'package:matcher/mirror_matchers.dart', - 'package:quiver/io.dart', - 'package:quiver/mirrors.dart', - 'package:vm_service_client/vm_service_client.dart', - 'package:web_socket_channel/html.dart', - ].join(','), - '--favicon=favicon.ico', - '--package-order', 'flutter,Dart,$kPlatformIntegrationPackageName,flutter_test,flutter_driver', - '--auto-include-dependencies', - ]; - - String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg; - print('Executing: (cd $kDocsRoot ; $dartExecutable ${dartdocArgs.map(quote).join(' ')})'); - - process = ProcessWrapper(await runPubProcess( - dartBinaryPath: dartExecutable, - arguments: dartdocArgs, - workingDirectory: kDocsRoot, - environment: pubEnvironment, - )); - printStream(process.stdout, prefix: args['json'] as bool ? '' : 'dartdoc:stdout: ', - filter: args['verbose'] as bool ? const [] : [ - RegExp(r'^generating docs for library '), // unnecessary verbosity - RegExp(r'^pars'), // unnecessary verbosity - ], - ); - printStream(process.stderr, prefix: args['json'] as bool ? '' : 'dartdoc:stderr: ', - filter: args['verbose'] as bool ? const [] : [ - RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control - ], - ); - final int exitCode = await process.done; - - if (exitCode != 0) { - exit(exitCode); - } - - sanityCheckDocs(); - checkForUnresolvedDirectives('$kPublishRoot/api'); - - createIndexAndCleanup(); -} - -ArgParser _createArgsParser() { - final ArgParser parser = ArgParser(); - parser.addFlag('help', abbr: 'h', negatable: false, - help: 'Show command help.'); - parser.addFlag('verbose', defaultsTo: true, - help: 'Whether to report all error messages (on) or attempt to ' - 'filter out some known false positives (off). Shut this off ' - 'locally if you want to address Flutter-specific issues.'); - parser.addFlag('checked', abbr: 'c', - help: 'Run dartdoc in checked mode.'); - parser.addFlag('json', - help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.'); - parser.addFlag('validate-links', - help: 'Display warnings for broken links generated by dartdoc (slow)'); - return parser; -} - -final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); - -/// Get the name of the release branch. -/// -/// On LUCI builds, the git HEAD is detached, so first check for the env -/// variable "LUCI_BRANCH"; if it is not set, fall back to calling git. -String getBranchName({ - @visibleForTesting - Platform platform = const LocalPlatform(), - @visibleForTesting - ProcessManager processManager = const LocalProcessManager(), -}) { - final String? luciBranch = platform.environment['LUCI_BRANCH']; - if (luciBranch != null && luciBranch.trim().isNotEmpty) { - return luciBranch.trim(); - } - final ProcessResult gitResult = processManager.runSync(['git', 'status', '-b', '--porcelain']); - if (gitResult.exitCode != 0) { - throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; - } - final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch( - (gitResult.stdout as String).trim().split('\n').first); - return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; -} - -String gitRevision() { - const int kGitRevisionLength = 10; - - final ProcessResult gitResult = Process.runSync('git', ['rev-parse', 'HEAD']); - if (gitResult.exitCode != 0) { - throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; - } - final String gitRevision = (gitResult.stdout as String).trim(); - - return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; -} - -void createFooter(String footerPath, String version) { - final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); - final String gitBranch = getBranchName(); - final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; - File('${footerPath}footer.html').writeAsStringSync(''); - File('$kPublishRoot/api/footer.js') - ..createSync(recursive: true) - ..writeAsStringSync(''' -(function() { - var span = document.querySelector('footer>span'); - if (span) { - span.innerText = 'Flutter $version • $timestamp • ${gitRevision()} $gitBranchOut'; - } - var sourceLink = document.querySelector('a.source-link'); - if (sourceLink) { - sourceLink.href = sourceLink.href.replace('/master/', '/${gitRevision()}/'); - } -})(); -'''); -} - -/// Generates an OpenSearch XML description that can be used to add a custom -/// search for Flutter API docs to the browser. Unfortunately, it has to know -/// the URL to which site to search, so we customize it here based upon the -/// branch name. -void createSearchMetadata(String templatePath, String metadataPath) { - final String template = File(templatePath).readAsStringSync(); - final String branch = getBranchName(); - final String metadata = template.replaceAll( - '{SITE_URL}', - branch == 'stable' ? 'https://api.flutter.dev/' : 'https://master-api.flutter.dev/', - ); - Directory(path.dirname(metadataPath)).create(recursive: true); - File(metadataPath).writeAsStringSync(metadata); -} - -/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if -/// specified, for each source/destination file pair. -/// -/// Creates `destDir` if needed. -void copyDirectorySync(Directory srcDir, Directory destDir, [void Function(File srcFile, File destFile)? onFileCopied]) { - if (!srcDir.existsSync()) { - throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); - } - - if (!destDir.existsSync()) { - destDir.createSync(recursive: true); - } - - for (final FileSystemEntity entity in srcDir.listSync()) { - final String newPath = path.join(destDir.path, path.basename(entity.path)); - if (entity is File) { - final File newFile = File(newPath); - entity.copySync(newPath); - onFileCopied?.call(entity, newFile); - } else if (entity is Directory) { - copyDirectorySync(entity, Directory(newPath)); - } else { - throw Exception('${entity.path} is neither File nor Directory'); - } - } -} - -void copyAssets() { - final Directory assetsDir = Directory(path.join(kPublishRoot, 'assets')); - if (assetsDir.existsSync()) { - assetsDir.deleteSync(recursive: true); - } - copyDirectorySync( - Directory(path.join(kDocsRoot, 'assets')), - Directory(path.join(kPublishRoot, 'assets')), - (File src, File dest) => print('Copied ${src.path} to ${dest.path}')); -} - -/// Clean out any existing snippets so that we don't publish old files from -/// previous runs accidentally. -void cleanOutSnippets() { - final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets')); - if (snippetsDir.existsSync()) { - snippetsDir - ..deleteSync(recursive: true) - ..createSync(recursive: true); - } -} - -void _sanityCheckExample(String fileString, String regExpString) { - final File file = File(fileString); - if (file.existsSync()) { - final RegExp regExp = RegExp(regExpString, dotAll: true); - final String contents = file.readAsStringSync(); - if (!regExp.hasMatch(contents)) { - throw Exception("Missing example code matching '$regExpString' in ${file.path}."); - } - } else { - throw Exception( - "Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file."); - } -} - -/// Runs a sanity check by running a test. -void sanityCheckDocs([Platform platform = const LocalPlatform()]) { - final List canaries = [ - '$kPublishRoot/assets/overrides.css', - '$kPublishRoot/api/dart-io/File-class.html', - '$kPublishRoot/api/dart-ui/Canvas-class.html', - '$kPublishRoot/api/dart-ui/Canvas/drawRect.html', - '$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html', - '$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html', - '$kPublishRoot/api/material/Material-class.html', - '$kPublishRoot/api/material/Tooltip-class.html', - '$kPublishRoot/api/widgets/Widget-class.html', - '$kPublishRoot/api/widgets/Listener-class.html', - ]; - for (final String canary in canaries) { - if (!File(canary).existsSync()) { - throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.'); - } - } - // Make sure at least one example of each kind includes source code. - - // Check a "sample" example, any one will do. - _sanityCheckExample( - '$kPublishRoot/api/widgets/showGeneralDialog.html', - r'\s*\s*import 'package:flutter/material.dart';', - ); - - // Check a "snippet" example, any one will do. - _sanityCheckExample( - '$kPublishRoot/api/widgets/ModalRoute/barrierColor.html', - r'\s*.*Color\s+get\s+barrierColor.*', - ); - - // Check a "dartpad" example, any one will do, and check for the correct URL - // arguments. - // Just use "master" for any branch other than the LUCI_BRANCH. - final String? luciBranch = platform.environment['LUCI_BRANCH']?.trim(); - final String expectedBranch = luciBranch != null && luciBranch.isNotEmpty ? luciBranch : 'master'; - final List argumentRegExps = [ - r'split=\d+', - r'run=true', - r'sample_id=widgets\.Listener\.\d+', - 'sample_channel=$expectedBranch', - 'channel=$expectedBranch', - ]; - for (final String argumentRegExp in argumentRegExps) { - _sanityCheckExample( - '$kPublishRoot/api/widgets/Listener-class.html', - r'\s*\s*<\/iframe>', - ); - } -} - -/// Creates a custom index.html because we try to maintain old -/// paths. Cleanup unused index.html files no longer needed. -void createIndexAndCleanup() { - print('\nCreating a custom index.html in $kPublishRoot/index.html'); - removeOldFlutterDocsDir(); - renameApiDir(); - copyIndexToRootOfDocs(); - addHtmlBaseToIndex(); - changePackageToSdkInTitlebar(); - putRedirectInOldIndexLocation(); - writeSnippetsIndexFile(); - print('\nDocs ready to go!'); -} - -void removeOldFlutterDocsDir() { - try { - Directory('$kPublishRoot/flutter').deleteSync(recursive: true); - } on FileSystemException { - // If the directory does not exist, that's OK. - } -} - -void renameApiDir() { - Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter'); -} - -void copyIndexToRootOfDocs() { - File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html'); -} - -void changePackageToSdkInTitlebar() { - final File indexFile = File('$kPublishRoot/index.html'); - String indexContents = indexFile.readAsStringSync(); - indexContents = indexContents.replaceFirst( - '
  • Flutter package
  • ', - '
  • Flutter SDK
  • ', - ); - - indexFile.writeAsStringSync(indexContents); -} - -void addHtmlBaseToIndex() { - final File indexFile = File('$kPublishRoot/index.html'); - String indexContents = indexFile.readAsStringSync(); - indexContents = indexContents.replaceFirst( - '\n', - '\n \n', - ); - indexContents = indexContents.replaceAll( - 'href="Android/Android-library.html"', - 'href="/javadoc/"', - ); - indexContents = indexContents.replaceAll( - 'href="iOS/iOS-library.html"', - 'href="/objcdoc/"', - ); - - indexFile.writeAsStringSync(indexContents); -} - -void putRedirectInOldIndexLocation() { - const String metaTag = ''; - File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag); -} - -void writeSnippetsIndexFile() { - final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets')); - if (snippetsDir.existsSync()) { - const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); - final Iterable files = snippetsDir - .listSync() - .whereType() - .where((File file) => path.extension(file.path) == '.json'); - // Combine all the metadata into a single JSON array. - final Iterable fileContents = files.map((File file) => file.readAsStringSync()); - final List metadataObjects = fileContents.map(json.decode).toList(); - final String jsonArray = jsonEncoder.convert(metadataObjects); - File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray); - } -} - -List findPackageNames() { - return findPackages().map((FileSystemEntity file) => path.basename(file.path)).toList(); -} - -/// Finds all packages in the Flutter SDK -List findPackages() { - return Directory('packages') - .listSync() - .where((FileSystemEntity entity) { - if (entity is! Directory) { - return false; - } - final File pubspec = File('${entity.path}/pubspec.yaml'); - if (!pubspec.existsSync()) { - print("Unexpected package '${entity.path}' found in packages directory"); - return false; - } - // TODO(ianh): Use a real YAML parser here - return !pubspec.readAsStringSync().contains('nodoc: true'); - }) - .cast() - .toList(); -} - -/// Returns import or on-disk paths for all libraries in the Flutter SDK. -Iterable libraryRefs() sync* { - for (final Directory dir in findPackages()) { - final String dirName = path.basename(dir.path); - for (final FileSystemEntity file in Directory('${dir.path}/lib').listSync()) { - if (file is File && file.path.endsWith('.dart')) { - yield '$dirName/${path.basename(file.path)}'; - } - } - } - - // Add a fake package for platform integration APIs. - yield '$kPlatformIntegrationPackageName/android.dart'; - yield '$kPlatformIntegrationPackageName/ios.dart'; -} - -void printStream(Stream> stream, { String prefix = '', List filter = const [] }) { - stream - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - if (!filter.any((Pattern pattern) => line.contains(pattern))) { - print('$prefix$line'.trim()); - } - }); -} - -Future runPubProcess({ - required String dartBinaryPath, - required List arguments, - String? workingDirectory, - Map? environment, - @visibleForTesting - ProcessManager processManager = const LocalProcessManager(), -}) { - return processManager.start( - [dartBinaryPath, 'pub', ...arguments], - workingDirectory: workingDirectory, - environment: environment, - ); -} diff --git a/dev/tools/dartdoc_checker.dart b/dev/tools/dartdoc_checker.dart index 75fbd2be38761..cbc47f8540e10 100644 --- a/dev/tools/dartdoc_checker.dart +++ b/dev/tools/dartdoc_checker.dart @@ -4,9 +4,31 @@ import 'dart:io'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -/// Scans the dartdoc HTML output in the provided `htmlOutputPath` for +/// Makes sure that the path we were given contains some of the expected +/// libraries. +@visibleForTesting +const List dartdocDirectiveCanaryLibraries = [ + 'animation', + 'cupertino', + 'material', + 'widgets', + 'rendering', + 'flutter_driver', +]; + +/// Makes sure that the path we were given contains some of the expected +/// HTML files. +@visibleForTesting +const List dartdocDirectiveCanaryFiles = [ + 'Widget-class.html', + 'Material-class.html', + 'Canvas-class.html', +]; + +/// Scans the dartdoc HTML output in the provided `dartDocDir` for /// unresolved dartdoc directives (`{@foo x y}`). /// /// Dartdoc usually replaces those directives with other content. However, @@ -22,27 +44,14 @@ import 'package:path/path.dart' as path; /// ``` /// void foo({@required int bar}); /// ``` -void checkForUnresolvedDirectives(String htmlOutputPath) { - final Directory dartDocDir = Directory(htmlOutputPath); +void checkForUnresolvedDirectives(Directory dartDocDir) { if (!dartDocDir.existsSync()) { throw Exception('Directory with dartdoc output (${dartDocDir.path}) does not exist.'); } - // Makes sure that the path we were given contains some of the expected - // libraries and HTML files. - final List canaryLibraries = [ - 'animation', - 'cupertino', - 'material', - 'widgets', - 'rendering', - 'flutter_driver', - ]; - final List canaryFiles = [ - 'Widget-class.html', - 'Material-class.html', - 'Canvas-class.html', - ]; + // Make a copy since this will be mutated + final List canaryLibraries = dartdocDirectiveCanaryLibraries.toList(); + final List canaryFiles = dartdocDirectiveCanaryFiles.toList(); print('Scanning for unresolved dartdoc directives...'); @@ -112,5 +121,5 @@ void main(List args) { if (!Directory(args.single).existsSync()) { throw Exception('The dartdoc HTML output directory ${args.single} does not exist.'); } - checkForUnresolvedDirectives(args.single); + checkForUnresolvedDirectives(Directory(args.single)); } diff --git a/dev/tools/examples_smoke_test.dart b/dev/tools/examples_smoke_test.dart index f4877ff11a222..84660fda38733 100644 --- a/dev/tools/examples_smoke_test.dart +++ b/dev/tools/examples_smoke_test.dart @@ -17,21 +17,21 @@ import 'package:path/path.dart' as path; import 'package:platform/platform.dart'; import 'package:process/process.dart'; -FileSystem filesystem = const LocalFileSystem(); -ProcessManager processManager = const LocalProcessManager(); -Platform platform = const LocalPlatform(); +const FileSystem _kFilesystem = LocalFileSystem(); +const ProcessManager _kProcessManager = LocalProcessManager(); +const Platform _kPlatform = LocalPlatform(); FutureOr main() async { - if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) { + if (!_kPlatform.isLinux && !_kPlatform.isWindows && !_kPlatform.isMacOS) { stderr.writeln('Example smoke tests are only designed to run on desktop platforms'); exitCode = 4; return; } - final Directory flutterDir = filesystem.directory( + final Directory flutterDir = _kFilesystem.directory( path.absolute( path.dirname( path.dirname( - path.dirname(platform.script.toFilePath()), + path.dirname(_kPlatform.script.toFilePath()), ), ), ), @@ -63,16 +63,16 @@ Future runSmokeTests({ required Directory apiDir, }) async { final File flutterExe = - flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter'); + flutterDir.childDirectory('bin').childFile(_kPlatform.isWindows ? 'flutter.bat' : 'flutter'); final List cmd = [ // If we're in a container with no X display, then use the virtual framebuffer. - if (platform.isLinux && - (platform.environment['DISPLAY'] == null || - platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run', + if (_kPlatform.isLinux && + (_kPlatform.environment['DISPLAY'] == null || + _kPlatform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run', flutterExe.absolute.path, 'test', '--reporter=expanded', - '--device-id=${platform.operatingSystem}', + '--device-id=${_kPlatform.operatingSystem}', integrationTest.absolute.path, ]; await runCommand(cmd, workingDirectory: apiDir); @@ -112,7 +112,7 @@ Future generateTest(Directory apiDir) async { .trim() .split('\n'); final Iterable examples = gitFiles.map((String examplePath) { - return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath)); + return _kFilesystem.file(path.join(examplesLibDir.absolute.path, examplePath)); }); // Collect the examples, and import them all as separate symbols. @@ -202,7 +202,7 @@ Future runCommand( } try { - process = await processManager.start( + process = await _kProcessManager.start( cmd, workingDirectory: workingDirectory.absolute.path, environment: environment, diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index ee3acd11f9453..6dc16b7d044ac 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -37,6 +37,7 @@ import 'package:gen_defaults/input_chip_template.dart'; import 'package:gen_defaults/input_decorator_template.dart'; import 'package:gen_defaults/list_tile_template.dart'; import 'package:gen_defaults/menu_template.dart'; +import 'package:gen_defaults/motion_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_drawer_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart'; @@ -131,6 +132,7 @@ Future main(List args) async { ListTileTemplate('LisTile', '$materialLib/list_tile.dart', tokens).updateFile(); InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile(); MenuTemplate('Menu', '$materialLib/menu_anchor.dart', tokens).updateFile(); + MotionTemplate('Motion', '$materialLib/motion.dart', tokens, tokenLogger).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationDrawerTemplate('NavigationDrawer', '$materialLib/navigation_drawer.dart', tokens).updateFile(); NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/generated/used_tokens.csv b/dev/tools/gen_defaults/generated/used_tokens.csv index fd5cf2e88c0e7..12754860bc581 100644 --- a/dev/tools/gen_defaults/generated/used_tokens.csv +++ b/dev/tools/gen_defaults/generated/used_tokens.csv @@ -530,6 +530,7 @@ md.comp.primary-navigation-tab.active.hover.state-layer.opacity, md.comp.primary-navigation-tab.active.pressed.state-layer.color, md.comp.primary-navigation-tab.active.pressed.state-layer.opacity, md.comp.primary-navigation-tab.divider.color, +md.comp.primary-navigation-tab.divider.height, md.comp.primary-navigation-tab.inactive.focus.state-layer.color, md.comp.primary-navigation-tab.inactive.focus.state-layer.opacity, md.comp.primary-navigation-tab.inactive.hover.state-layer.color, @@ -589,6 +590,7 @@ md.comp.search-view.header.supporting-text.color, md.comp.search-view.header.supporting-text.text-style, md.comp.secondary-navigation-tab.active.label-text.color, md.comp.secondary-navigation-tab.divider.color, +md.comp.secondary-navigation-tab.divider.height, md.comp.secondary-navigation-tab.focus.state-layer.color, md.comp.secondary-navigation-tab.focus.state-layer.opacity, md.comp.secondary-navigation-tab.hover.state-layer.color, @@ -737,6 +739,7 @@ md.comp.time-picker.period-selector.selected.label-text.color, md.comp.time-picker.period-selector.selected.pressed.label-text.color, md.comp.time-picker.period-selector.unselected.focus.label-text.color, md.comp.time-picker.period-selector.unselected.hover.label-text.color, +md.comp.time-picker.period-selector.unselected.label-text.color, md.comp.time-picker.period-selector.unselected.pressed.label-text.color, md.comp.time-picker.period-selector.vertical.container.height, md.comp.time-picker.period-selector.vertical.container.width, @@ -746,7 +749,6 @@ md.comp.time-picker.time-selector.container.shape, md.comp.time-picker.time-selector.container.width, md.comp.time-picker.time-selector.focus.state-layer.opacity, md.comp.time-picker.time-selector.hover.state-layer.opacity, -md.comp.time-picker.time-selector.label-text.text-style, md.comp.time-picker.time-selector.selected.container.color, md.comp.time-picker.time-selector.selected.focus.label-text.color, md.comp.time-picker.time-selector.selected.focus.state-layer.color, @@ -856,6 +858,31 @@ md.sys.elevation.level2, md.sys.elevation.level3, md.sys.elevation.level4, md.sys.elevation.level5, +md.sys.motion.duration.extra-long1Ms, +md.sys.motion.duration.extra-long2Ms, +md.sys.motion.duration.extra-long3Ms, +md.sys.motion.duration.extra-long4Ms, +md.sys.motion.duration.long1Ms, +md.sys.motion.duration.long2Ms, +md.sys.motion.duration.long3Ms, +md.sys.motion.duration.long4Ms, +md.sys.motion.duration.medium1Ms, +md.sys.motion.duration.medium2Ms, +md.sys.motion.duration.medium3Ms, +md.sys.motion.duration.medium4Ms, +md.sys.motion.duration.short1Ms, +md.sys.motion.duration.short2Ms, +md.sys.motion.duration.short3Ms, +md.sys.motion.duration.short4Ms, +md.sys.motion.easing.emphasized.accelerate, +md.sys.motion.easing.emphasized.decelerate, +md.sys.motion.easing.legacy, +md.sys.motion.easing.legacy.accelerate, +md.sys.motion.easing.legacy.decelerate, +md.sys.motion.easing.linear, +md.sys.motion.easing.standard, +md.sys.motion.easing.standard.accelerate, +md.sys.motion.easing.standard.decelerate, md.sys.shape.corner.extra-large, md.sys.shape.corner.extra-large.top, md.sys.shape.corner.extra-small, diff --git a/dev/tools/gen_defaults/lib/action_chip_template.dart b/dev/tools/gen_defaults/lib/action_chip_template.dart index 54027c13d75d2..ade6164e6334b 100644 --- a/dev/tools/gen_defaults/lib/action_chip_template.dart +++ b/dev/tools/gen_defaults/lib/action_chip_template.dart @@ -91,7 +91,7 @@ class _${blockName}DefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } '''; diff --git a/dev/tools/gen_defaults/lib/chip_template.dart b/dev/tools/gen_defaults/lib/chip_template.dart index 8d9c05b7ccb73..4d9383399d16d 100644 --- a/dev/tools/gen_defaults/lib/chip_template.dart +++ b/dev/tools/gen_defaults/lib/chip_template.dart @@ -70,7 +70,7 @@ class _${blockName}DefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } '''; diff --git a/dev/tools/gen_defaults/lib/date_picker_template.dart b/dev/tools/gen_defaults/lib/date_picker_template.dart index 6f45fdf35f67b..e1c8836dee8f4 100644 --- a/dev/tools/gen_defaults/lib/date_picker_template.dart +++ b/dev/tools/gen_defaults/lib/date_picker_template.dart @@ -56,6 +56,16 @@ class _${blockName}DefaultsM3 extends DatePickerThemeData { @override Color? get backgroundColor => ${componentColor("md.comp.date-picker.modal.container")}; + @override + ButtonStyle get cancelButtonStyle { + return TextButton.styleFrom(); + } + + @override + ButtonStyle get confirmButtonStyle { + return TextButton.styleFrom(); + } + @override Color? get shadowColor => ${colorOrTransparent("md.comp.date-picker.modal.container.shadow-color")}; @@ -231,8 +241,6 @@ class _${blockName}DefaultsM3 extends DatePickerThemeData { @override TextStyle? get rangePickerHeaderHelpStyle => ${textStyle("md.comp.date-picker.modal.range-selection.month.subhead")}; - - } '''; } diff --git a/dev/tools/gen_defaults/lib/filter_chip_template.dart b/dev/tools/gen_defaults/lib/filter_chip_template.dart index 6609613cde24e..d13cba9cff9e3 100644 --- a/dev/tools/gen_defaults/lib/filter_chip_template.dart +++ b/dev/tools/gen_defaults/lib/filter_chip_template.dart @@ -108,7 +108,7 @@ class _${blockName}DefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } '''; diff --git a/dev/tools/gen_defaults/lib/input_chip_template.dart b/dev/tools/gen_defaults/lib/input_chip_template.dart index 245a171b9073d..529113cbbfa42 100644 --- a/dev/tools/gen_defaults/lib/input_chip_template.dart +++ b/dev/tools/gen_defaults/lib/input_chip_template.dart @@ -85,7 +85,7 @@ class _${blockName}DefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } '''; diff --git a/dev/tools/gen_defaults/lib/menu_template.dart b/dev/tools/gen_defaults/lib/menu_template.dart index 50dc6c4e2f708..6d2d6b443057a 100644 --- a/dev/tools/gen_defaults/lib/menu_template.dart +++ b/dev/tools/gen_defaults/lib/menu_template.dart @@ -65,6 +65,7 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + late final TextTheme _textTheme = Theme.of(context).textTheme; @override MaterialStateProperty? get backgroundColor { @@ -180,7 +181,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty get textStyle { - return MaterialStatePropertyAll(${textStyle('md.comp.list.list-item.label-text')}); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + return MaterialStatePropertyAll(_textTheme.labelLarge); } @override diff --git a/dev/tools/gen_defaults/lib/motion_template.dart b/dev/tools/gen_defaults/lib/motion_template.dart new file mode 100644 index 0000000000000..0683244133c91 --- /dev/null +++ b/dev/tools/gen_defaults/lib/motion_template.dart @@ -0,0 +1,90 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'template.dart'; +import 'token_logger.dart'; + +class MotionTemplate extends TokenTemplate { + /// Since we generate the tokens dynamically, we need to store them and log + /// them manually, instead of using [getToken]. + MotionTemplate(String blockName, String fileName, this.tokens, this.tokensLogger) : super(blockName, fileName, tokens); + Map tokens; + TokenLogger tokensLogger; + + // List of duration tokens. + late List> durationTokens = tokens.entries.where( + (MapEntry entry) => entry.key.contains('.duration.') + ).toList() + ..sort( + (MapEntry a, MapEntry b) => (a.value as double).compareTo(b.value as double) + ); + + // List of easing curve tokens. + late List> easingCurveTokens = tokens.entries.where( + (MapEntry entry) => entry.key.contains('.easing.') + ).toList() + ..sort( + // Sort the legacy curves at the end of the list. + (MapEntry a, MapEntry b) => a.key.contains('legacy') ? 1 : a.key.compareTo(b.key) + ); + + String durationTokenString(String token, dynamic tokenValue) { + tokensLogger.log(token); + final String tokenName = token.split('.').last.replaceAll('-', '').replaceFirst('Ms', ''); + final int milliseconds = (tokenValue as double).toInt(); + return +''' + /// The $tokenName duration (${milliseconds}ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration $tokenName = Duration(milliseconds: $milliseconds); +'''; + } + + String easingCurveTokenString(String token, dynamic tokenValue) { + tokensLogger.log(token); + final String tokenName = token + .replaceFirst('md.sys.motion.easing.', '') + .replaceAllMapped(RegExp(r'[-\.](\w)'), (Match match) { + return match.group(1)!.toUpperCase(); + }); + return ''' + /// The $tokenName easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve $tokenName = $tokenValue; +'''; + } + + @override + String generate() => ''' +/// The set of durations in the Material specification. +/// +/// See also: +/// +/// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) +/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) +abstract final class Durations { +${durationTokens.map((MapEntry entry) => durationTokenString(entry.key, entry.value)).join('\n')}} + + +// TODO(guidezpl): Improve with description and assets, b/289870605 + +/// The set of easing curves in the Material specification. +/// +/// See also: +/// +/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) +/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) +/// * [Curves], for a collection of non-Material animation easing curves. +abstract final class Easing { +${easingCurveTokens.map((MapEntry entry) => easingCurveTokenString(entry.key, entry.value)).join('\n')}} +'''; +} diff --git a/dev/tools/gen_defaults/lib/popup_menu_template.dart b/dev/tools/gen_defaults/lib/popup_menu_template.dart index f11b89c9500e9..bed11d0b8b09a 100644 --- a/dev/tools/gen_defaults/lib/popup_menu_template.dart +++ b/dev/tools/gen_defaults/lib/popup_menu_template.dart @@ -42,5 +42,9 @@ class _${blockName}DefaultsM3 extends PopupMenuThemeData { @override ShapeBorder? get shape => ${shape("md.comp.menu.container")}; + + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 12.0); }'''; } diff --git a/dev/tools/gen_defaults/lib/search_bar_template.dart b/dev/tools/gen_defaults/lib/search_bar_template.dart index 5a6d7a660c1ac..670121b9cb505 100644 --- a/dev/tools/gen_defaults/lib/search_bar_template.dart +++ b/dev/tools/gen_defaults/lib/search_bar_template.dart @@ -71,6 +71,9 @@ class _SearchBarDefaultsM3 extends SearchBarThemeData { @override BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: ${getToken('md.comp.search-bar.container.height')}); + + @override + TextCapitalization get textCapitalization => TextCapitalization.none; } '''; } diff --git a/dev/tools/gen_defaults/lib/tabs_template.dart b/dev/tools/gen_defaults/lib/tabs_template.dart index f6388a87458d3..f520f617b2012 100644 --- a/dev/tools/gen_defaults/lib/tabs_template.dart +++ b/dev/tools/gen_defaults/lib/tabs_template.dart @@ -24,6 +24,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")}; + @override + double? get dividerHeight => ${getToken('md.comp.primary-navigation-tab.divider.height')}; + @override Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; @@ -71,7 +74,7 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override - TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; + TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')}; } @@ -88,6 +91,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")}; + @override + double? get dividerHeight => ${getToken('md.comp.secondary-navigation-tab.divider.height')}; + @override Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; @@ -135,7 +141,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override - TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; + TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; } '''; diff --git a/dev/tools/gen_defaults/lib/time_picker_template.dart b/dev/tools/gen_defaults/lib/time_picker_template.dart index b1aa65ecda63e..4d60175f88281 100644 --- a/dev/tools/gen_defaults/lib/time_picker_template.dart +++ b/dev/tools/gen_defaults/lib/time_picker_template.dart @@ -84,44 +84,28 @@ class _${blockName}DefaultsM3 extends _TimePickerDefaults { @override Color get dayPeriodTextColor { return MaterialStateColor.resolveWith((Set states) { - return _dayPeriodForegroundColor.resolve(states); - }); - } - - MaterialStateProperty get _dayPeriodForegroundColor { - return MaterialStateProperty.resolveWith((Set states) { - Color? textColor; if (states.contains(MaterialState.selected)) { - if (states.contains(MaterialState.pressed)) { - textColor = ${componentColor("$dayPeriodComponent.selected.pressed.label-text")}; - } else { - // not pressed - if (states.contains(MaterialState.hovered)) { - textColor = ${componentColor("$dayPeriodComponent.selected.hover.label-text")}; - } else { - // not hovered - if (states.contains(MaterialState.focused)) { - textColor = ${componentColor("$dayPeriodComponent.selected.focus.label-text")}; - } - } + if (states.contains(MaterialState.focused)) { + return ${componentColor("$dayPeriodComponent.selected.focus.label-text")}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor("$dayPeriodComponent.selected.hover.label-text")}; } - } else { - // unselected if (states.contains(MaterialState.pressed)) { - textColor = ${componentColor("$dayPeriodComponent.unselected.pressed.label-text")}; - } else { - // not pressed - if (states.contains(MaterialState.hovered)) { - textColor = ${componentColor("$dayPeriodComponent.unselected.hover.label-text")}; - } else { - // not hovered - if (states.contains(MaterialState.focused)) { - textColor = ${componentColor("$dayPeriodComponent.unselected.focus.label-text")}; - } - } + return ${componentColor("$dayPeriodComponent.selected.pressed.label-text")}; } + return ${componentColor("$dayPeriodComponent.selected.label-text")}; + } + if (states.contains(MaterialState.focused)) { + return ${componentColor("$dayPeriodComponent.unselected.focus.label-text")}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor("$dayPeriodComponent.unselected.hover.label-text")}; + } + if (states.contains(MaterialState.pressed)) { + return ${componentColor("$dayPeriodComponent.unselected.pressed.label-text")}; } - return textColor ?? ${componentColor("$dayPeriodComponent.selected.label-text")}; + return ${componentColor("$dayPeriodComponent.unselected.label-text")}; }); } @@ -132,7 +116,7 @@ class _${blockName}DefaultsM3 extends _TimePickerDefaults { @override Color get dialBackgroundColor { - return ${componentColor(dialComponent)}.withOpacity(_colors.brightness == Brightness.dark ? 0.12 : 0.08); + return ${componentColor(dialComponent)}; } @override @@ -297,7 +281,10 @@ class _${blockName}DefaultsM3 extends _TimePickerDefaults { @override TextStyle get hourMinuteTextStyle { return MaterialStateTextStyle.resolveWith((Set states) { - return ${textStyle('$hourMinuteComponent.label-text')}!.copyWith(color: _hourMinuteTextColor.resolve(states)); + // TODO(tahatesser): Update this when https://github.com/flutter/flutter/issues/131247 is fixed. + // This is using the correct text style from Material 3 spec. + // https://m3.material.io/components/time-pickers/specs#fd0b6939-edab-4058-82e1-93d163945215 + return _textTheme.displayMedium!.copyWith(color: _hourMinuteTextColor.resolve(states)); }); } diff --git a/dev/tools/gen_defaults/pubspec.yaml b/dev/tools/gen_defaults/pubspec.yaml index c4665a8c1d9ed..15656eda02485 100644 --- a/dev/tools/gen_defaults/pubspec.yaml +++ b/dev/tools/gen_defaults/pubspec.yaml @@ -3,20 +3,20 @@ description: A command line script to generate Material component defaults from version: 1.0.0 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: args: 2.4.2 dev_dependencies: path: 1.8.3 - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -29,7 +29,7 @@ dev_dependencies: js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -42,17 +42,17 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: df79 +# PUBSPEC CHECKSUM: 80a4 diff --git a/dev/tools/gen_keycodes/lib/physical_key_data.dart b/dev/tools/gen_keycodes/lib/physical_key_data.dart index 8bc7065ea8c08..b71a7cba1510a 100644 --- a/dev/tools/gen_keycodes/lib/physical_key_data.dart +++ b/dev/tools/gen_keycodes/lib/physical_key_data.dart @@ -214,8 +214,6 @@ class PhysicalKeyData { /// written with the [toJson] method. class PhysicalKeyEntry { /// Creates a single key entry from available data. - /// - /// The [usbHidCode] and [chromiumName] parameters must not be null. PhysicalKeyEntry({ required this.usbHidCode, required this.name, diff --git a/dev/tools/gen_keycodes/pubspec.yaml b/dev/tools/gen_keycodes/pubspec.yaml index df0953cc1d728..3e6a67130af6f 100644 --- a/dev/tools/gen_keycodes/pubspec.yaml +++ b/dev/tools/gen_keycodes/pubspec.yaml @@ -2,17 +2,17 @@ name: gen_keycodes description: Generates keycode source files from various resources. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: args: 2.4.2 http: 0.13.6 - meta: 1.9.1 + meta: 1.10.0 path: 1.8.3 - platform: 3.1.0 + platform: 3.1.2 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -20,11 +20,11 @@ dependencies: typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 - test_api: 0.6.0 + test: 1.24.6 + test_api: 0.6.1 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -48,13 +48,13 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 5270 +# PUBSPEC CHECKSUM: 9e9d diff --git a/dev/tools/java_and_objc_doc.dart b/dev/tools/java_and_objc_doc.dart deleted file mode 100644 index af8b745baadbe..0000000000000 --- a/dev/tools/java_and_objc_doc.dart +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'dart:math'; - -import 'package:archive/archive.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; - -const String kDocRoot = 'dev/docs/doc'; - -/// This script downloads an archive of Javadoc and objc doc for the engine from -/// the artifact store and extracts them to the location used for Dartdoc. -Future main(List args) async { - final String engineVersion = File('bin/internal/engine.version').readAsStringSync().trim(); - - final String javadocUrl = 'https://storage.googleapis.com/flutter_infra_release/flutter/$engineVersion/android-javadoc.zip'; - generateDocs(javadocUrl, 'javadoc', 'io/flutter/view/FlutterView.html'); - - final String objcdocUrl = 'https://storage.googleapis.com/flutter_infra_release/flutter/$engineVersion/ios-objcdoc.zip'; - generateDocs(objcdocUrl, 'objcdoc', 'Classes/FlutterViewController.html'); -} - -/// Fetches the zip archive at the specified url. -/// -/// Returns null if the archive fails to download after [maxTries] attempts. -Future fetchArchive(String url, int maxTries) async { - List? responseBytes; - for (int i = 0; i < maxTries; i++) { - final http.Response response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - responseBytes = response.bodyBytes; - break; - } - stderr.writeln('Failed attempt ${i+1} to fetch $url.'); - - // On failure print a short snipped from the body in case it's helpful. - final int bodyLength = min(1024, response.body.length); - stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}'); - sleep(const Duration(seconds: 1)); - } - return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes); -} - -Future generateDocs(String url, String docName, String checkFile) async { - const int maxTries = 5; - final Archive? archive = await fetchArchive(url, maxTries); - if (archive == null) { - stderr.writeln('Failed to fetch zip archive from: $url after $maxTries attempts. Giving up.'); - exit(1); - } - - final Directory output = Directory('$kDocRoot/$docName'); - print('Extracting $docName to ${output.path}'); - output.createSync(recursive: true); - - for (final ArchiveFile af in archive) { - if (!af.name.endsWith('/')) { - final File file = File('${output.path}/${af.name}'); - file.createSync(recursive: true); - file.writeAsBytesSync(af.content as List); - } - } - - /// If object then copy files to old location if the archive is using the new location. - final bool exists = Directory('$kDocRoot/$docName/objectc_docs').existsSync(); - if (exists) { - copyFolder(Directory('$kDocRoot/$docName/objectc_docs'), Directory('$kDocRoot/$docName/')); - } - - final File testFile = File('${output.path}/$checkFile'); - if (!testFile.existsSync()) { - print('Expected file ${testFile.path} not found'); - exit(1); - } - print('$docName ready to go!'); -} - -/// Copies the files in a directory recursively to a new location. -void copyFolder(Directory source, Directory destination) { - source.listSync() - .forEach((FileSystemEntity entity) { - if (entity is Directory) { - final Directory newDirectory = Directory(path.join(destination.absolute.path, path.basename(entity.path))); - newDirectory.createSync(); - copyFolder(entity.absolute, newDirectory); - } else if (entity is File) { - entity.copySync(path.join(destination.path, path.basename(entity.path))); - } - }); -} diff --git a/dev/tools/localization/bin/gen_localizations.dart b/dev/tools/localization/bin/gen_localizations.dart index 91490fec321c4..af70e2368955f 100644 --- a/dev/tools/localization/bin/gen_localizations.dart +++ b/dev/tools/localization/bin/gen_localizations.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This program generates a getMaterialTranslation() and a -// getCupertinoTranslation() function that look up the translations provided by +// This program generates getMaterialTranslation(), getCupertinoTranslation(), +// and getWidgetsTranslation() functions that look up the translations provided by // the arb files. The returned value is a generated instance of a -// GlobalMaterialLocalizations or a GlobalCupertinoLocalizations that -// corresponds to a single locale. +// GlobalMaterialLocalizations, GlobalCupertinoLocalizations, or +// GlobalWidgetsLocalizations object that corresponds to a single locale. // // The *.arb files are in packages/flutter_localizations/lib/src/l10n. // @@ -40,8 +40,8 @@ // ``` // // If the data looks good, use the `-w` or `--overwrite` option to overwrite the -// packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart -// and packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart file: +// generated_material_localizations.dart, generated_cupertino_localizations.dart, +// and generated_widgets_localizations.dart files in packages/flutter_localizations/lib/src/l10n/: // // ``` // dart dev/tools/localization/bin/gen_localizations.dart --overwrite @@ -543,19 +543,19 @@ void main(List rawArgs) { // Maps of locales to resource key/value pairs for Widgets ARBs. final Map> widgetsLocaleToResources = >{}; - // Maps of locales to resource key/attributes pairs for Widgets ARBs.. + // Maps of locales to resource key/attributes pairs for Widgets ARBs. // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes final Map> widgetsLocaleToResourceAttributes = >{}; // Maps of locales to resource key/value pairs for Material ARBs. final Map> materialLocaleToResources = >{}; - // Maps of locales to resource key/attributes pairs for Material ARBs.. + // Maps of locales to resource key/attributes pairs for Material ARBs. // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes final Map> materialLocaleToResourceAttributes = >{}; // Maps of locales to resource key/value pairs for Cupertino ARBs. final Map> cupertinoLocaleToResources = >{}; - // Maps of locales to resource key/attributes pairs for Cupertino ARBs.. + // Maps of locales to resource key/attributes pairs for Cupertino ARBs. // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes final Map> cupertinoLocaleToResourceAttributes = >{}; diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index 13c5c0edbb227..e3a4cac9649f2 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -2,35 +2,36 @@ name: dev_tools description: Various repository development tools for flutter. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: archive: 3.3.2 args: 2.4.2 http: 0.13.6 intl: 0.18.1 - meta: 1.9.1 + meta: 1.10.0 path: 1.8.3 process: 4.2.4 + pub_semver: 2.1.4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 - test_api: 0.6.0 + test: 1.24.6 + test_api: 0.6.1 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -45,20 +46,19 @@ dev_dependencies: node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: cc8a +# PUBSPEC CHECKSUM: 64b7 diff --git a/dev/tools/test/create_api_docs_test.dart b/dev/tools/test/create_api_docs_test.dart new file mode 100644 index 0000000000000..bc557b157a6a0 --- /dev/null +++ b/dev/tools/test/create_api_docs_test.dart @@ -0,0 +1,442 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; +import '../create_api_docs.dart' as apidocs; +import '../dartdoc_checker.dart'; + +void main() { + group('FlutterInformation', () { + late FakeProcessManager fakeProcessManager; + late FakePlatform fakePlatform; + late MemoryFileSystem memoryFileSystem; + late apidocs.FlutterInformation flutterInformation; + + void setUpWithEnvironment(Map environment) { + fakePlatform = FakePlatform(environment: environment); + flutterInformation = apidocs.FlutterInformation( + filesystem: memoryFileSystem, + processManager: fakeProcessManager, + platform: fakePlatform, + ); + apidocs.FlutterInformation.instance = flutterInformation; + } + + setUp(() { + fakeProcessManager = FakeProcessManager.empty(); + memoryFileSystem = MemoryFileSystem(); + setUpWithEnvironment({}); + }); + + test('getBranchName does not call git if env LUCI_BRANCH provided', () { + setUpWithEnvironment( + { + 'LUCI_BRANCH': branchName, + }, + ); + fakeProcessManager.addCommand(const FakeCommand( + command: ['flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + expect( + apidocs.FlutterInformation.instance.getBranchName(), + branchName, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + test('getBranchName calls git if env LUCI_BRANCH not provided', () { + fakeProcessManager.addCommand(const FakeCommand( + command: ['flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + + expect( + apidocs.FlutterInformation.instance.getBranchName(), + branchName, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + test('getBranchName calls git if env LUCI_BRANCH is empty', () { + setUpWithEnvironment( + { + 'LUCI_BRANCH': '', + }, + ); + fakeProcessManager.addCommand(const FakeCommand( + command: ['flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + + expect( + apidocs.FlutterInformation.instance.getBranchName(), + branchName, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + test("runPubProcess doesn't use the pub binary", () { + final Platform platform = FakePlatform( + environment: { + 'FLUTTER_ROOT': '/flutter', + }, + ); + final ProcessManager processManager = FakeProcessManager.list( + [ + const FakeCommand( + command: ['/flutter/bin/flutter', 'pub', '--one', '--two'], + ), + ], + ); + apidocs.FlutterInformation.instance = + apidocs.FlutterInformation(platform: platform, processManager: processManager, filesystem: memoryFileSystem); + + apidocs.runPubProcess( + arguments: ['--one', '--two'], + processManager: processManager, + filesystem: memoryFileSystem, + ); + + expect(processManager, hasNoRemainingExpectations); + }); + + test('calls out to flutter if FLUTTER_VERSION is not set', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + final Map info = flutterInformation.getFlutterInformation(); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); + }); + test("doesn't call out to flutter if FLUTTER_VERSION is set", () async { + setUpWithEnvironment({ + 'FLUTTER_VERSION': testVersionInfo, + }); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + final Map info = flutterInformation.getFlutterInformation(); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); + }); + test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + final Directory root = flutterInformation.getFlutterRoot(); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(root.path, equals('/home/user/flutter')); + }); + test("getFlutterRoot doesn't call out to flutter if FLUTTER_ROOT is set", () async { + setUpWithEnvironment({'FLUTTER_ROOT': '/home/user/flutter'}); + final Directory root = flutterInformation.getFlutterRoot(); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(root.path, equals('/home/user/flutter')); + }); + test('parses version properly', () async { + fakePlatform.environment['FLUTTER_VERSION'] = testVersionInfo; + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + final Map info = flutterInformation.getFlutterInformation(); + expect(info['frameworkVersion'], isNotNull); + expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); + expect(info['dartSdkVersion'], isNotNull); + expect(info['dartSdkVersion'], equals(Version.parse('2.14.0-360.0.dev'))); + }); + test('the engine realm is read from the engine.realm file', () async { + final Directory flutterHome = memoryFileSystem + .directory('/home') + .childDirectory('user') + .childDirectory('flutter') + .childDirectory('bin') + .childDirectory('internal'); + flutterHome.childFile('engine.realm') + ..createSync(recursive: true) + ..writeAsStringSync('realm'); + setUpWithEnvironment({'FLUTTER_ROOT': '/home/user/flutter'}); + fakeProcessManager.addCommand(const FakeCommand( + command: ['/home/user/flutter/bin/flutter', '--version', '--machine'], + stdout: testVersionInfo, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + )); + final Map info = flutterInformation.getFlutterInformation(); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(info['engineRealm'], equals('realm')); + }); + }); + + group('DartDocGenerator', () { + late apidocs.DartdocGenerator generator; + late MemoryFileSystem fs; + late FakeProcessManager processManager; + late Directory publishRoot; + + setUp(() { + fs = MemoryFileSystem.test(); + publishRoot = fs.directory('/path/to/publish'); + processManager = FakeProcessManager.empty(); + generator = apidocs.DartdocGenerator( + packageRoot: fs.directory('/path/to/package'), + publishRoot: publishRoot, + docsRoot: fs.directory('/path/to/docs'), + filesystem: fs, + processManager: processManager, + ); + final Directory repoRoot = fs.directory('/flutter'); + repoRoot.childDirectory('packages').createSync(recursive: true); + apidocs.FlutterInformation.instance = apidocs.FlutterInformation( + filesystem: fs, + processManager: processManager, + platform: FakePlatform(environment: { + 'FLUTTER_ROOT': repoRoot.path, + }), + ); + }); + + test('.generateDartDoc() invokes dartdoc with the correct command line arguments', () async { + processManager.addCommands([ + const FakeCommand(command: ['/flutter/bin/flutter', 'pub', 'get']), + const FakeCommand( + command: ['/flutter/bin/flutter', '--version', '--machine'], + stdout: testVersionInfo, + ), + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + ), + const FakeCommand( + command: ['/flutter/bin/flutter', 'pub', 'global', 'list'], + ), + FakeCommand( + command: [ + '/flutter/bin/flutter', + 'pub', + 'global', + 'run', + '--enable-asserts', + 'dartdoc', + '--output', + '/path/to/publish/flutter', + '--allow-tools', + '--json', + '--validate-links', + '--link-to-source-excludes', + '/flutter/bin/cache', + '--link-to-source-root', + '/flutter', + '--link-to-source-uri-template', + 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', + '/path/to/docs/styles.html', + '--header', + '/path/to/docs/analytics-header.html', + '--header', + '/path/to/docs/survey.html', + '--header', + '/path/to/docs/snippets.html', + '--header', + '/path/to/docs/opensearch.html', + '--footer', + '/path/to/docs/analytics-footer.html', + '--footer-text', + '/path/to/package/footer.html', + '--allow-warnings-in-packages', + // match package names + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude-packages', + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude', + // match dart package URIs + RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'), + '--favicon', + '/path/to/docs/favicon.ico', + '--package-order', + 'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver', + '--auto-include-dependencies', + ], + ), + ]); + + // This will throw while sanity checking generated files, which is tested independently + await expectLater( + () => generator.generateDartdoc(), + throwsA( + isA().having( + (Exception e) => e.toString(), + 'message', + contains(RegExp(r'Missing .* which probably means the documentation failed to build correctly.')), + ), + ), + ); + + expect(processManager, hasNoRemainingExpectations); + }); + + test('sanity checks spot check generated files', () async { + processManager.addCommands([ + const FakeCommand(command: ['/flutter/bin/flutter', 'pub', 'get']), + const FakeCommand( + command: ['/flutter/bin/flutter', '--version', '--machine'], + stdout: testVersionInfo, + ), + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + ), + const FakeCommand( + command: ['/flutter/bin/flutter', 'pub', 'global', 'list'], + ), + FakeCommand( + command: [ + '/flutter/bin/flutter', + 'pub', + 'global', + 'run', + '--enable-asserts', + 'dartdoc', + '--output', + '/path/to/publish/flutter', + '--allow-tools', + '--json', + '--validate-links', + '--link-to-source-excludes', + '/flutter/bin/cache', + '--link-to-source-root', + '/flutter', + '--link-to-source-uri-template', + 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', + '/path/to/docs/styles.html', + '--header', + '/path/to/docs/analytics-header.html', + '--header', + '/path/to/docs/survey.html', + '--header', + '/path/to/docs/snippets.html', + '--header', + '/path/to/docs/opensearch.html', + '--footer', + '/path/to/docs/analytics-footer.html', + '--footer-text', + '/path/to/package/footer.html', + '--allow-warnings-in-packages', + // match package names + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude-packages', + RegExp(r'^(\w+,)+(\w+)$'), + '--exclude', + // match dart package URIs + RegExp(r'^([\w\/:.]+,)+([\w\/:.]+)$'), + '--favicon', + '/path/to/docs/favicon.ico', + '--package-order', + 'flutter,Dart,${apidocs.kPlatformIntegrationPackageName},flutter_test,flutter_driver', + '--auto-include-dependencies', + ], + onRun: () { + for (final File canary in generator.canaries) { + canary.createSync(recursive: true); + } + for (final String path in dartdocDirectiveCanaryFiles) { + publishRoot.childDirectory('flutter').childFile(path).createSync(recursive: true); + } + for (final String path in dartdocDirectiveCanaryLibraries) { + publishRoot.childDirectory('flutter').childDirectory(path).createSync(recursive: true); + } + publishRoot.childDirectory('flutter').childFile('index.html').createSync(); + + final Directory widgetsDir = publishRoot + .childDirectory('flutter') + .childDirectory('widgets') + ..createSync(recursive: true); + widgetsDir.childFile('showGeneralDialog.html').writeAsStringSync(''' +
    +  
    +    import 'package:flutter/material.dart';
    +  
    +
    +''', + ); + expect(publishRoot.childDirectory('flutter').existsSync(), isTrue); + (widgetsDir + .childDirectory('ModalRoute') + ..createSync(recursive: true)) + .childFile('barrierColor.html') + .writeAsStringSync(''' +
    +  
    +    class FooClass {
    +      Color get barrierColor => FooColor();
    +    }
    +  
    +
    +'''); + const String queryParams = 'split=1&run=true&sample_id=widgets.Listener.123&sample_channel=master&channel=master'; + widgetsDir.childFile('Listener-class.html').writeAsStringSync(''' + +'''); + } + ), + ]); + + await generator.generateDartdoc(); + }); + }); +} + +const String branchName = 'stable'; +const String testVersionInfo = ''' +{ + "frameworkVersion": "2.5.0", + "channel": "$branchName", + "repositoryUrl": "git@github.com:flutter/flutter.git", + "frameworkRevision": "0000000000000000000000000000000000000000", + "frameworkCommitDate": "2021-07-28 13:03:40 -0700", + "engineRevision": "0000000000000000000000000000000000000001", + "dartSdkVersion": "2.14.0 (build 2.14.0-360.0.dev)", + "flutterRoot": "/home/user/flutter" +} +'''; diff --git a/dev/tools/test/dartdoc_test.dart b/dev/tools/test/dartdoc_test.dart deleted file mode 100644 index 5744650ee3fa8..0000000000000 --- a/dev/tools/test/dartdoc_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; -import '../dartdoc.dart' show getBranchName, runPubProcess; - -void main() { - const String branchName = 'stable'; - test('getBranchName does not call git if env LUCI_BRANCH provided', () { - final Platform platform = FakePlatform( - environment: { - 'LUCI_BRANCH': branchName, - }, - ); - - final ProcessManager processManager = FakeProcessManager.empty(); - - expect( - getBranchName( - platform: platform, - processManager: processManager, - ), - branchName, - ); - }); - - test('getBranchName calls git if env LUCI_BRANCH not provided', () { - final Platform platform = FakePlatform( - environment: {}, - ); - - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['git', 'status', '-b', '--porcelain'], - stdout: '## $branchName', - ), - ], - ); - - expect( - getBranchName( - platform: platform, - processManager: processManager, - ), - branchName, - ); - expect(processManager, hasNoRemainingExpectations); - }); - - test('getBranchName calls git if env LUCI_BRANCH is empty', () { - final Platform platform = FakePlatform( - environment: { - 'LUCI_BRANCH': '', - }, - ); - - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['git', 'status', '-b', '--porcelain'], - stdout: '## $branchName', - ), - ], - ); - - expect( - getBranchName( - platform: platform, - processManager: processManager, - ), - branchName, - ); - expect(processManager, hasNoRemainingExpectations); - }); - - test("runPubProcess doesn't use the pub binary", () { - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['dart', 'pub', '--one', '--two'], - ), - ], - ); - - runPubProcess( - dartBinaryPath: 'dart', - arguments: ['--one', '--two'], - processManager: processManager, - ); - - expect(processManager, hasNoRemainingExpectations); - }); -} diff --git a/dev/tools/test/update_icons_test.dart b/dev/tools/test/update_icons_test.dart index b7fa706d93ad8..6b1fbe436294f 100644 --- a/dev/tools/test/update_icons_test.dart +++ b/dev/tools/test/update_icons_test.dart @@ -61,4 +61,61 @@ void main() { 'Icon(Icons.onetwothree_rounded),', ); }); + + test('certain icons should be mirrored in RTL', () { + // Exact match + expect( + Icon(const MapEntry('help', '')).isMirroredInRTL, + true, + ); + // Variant + expect( + Icon(const MapEntry('help_rounded', '')).isMirroredInRTL, + true, + ); + // Common suffixes + expect( + Icon(const MapEntry('help_alt', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_new', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_off', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_on', '')).isMirroredInRTL, + true, + ); + // Common suffixes + variant + expect( + Icon(const MapEntry('help_alt_rounded', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_new_rounded', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_off_rounded', '')).isMirroredInRTL, + true, + ); + expect( + Icon(const MapEntry('help_on_rounded', '')).isMirroredInRTL, + true, + ); + // No match + expect( + Icon(const MapEntry('help_center_rounded', '')).isMirroredInRTL, + false, + ); + // No match + expect( + Icon(const MapEntry('arrow', '')).isMirroredInRTL, + false, + ); + }); } diff --git a/dev/tools/update_icons.dart b/dev/tools/update_icons.dart index bbe77f0459f15..9d56f540ba958 100644 --- a/dev/tools/update_icons.dart +++ b/dev/tools/update_icons.dart @@ -150,7 +150,7 @@ const Set _iconsMirroredWhenRTL = { 'navigate_next', 'next_week', 'note', - 'open_in_new', + 'open_in', 'playlist_add', 'queue_music', 'redo', @@ -513,12 +513,14 @@ class Icon { String get usage => 'Icon($className.$flutterId),'; - String get mirroredInRTL => _iconsMirroredWhenRTL.contains(shortId) - ? ', matchTextDirection: true' - : ''; + bool get isMirroredInRTL { + // Remove common suffixes (e.g. "_new" or "_alt") from the shortId. + final String normalizedShortId = shortId.replaceAll(RegExp(r'_(new|alt|off|on)$'), ''); + return _iconsMirroredWhenRTL.any((String shortIdMirroredWhenRTL) => normalizedShortId == shortIdMirroredWhenRTL); + } String get declaration => - "static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: '$fontFamily'$mirroredInRTL);"; + "static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: '$fontFamily'${isMirroredInRTL ? ', matchTextDirection: true' : ''});"; String get fullDeclaration => ''' diff --git a/dev/tools/vitool/pubspec.yaml b/dev/tools/vitool/pubspec.yaml index 59a59762161e3..35e57e16f4242 100644 --- a/dev/tools/vitool/pubspec.yaml +++ b/dev/tools/vitool/pubspec.yaml @@ -4,21 +4,21 @@ version: 0.0.1 homepage: https://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter args: 2.4.2 vector_math: 2.1.4 - xml: 6.3.0 + xml: 6.4.2 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - petitparser: 5.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 6.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -31,10 +31,10 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: f2ea +# PUBSPEC CHECKSUM: 1048 diff --git a/dev/tracing_tests/android/build.gradle b/dev/tracing_tests/android/build.gradle index 830c81ab1a6fa..68ee66d66c4c1 100644 --- a/dev/tracing_tests/android/build.gradle +++ b/dev/tracing_tests/android/build.gradle @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/dev/tracing_tests/android/gradle.properties b/dev/tracing_tests/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/dev/tracing_tests/android/gradle.properties +++ b/dev/tracing_tests/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/dev/tracing_tests/pubspec.yaml b/dev/tracing_tests/pubspec.yaml index 203fb17208fe5..32813482959eb 100644 --- a/dev/tracing_tests/pubspec.yaml +++ b/dev/tracing_tests/pubspec.yaml @@ -2,20 +2,20 @@ name: tracing_tests description: Various tests for tracing in flutter/flutter environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter - vm_service: 11.7.1 + vm_service: 11.10.0 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -28,10 +28,10 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: b2fa +# PUBSPEC CHECKSUM: 4580 diff --git a/dev/tracing_tests/test/common.dart b/dev/tracing_tests/test/common.dart index 29a60372a3763..7c534348805cd 100644 --- a/dev/tracing_tests/test/common.dart +++ b/dev/tracing_tests/test/common.dart @@ -26,7 +26,7 @@ void initTimelineTests() { } _vmService = await vmServiceConnectUri('ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws'); await _vmService.setVMTimelineFlags(['Dart']); - isolateId = developer.Service.getIsolateID(isolate.Isolate.current)!; + isolateId = developer.Service.getIsolateId(isolate.Isolate.current)!; }); } diff --git a/examples/api/android/gradle.properties b/examples/api/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/api/android/gradle.properties +++ b/examples/api/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart b/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart new file mode 100644 index 0000000000000..ec1229572be53 --- /dev/null +++ b/examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart @@ -0,0 +1,138 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +/// Flutter code sample for [TapAndPanGestureRecognizer]. + +void main() { + runApp(const TapAndDragToZoomApp()); +} + +class TapAndDragToZoomApp extends StatelessWidget { + const TapAndDragToZoomApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: TapAndDragToZoomWidget( + child: MyBoxWidget(), + ), + ), + ), + ); + } +} + +class MyBoxWidget extends StatelessWidget { + const MyBoxWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blueAccent, + height: 100.0, + width: 100.0, + ); + } +} + +// This widget will scale its child up when it detects a drag up, after a +// double tap/click. It will scale the widget down when it detects a drag down, +// after a double tap. Dragging down and then up after a double tap/click will +// zoom the child in/out. The scale of the child will be reset when the drag ends. +class TapAndDragToZoomWidget extends StatefulWidget { + const TapAndDragToZoomWidget({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _TapAndDragToZoomWidgetState(); +} + +class _TapAndDragToZoomWidgetState extends State { + final double scaleMultiplier = -0.0001; + double _currentScale = 1.0; + Offset? _previousDragPosition; + + static double _keepScaleWithinBounds(double scale) { + const double minScale = 0.1; + const double maxScale = 30; + if (scale <= 0) { + return minScale; + } + if (scale >= 30) { + return maxScale; + } + return scale; + } + + void _zoomLogic(Offset currentDragPosition) { + final double dx = (_previousDragPosition!.dx - currentDragPosition.dx).abs(); + final double dy = (_previousDragPosition!.dy - currentDragPosition.dy).abs(); + + if (dx > dy) { + // Ignore horizontal drags. + _previousDragPosition = currentDragPosition; + return; + } + + if (currentDragPosition.dy < _previousDragPosition!.dy) { + // Zoom out on drag up. + setState(() { + _currentScale += currentDragPosition.dy * scaleMultiplier; + _currentScale = _keepScaleWithinBounds(_currentScale); + }); + } else { + // Zoom in on drag down. + setState(() { + _currentScale -= currentDragPosition.dy * scaleMultiplier; + _currentScale = _keepScaleWithinBounds(_currentScale); + }); + } + _previousDragPosition = currentDragPosition; + } + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + gestures: { + TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => TapAndPanGestureRecognizer(), + (TapAndPanGestureRecognizer instance) { + instance + ..onTapDown = (TapDragDownDetails details) { + _previousDragPosition = details.globalPosition; + } + ..onDragStart = (TapDragStartDetails details) { + if (details.consecutiveTapCount == 2) { + _zoomLogic(details.globalPosition); + } + } + ..onDragUpdate = (TapDragUpdateDetails details) { + if (details.consecutiveTapCount == 2) { + _zoomLogic(details.globalPosition); + } + } + ..onDragEnd = (TapDragEndDetails details) { + if (details.consecutiveTapCount == 2) { + setState(() { + _currentScale = 1.0; + }); + _previousDragPosition = null; + } + }; + } + ), + }, + child: Transform.scale( + scale: _currentScale, + child: widget.child, + ), + ); + } +} diff --git a/examples/api/lib/material/action_buttons/action_icon_theme.0.dart b/examples/api/lib/material/action_buttons/action_icon_theme.0.dart index 653b6ee6a70c2..20671520545c7 100644 --- a/examples/api/lib/material/action_buttons/action_icon_theme.0.dart +++ b/examples/api/lib/material/action_buttons/action_icon_theme.0.dart @@ -73,7 +73,13 @@ class MyHomePage extends StatelessWidget { appBar: AppBar( title: Text(title), ), - drawer: const Drawer(), + drawer: Drawer( + child: Column( + children: [ + TextButton(child: const Text('Drawer Item'), onPressed: () {}), + ], + ), + ), body: const Center( child: NextPageButton(), ), diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.1.dart b/examples/api/lib/material/app_bar/sliver_app_bar.1.dart index 9d7ad116fc497..46b41d8180816 100644 --- a/examples/api/lib/material/app_bar/sliver_app_bar.1.dart +++ b/examples/api/lib/material/app_bar/sliver_app_bar.1.dart @@ -63,7 +63,7 @@ class _SliverAppBarExampleState extends State { color: index.isOdd ? Colors.white : Colors.black12, height: 100.0, child: Center( - child: Text('$index', textScaleFactor: 5), + child: Text('$index', textScaler: const TextScaler.linear(5)), ), ); }, diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.4.dart b/examples/api/lib/material/app_bar/sliver_app_bar.4.dart index 769c970fa1bd1..77d2773988bb5 100644 --- a/examples/api/lib/material/app_bar/sliver_app_bar.4.dart +++ b/examples/api/lib/material/app_bar/sliver_app_bar.4.dart @@ -51,7 +51,7 @@ class _StretchableSliverAppBarState extends State { color: index.isOdd ? Colors.white : Colors.black12, height: 100.0, child: Center( - child: Text('$index', textScaleFactor: 5), + child: Text('$index', textScaler: const TextScaler.linear(5.0)), ), ); }, diff --git a/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart b/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart index 560406e34cbd9..49e42910fa5ea 100644 --- a/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart +++ b/examples/api/lib/material/bottom_navigation_bar/bottom_navigation_bar.2.dart @@ -91,7 +91,7 @@ class _BottomNavigationBarExampleState extends State } void showModal(BuildContext context) { - showDialog( + showDialog( context: context, builder: (BuildContext context) => AlertDialog( content: const Text('Example Dialog'), diff --git a/examples/api/lib/material/data_table/data_table.1.dart b/examples/api/lib/material/data_table/data_table.1.dart index cdb47e73bd0d2..36690d3617496 100644 --- a/examples/api/lib/material/data_table/data_table.1.dart +++ b/examples/api/lib/material/data_table/data_table.1.dart @@ -30,13 +30,12 @@ class DataTableExample extends StatefulWidget { } class _DataTableExampleState extends State { - static const int numItems = 10; + static const int numItems = 20; List selected = List.generate(numItems, (int index) => false); @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, + return SingleChildScrollView( child: DataTable( columns: const [ DataColumn( diff --git a/examples/api/lib/material/drawer/drawer.0.dart b/examples/api/lib/material/drawer/drawer.0.dart new file mode 100644 index 0000000000000..46c96ac435bb7 --- /dev/null +++ b/examples/api/lib/material/drawer/drawer.0.dart @@ -0,0 +1,90 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [Drawer]. + +void main() => runApp(const DrawerApp()); + +class DrawerApp extends StatelessWidget { + const DrawerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const DrawerExample(), + ); + } +} + +class DrawerExample extends StatefulWidget { + const DrawerExample({super.key}); + + @override + State createState() => _DrawerExampleState(); +} + +class _DrawerExampleState extends State { + String selectedPage = ''; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Drawer Example'), + ), + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + decoration: BoxDecoration( + color: Colors.blue, + ), + child: Text( + 'Drawer Header', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), + ), + ), + ListTile( + leading: const Icon(Icons.message), + title: const Text('Messages'), + onTap: () { + setState(() { + selectedPage = 'Messages'; + }); + }, + ), + ListTile( + leading: const Icon(Icons.account_circle), + title: const Text('Profile'), + onTap: () { + setState(() { + selectedPage = 'Profile'; + }); + }, + ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('Settings'), + onTap: () { + setState(() { + selectedPage = 'Settings'; + }); + }, + ), + ], + ), + ), + body: Center( + child: Text('Page: $selectedPage'), + ), + ); + } +} diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart b/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart index 8a8e51b66c987..e7b1ea9b3db3d 100644 --- a/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart +++ b/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart @@ -2,13 +2,45 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// which is the default configuration, and the second one has a filled input decoration. - import 'package:flutter/material.dart'; -/// Flutter code sample for [DropdownMenu]s. The first dropdown menu has an outlined border. +// Flutter code sample for [DropdownMenu]s. The first dropdown menu +// has the default outlined border and demos using the +// [DropdownMenuEntry] style parameter to customize its appearance. +// The second dropdown menu customizes the appearance of the dropdown +// menu's text field with its [InputDecorationTheme] parameter. + +void main() { + runApp(const DropdownMenuExample()); +} + +// DropdownMenuEntry labels and values for the first dropdown menu. +enum ColorLabel { + blue('Blue', Colors.blue), + pink('Pink', Colors.pink), + green('Green', Colors.green), + yellow('Orange', Colors.orange), + grey('Grey', Colors.grey); + + const ColorLabel(this.label, this.color); + final String label; + final Color color; +} + +// DropdownMenuEntry labels and values for the second dropdown menu. +enum IconLabel { + smile('Smile', Icons.sentiment_satisfied_outlined), + cloud( + 'Cloud', + Icons.cloud_outlined, + ), + brush('Brush', Icons.brush_outlined), + heart('Heart', Icons.favorite); -void main() => runApp(const DropdownMenuExample()); + const IconLabel(this.label, this.icon); + final String label; + final IconData icon; +} class DropdownMenuExample extends StatefulWidget { const DropdownMenuExample({super.key}); @@ -25,18 +57,6 @@ class _DropdownMenuExampleState extends State { @override Widget build(BuildContext context) { - final List> colorEntries = >[]; - for (final ColorLabel color in ColorLabel.values) { - colorEntries.add( - DropdownMenuEntry(value: color, label: color.label, enabled: color.label != 'Grey'), - ); - } - - final List> iconEntries = >[]; - for (final IconLabel icon in IconLabel.values) { - iconEntries.add(DropdownMenuEntry(value: icon, label: icon.label)); - } - return MaterialApp( theme: ThemeData( useMaterial3: true, @@ -54,21 +74,37 @@ class _DropdownMenuExampleState extends State { DropdownMenu( initialSelection: ColorLabel.green, controller: colorController, + // requestFocusOnTap is enabled/disabled by platforms when it is null. + // On mobile platforms, this is false by default. Setting this to true will + // trigger focus request on the text field and virtual keyboard will appear + // afterward. On desktop platforms however, this defaults to true. + requestFocusOnTap: true, label: const Text('Color'), - dropdownMenuEntries: colorEntries, onSelected: (ColorLabel? color) { setState(() { selectedColor = color; }); }, + dropdownMenuEntries: ColorLabel.values.map>( + (ColorLabel color) { + return DropdownMenuEntry( + value: color, + label: color.label, + enabled: color.label != 'Grey', + style: MenuItemButton.styleFrom( + foregroundColor: color.color, + ), + ); + } + ).toList(), ), - const SizedBox(width: 20), + const SizedBox(width: 24), DropdownMenu( controller: iconController, enableFilter: true, + requestFocusOnTap: true, leadingIcon: const Icon(Icons.search), label: const Text('Icon'), - dropdownMenuEntries: iconEntries, inputDecorationTheme: const InputDecorationTheme( filled: true, contentPadding: EdgeInsets.symmetric(vertical: 5.0), @@ -78,7 +114,16 @@ class _DropdownMenuExampleState extends State { selectedIcon = icon; }); }, - ) + dropdownMenuEntries: IconLabel.values.map>( + (IconLabel icon) { + return DropdownMenuEntry( + value: icon, + label: icon.label, + leadingIcon: Icon(icon.icon), + ); + }, + ).toList(), + ), ], ), ), @@ -105,29 +150,3 @@ class _DropdownMenuExampleState extends State { ); } } - -enum ColorLabel { - blue('Blue', Colors.blue), - pink('Pink', Colors.pink), - green('Green', Colors.green), - yellow('Yellow', Colors.yellow), - grey('Grey', Colors.grey); - - const ColorLabel(this.label, this.color); - final String label; - final Color color; -} - -enum IconLabel { - smile('Smile', Icons.sentiment_satisfied_outlined), - cloud( - 'Cloud', - Icons.cloud_outlined, - ), - brush('Brush', Icons.brush_outlined), - heart('Heart', Icons.favorite); - - const IconLabel(this.label, this.icon); - final String label; - final IconData icon; -} diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart b/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart new file mode 100644 index 0000000000000..a4c77c6244503 --- /dev/null +++ b/examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart @@ -0,0 +1,91 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for the [DropdownMenuEntry] `labelWidget` property. + +enum ColorItem { + blue('Blue', Colors.blue), + pink('Pink', Colors.pink), + green('Green', Colors.green), + yellow('Yellow', Colors.yellow), + grey('Grey', Colors.grey); + + const ColorItem(this.label, this.color); + final String label; + final Color color; +} + +class DropdownMenuEntryLabelWidgetExample extends StatefulWidget { + const DropdownMenuEntryLabelWidgetExample({ super.key }); + + @override + State createState() => _DropdownMenuEntryLabelWidgetExampleState(); +} + +class _DropdownMenuEntryLabelWidgetExampleState extends State { + late final TextEditingController controller; + + @override + void initState() { + super.initState(); + controller = TextEditingController(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Created by Google Bard from 'create a lyrical phrase of about 25 words that begins with "is a color"'. + const String longText = 'is a color that sings of hope, A hue that shines like gold. It is the color of dreams, A shade that never grows old.'; + + return Scaffold( + body: Center( + child: DropdownMenu( + width: 300, + controller: controller, + initialSelection: ColorItem.green, + label: const Text('Color'), + onSelected: (ColorItem? color) { + print('Selected $color'); + }, + dropdownMenuEntries: ColorItem.values.map>((ColorItem item) { + final String labelText = '${item.label} $longText\n'; + return DropdownMenuEntry( + value: item, + label: labelText, + // Try commenting the labelWidget out or changing + // the labelWidget's Text parameters. + labelWidget: Text( + labelText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + ), + ), + ); + } +} + +class DropdownMenuEntryLabelWidgetExampleApp extends StatelessWidget { + const DropdownMenuEntryLabelWidgetExampleApp({ super.key }); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: DropdownMenuEntryLabelWidgetExample(), + ); + } +} + +void main() { + runApp(const DropdownMenuEntryLabelWidgetExampleApp()); +} diff --git a/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart b/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart index 740250e4f6097..6fb02ad07408f 100644 --- a/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart +++ b/examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart @@ -67,7 +67,7 @@ class _ExpansionPanelListExampleState extends State { return ExpansionPanelList( expansionCallback: (int index, bool isExpanded) { setState(() { - _data[index].isExpanded = !isExpanded; + _data[index].isExpanded = isExpanded; }); }, children: _data.map((Item item) { diff --git a/examples/api/lib/material/floating_action_button/floating_action_button.1.dart b/examples/api/lib/material/floating_action_button/floating_action_button.1.dart index 9650247a25edd..3aef595986a5f 100644 --- a/examples/api/lib/material/floating_action_button/floating_action_button.1.dart +++ b/examples/api/lib/material/floating_action_button/floating_action_button.1.dart @@ -14,14 +14,14 @@ class FloatingActionButtonExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true), - home: const FabExample(), + theme: ThemeData(useMaterial3: true), + home: const FloatingActionButtonExample(), ); } } -class FabExample extends StatelessWidget { - const FabExample({super.key}); +class FloatingActionButtonExample extends StatelessWidget { + const FloatingActionButtonExample({super.key}); @override Widget build(BuildContext context) { diff --git a/examples/api/lib/material/floating_action_button/floating_action_button.2.dart b/examples/api/lib/material/floating_action_button/floating_action_button.2.dart new file mode 100644 index 0000000000000..dda2e9b6db284 --- /dev/null +++ b/examples/api/lib/material/floating_action_button/floating_action_button.2.dart @@ -0,0 +1,104 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [FloatingActionButton]. + +void main() => runApp(const FloatingActionButtonExampleApp()); + +class FloatingActionButtonExampleApp extends StatelessWidget { + const FloatingActionButtonExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const FloatingActionButtonExample(), + ); + } +} + +class FloatingActionButtonExample extends StatelessWidget { + const FloatingActionButtonExample({super.key}); + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + Widget titleBox(String title) { + return DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(4), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text(title, style: TextStyle(color: colorScheme.onInverseSurface)), + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('FAB Additional Color Mappings'), + ), + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Surface color mapping. + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton.large( + foregroundColor: colorScheme.primary, + backgroundColor: colorScheme.surface, + onPressed: () { + // Add your onPressed code here! + }, + child: const Icon(Icons.edit_outlined), + ), + const SizedBox(height: 20), + titleBox('Surface'), + ], + ), + // Secondary color mapping. + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton.large( + foregroundColor: colorScheme.onSecondaryContainer, + backgroundColor: colorScheme.secondaryContainer, + onPressed: () { + // Add your onPressed code here! + }, + child: const Icon(Icons.edit_outlined), + ), + const SizedBox(height: 20), + titleBox('Secondary'), + ], + ), + // Tertiary color mapping. + Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton.large( + foregroundColor: colorScheme.onTertiaryContainer, + backgroundColor: colorScheme.tertiaryContainer, + onPressed: () { + // Add your onPressed code here! + }, + child: const Icon(Icons.edit_outlined), + ), + const SizedBox(height: 20), + titleBox('Tertiary'), + ], + ), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/material/input_chip/input_chip.1.dart b/examples/api/lib/material/input_chip/input_chip.1.dart new file mode 100644 index 0000000000000..c503e6ce7e0fa --- /dev/null +++ b/examples/api/lib/material/input_chip/input_chip.1.dart @@ -0,0 +1,369 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +const List _pizzaToppings = [ + 'Olives', + 'Tomato', + 'Cheese', + 'Pepperoni', + 'Bacon', + 'Onion', + 'Jalapeno', + 'Mushrooms', + 'Pineapple', +]; + +void main() => runApp(const EditableChipFieldApp()); + +class EditableChipFieldApp extends StatelessWidget { + const EditableChipFieldApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const EditableChipFieldExample(), + ); + } +} + +class EditableChipFieldExample extends StatefulWidget { + const EditableChipFieldExample({super.key}); + + @override + EditableChipFieldExampleState createState() { + return EditableChipFieldExampleState(); + } +} + +class EditableChipFieldExampleState extends State { + final FocusNode _chipFocusNode = FocusNode(); + List _toppings = [_pizzaToppings.first]; + List _suggestions = []; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Editable Chip Field Sample'), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ChipsInput( + values: _toppings, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.local_pizza_rounded), + hintText: 'Search for toppings', + ), + strutStyle: const StrutStyle(fontSize: 15), + onChanged: _onChanged, + onSubmitted: _onSubmitted, + chipBuilder: _chipBuilder, + onTextChanged: _onSearchChanged, + ), + ), + if (_suggestions.isNotEmpty) + Expanded( + child: ListView.builder( + itemCount: _suggestions.length, + itemBuilder: (BuildContext context, int index) { + return ToppingSuggestion( + _suggestions[index], + onTap: _selectSuggestion, + ); + }, + ), + ), + ], + ), + ); + } + + Future _onSearchChanged(String value) async { + final List results = await _suggestionCallback(value); + setState(() { + _suggestions = results + .where((String topping) => !_toppings.contains(topping)) + .toList(); + }); + } + + Widget _chipBuilder(BuildContext context, String topping) { + return ToppingInputChip( + topping: topping, + onDeleted: _onChipDeleted, + onSelected: _onChipTapped, + ); + } + + void _selectSuggestion(String topping) { + setState(() { + _toppings.add(topping); + _suggestions = []; + }); + } + + void _onChipTapped(String topping) {} + + void _onChipDeleted(String topping) { + setState(() { + _toppings.remove(topping); + _suggestions = []; + }); + } + + void _onSubmitted(String text) { + if (text.trim().isNotEmpty) { + setState(() { + _toppings = [..._toppings, text.trim()]; + }); + } else { + _chipFocusNode.unfocus(); + setState(() { + _toppings = []; + }); + } + } + + void _onChanged(List data) { + setState(() { + _toppings = data; + }); + } + + FutureOr> _suggestionCallback(String text) { + if (text.isNotEmpty) { + return _pizzaToppings.where((String topping) { + return topping.toLowerCase().contains(text.toLowerCase()); + }).toList(); + } + return const []; + } +} + +class ChipsInput extends StatefulWidget { + const ChipsInput({ + super.key, + required this.values, + this.decoration = const InputDecoration(), + this.style, + this.strutStyle, + required this.chipBuilder, + required this.onChanged, + this.onChipTapped, + this.onSubmitted, + this.onTextChanged, + }); + + final List values; + final InputDecoration decoration; + final TextStyle? style; + final StrutStyle? strutStyle; + + final ValueChanged> onChanged; + final ValueChanged? onChipTapped; + final ValueChanged? onSubmitted; + final ValueChanged? onTextChanged; + + final Widget Function(BuildContext context, T data) chipBuilder; + + @override + ChipsInputState createState() => ChipsInputState(); +} + +class ChipsInputState extends State> { + @visibleForTesting + late final ChipsInputEditingController controller; + + String _previousText = ''; + TextSelection? _previousSelection; + + @override + void initState() { + super.initState(); + + controller = ChipsInputEditingController( + [...widget.values], + widget.chipBuilder, + ); + controller.addListener(_textListener); + } + + @override + void dispose() { + controller.removeListener(_textListener); + controller.dispose(); + + super.dispose(); + } + + void _textListener() { + final String currentText = controller.text; + + if (_previousSelection != null) { + final int currentNumber = countReplacements(currentText); + final int previousNumber = countReplacements(_previousText); + + final int cursorEnd = _previousSelection!.extentOffset; + final int cursorStart = _previousSelection!.baseOffset; + + final List values = [...widget.values]; + + // If the current number and the previous number of replacements are different, then + // the user has deleted the InputChip using the keyboard. In this case, we trigger + // the onChanged callback. We need to be sure also that the current number of + // replacements is different from the input chip to avoid double-deletion. + if (currentNumber < previousNumber && currentNumber != values.length) { + if (cursorStart == cursorEnd) { + values.removeRange(cursorStart - 1, cursorEnd); + } else { + if (cursorStart > cursorEnd) { + values.removeRange(cursorEnd, cursorStart); + } else { + values.removeRange(cursorStart, cursorEnd); + } + } + widget.onChanged(values); + } + } + + _previousText = currentText; + _previousSelection = controller.selection; + } + + static int countReplacements(String text) { + return text.codeUnits + .where((int u) => u == ChipsInputEditingController.kObjectReplacementChar) + .length; + } + + @override + Widget build(BuildContext context) { + controller.updateValues([...widget.values]); + + return TextField( + minLines: 1, + maxLines: 3, + textInputAction: TextInputAction.done, + style: widget.style, + strutStyle: widget.strutStyle, + controller: controller, + onChanged: (String value) => + widget.onTextChanged?.call(controller.textWithoutReplacements), + onSubmitted: (String value) => + widget.onSubmitted?.call(controller.textWithoutReplacements), + ); + } +} + +class ChipsInputEditingController extends TextEditingController { + ChipsInputEditingController(this.values, this.chipBuilder) + : super( + text: String.fromCharCode(kObjectReplacementChar) * values.length, + ); + + // This constant character acts as a placeholder in the TextField text value. + // There will be one character for each of the InputChip displayed. + static const int kObjectReplacementChar = 0xFFFE; + + List values; + + final Widget Function(BuildContext context, T data) chipBuilder; + + /// Called whenever chip is either added or removed + /// from the outside the context of the text field. + void updateValues(List values) { + if (values.length != this.values.length) { + final String char = String.fromCharCode(kObjectReplacementChar); + final int length = values.length; + value = TextEditingValue( + text: char * length, + selection: TextSelection.collapsed(offset: length), + ); + this.values = values; + } + } + + String get textWithoutReplacements { + final String char = String.fromCharCode(kObjectReplacementChar); + return text.replaceAll(RegExp(char), ''); + } + + String get textWithReplacements => text; + + @override + TextSpan buildTextSpan( + {required BuildContext context, TextStyle? style, required bool withComposing}) { + + final Iterable chipWidgets = + values.map((T v) => WidgetSpan(child: chipBuilder(context, v))); + + return TextSpan( + style: style, + children: [ + ...chipWidgets, + if (textWithoutReplacements.isNotEmpty) + TextSpan(text: textWithoutReplacements) + ], + ); + } +} + +class ToppingSuggestion extends StatelessWidget { + const ToppingSuggestion(this.topping, {super.key, this.onTap}); + + final String topping; + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) { + return ListTile( + key: ObjectKey(topping), + leading: CircleAvatar( + child: Text( + topping[0].toUpperCase(), + ), + ), + title: Text(topping), + onTap: () => onTap?.call(topping), + ); + } +} + +class ToppingInputChip extends StatelessWidget { + const ToppingInputChip({ + super.key, + required this.topping, + required this.onDeleted, + required this.onSelected, + }); + + final String topping; + final ValueChanged onDeleted; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(right: 3), + child: InputChip( + key: ObjectKey(topping), + label: Text(topping), + avatar: CircleAvatar( + child: Text(topping[0].toUpperCase()), + ), + onDeleted: () => onDeleted(topping), + onSelected: (bool value) => onSelected(topping), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.all(2), + ), + ); + } +} diff --git a/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart b/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart index 6279b6e2227cf..8a6066f7ce850 100644 --- a/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart +++ b/examples/api/lib/material/menu_anchor/checkbox_menu_button.0.dart @@ -101,8 +101,9 @@ class MenuApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold(body: MyCheckboxMenu(message: kMessage)), + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold(body: SafeArea(child: MyCheckboxMenu(message: kMessage))), ); } } diff --git a/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart b/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart index 802cb9c66d07e..5e09ef9328486 100644 --- a/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart +++ b/examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart @@ -110,7 +110,7 @@ class MenuAcceleratorApp extends StatelessWidget { debugDumpApp(); }), }, - child: const Scaffold(body: MyMenuBar()), + child: const Scaffold(body: SafeArea(child: MyMenuBar())), ), ); } diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.0.dart b/examples/api/lib/material/menu_anchor/menu_anchor.0.dart index 413879d665d30..d4a3de7dece2b 100644 --- a/examples/api/lib/material/menu_anchor/menu_anchor.0.dart +++ b/examples/api/lib/material/menu_anchor/menu_anchor.0.dart @@ -204,7 +204,7 @@ class MenuApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(useMaterial3: true), - home: const Scaffold(body: MyCascadingMenu(message: kMessage)), + home: const Scaffold(body: SafeArea(child: MyCascadingMenu(message: kMessage))), ); } } diff --git a/examples/api/lib/material/menu_anchor/menu_bar.0.dart b/examples/api/lib/material/menu_anchor/menu_bar.0.dart index 23ccec10999ca..b769630027a38 100644 --- a/examples/api/lib/material/menu_anchor/menu_bar.0.dart +++ b/examples/api/lib/material/menu_anchor/menu_bar.0.dart @@ -230,7 +230,7 @@ class MenuBarApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( - home: Scaffold(body: MyMenuBar(message: kMessage)), + home: Scaffold(body: SafeArea(child: MyMenuBar(message: kMessage))), ); } } diff --git a/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart b/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart index 20448926d3e45..3976c88d3745e 100644 --- a/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart +++ b/examples/api/lib/material/menu_anchor/radio_menu_button.0.dart @@ -107,8 +107,9 @@ class MenuApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold(body: MyRadioMenu()), + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold(body: SafeArea(child: MyRadioMenu())), ); } } diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.0.dart b/examples/api/lib/material/navigation_bar/navigation_bar.0.dart index b5ff01d313111..5c38b8c1615ed 100644 --- a/examples/api/lib/material/navigation_bar/navigation_bar.0.dart +++ b/examples/api/lib/material/navigation_bar/navigation_bar.0.dart @@ -13,7 +13,10 @@ class NavigationBarApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp(home: NavigationExample()); + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const NavigationExample(), + ); } } @@ -29,6 +32,7 @@ class _NavigationExampleState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); return Scaffold( bottomNavigationBar: NavigationBar( onDestinationSelected: (int index) { @@ -36,7 +40,7 @@ class _NavigationExampleState extends State { currentPageIndex = index; }); }, - indicatorColor: Colors.amber[800], + indicatorColor: Colors.amber, selectedIndex: currentPageIndex, destinations: const [ NavigationDestination( @@ -45,31 +49,94 @@ class _NavigationExampleState extends State { label: 'Home', ), NavigationDestination( - icon: Icon(Icons.business), - label: 'Business', + icon: Badge(child: Icon(Icons.notifications_sharp)), + label: 'Notifications', ), NavigationDestination( - selectedIcon: Icon(Icons.school), - icon: Icon(Icons.school_outlined), - label: 'School', + icon: Badge( + label: Text('2'), + child: Icon(Icons.messenger_sharp), + ), + label: 'Messages', ), ], ), body: [ - Container( - color: Colors.red, - alignment: Alignment.center, - child: const Text('Page 1'), + /// Home page + Card( + shadowColor: Colors.transparent, + margin: const EdgeInsets.all(8.0), + child: SizedBox.expand( + child: Center( + child: Text( + 'Home page', + style: theme.textTheme.titleLarge, + ), + ), + ), ), - Container( - color: Colors.green, - alignment: Alignment.center, - child: const Text('Page 2'), + /// Notifications page + const Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + Card( + child: ListTile( + leading: Icon(Icons.notifications_sharp), + title: Text('Notification 1'), + subtitle: Text('This is a notification'), + ), + ), + Card( + child: ListTile( + leading: Icon(Icons.notifications_sharp), + title: Text('Notification 2'), + subtitle: Text('This is a notification'), + ), + ), + ], + ), ), - Container( - color: Colors.blue, - alignment: Alignment.center, - child: const Text('Page 3'), + /// Messages page + ListView.builder( + reverse: true, + itemCount: 2, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return Align( + alignment: Alignment.centerRight, + child: Container( + margin: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + 'Hello', + style: theme.textTheme.bodyLarge! + .copyWith(color: theme.colorScheme.onPrimary), + ), + ), + ); + } + return Align( + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + 'Hi!', + style: theme.textTheme.bodyLarge! + .copyWith(color: theme.colorScheme.onPrimary), + ), + ), + ); + }, ), ][currentPageIndex], ); diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart index 7af02cfe00708..b0056c37bc916 100644 --- a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart +++ b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart @@ -71,14 +71,10 @@ class _HomeState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { + return NavigatorPopHandler( + onPop: () { final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!; - if (!navigator.canPop()) { - return true; - } navigator.pop(); - return false; }, child: Scaffold( body: SafeArea( @@ -178,7 +174,7 @@ class RootPage extends StatelessWidget { ElevatedButton( style: buttonStyle, onPressed: () { - showDialog( + showDialog( context: context, useRootNavigator: false, builder: _buildDialog, @@ -190,9 +186,9 @@ class RootPage extends StatelessWidget { ElevatedButton( style: buttonStyle, onPressed: () { - showDialog( + showDialog( context: context, - useRootNavigator: true, + useRootNavigator: true, // ignore: avoid_redundant_argument_values builder: _buildDialog, ); }, @@ -204,7 +200,7 @@ class RootPage extends StatelessWidget { return ElevatedButton( style: buttonStyle, onPressed: () { - showBottomSheet( + showBottomSheet( context: context, builder: (BuildContext context) { return Container( diff --git a/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart b/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart index 446cd1308c3ff..1854b6f850696 100644 --- a/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart +++ b/examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart @@ -11,6 +11,8 @@ import 'package:flutter/material.dart'; /// Flutter code sample for [NavigationDrawer]. +void main() => runApp(const NavigationDrawerApp()); + class ExampleDestination { const ExampleDestination(this.label, this.icon, this.selectedIcon); @@ -20,20 +22,22 @@ class ExampleDestination { } const List destinations = [ - ExampleDestination('page 0', Icon(Icons.widgets_outlined), Icon(Icons.widgets)), - ExampleDestination('page 1', Icon(Icons.format_paint_outlined), Icon(Icons.format_paint)), - ExampleDestination('page 2', Icon(Icons.text_snippet_outlined), Icon(Icons.text_snippet)), - ExampleDestination('page 3', Icon(Icons.invert_colors_on_outlined), Icon(Icons.opacity)), + ExampleDestination('Messages', Icon(Icons.widgets_outlined), Icon(Icons.widgets)), + ExampleDestination('Profile', Icon(Icons.format_paint_outlined), Icon(Icons.format_paint)), + ExampleDestination('Settings', Icon(Icons.settings_outlined), Icon(Icons.settings)), ]; -void main() { - runApp( - MaterialApp( +class NavigationDrawerApp extends StatelessWidget { + const NavigationDrawerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData(useMaterial3: true), home: const NavigationDrawerExample(), - ), - ); + ); + } } class NavigationDrawerExample extends StatefulWidget { @@ -65,7 +69,7 @@ class _NavigationDrawerExampleState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Text('Page Index = $screenIndex'), + Text('Page Index = $screenIndex'), ], ), ), @@ -125,7 +129,7 @@ class _NavigationDrawerExampleState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Text('Page Index = $screenIndex'), + Text('Page Index = $screenIndex'), ElevatedButton( onPressed: openDrawer, child: const Text('Open Drawer'), diff --git a/examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart b/examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart deleted file mode 100644 index 344f410481773..0000000000000 --- a/examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -/// Flutter code sample for [NavigationDrawer]. - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - ), - home: const MyHomePage(), - ); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Drawer Demo'), - ), - drawer: NavigationDrawer( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), - child: Text( - 'Drawer Header', - style: Theme.of(context).textTheme.titleSmall, - ), - ), - const NavigationDrawerDestination( - icon: Icon(Icons.message), - label: Text('Messages'), - ), - const NavigationDrawerDestination( - icon: Icon(Icons.account_circle), - label: Text('Profile'), - ), - const NavigationDrawerDestination( - icon: Icon(Icons.settings), - label: Text('Settings'), - ), - ]) - ); - } -} diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.1.dart b/examples/api/lib/material/navigation_rail/navigation_rail.1.dart index 812f3341b6941..f3e943736e3cd 100644 --- a/examples/api/lib/material/navigation_rail/navigation_rail.1.dart +++ b/examples/api/lib/material/navigation_rail/navigation_rail.1.dart @@ -14,7 +14,7 @@ class NavigationRailExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true), + theme: ThemeData(useMaterial3: true), home: const NavRailExample(), ); } @@ -73,13 +73,19 @@ class _NavRailExampleState extends State { label: Text('First'), ), NavigationRailDestination( - icon: Icon(Icons.bookmark_border), - selectedIcon: Icon(Icons.book), + icon: Badge(child: Icon(Icons.bookmark_border)), + selectedIcon: Badge(child: Icon(Icons.book)), label: Text('Second'), ), NavigationRailDestination( - icon: Icon(Icons.star_border), - selectedIcon: Icon(Icons.star), + icon: Badge( + label: Text('4'), + child: Icon(Icons.star_border), + ), + selectedIcon: Badge( + label: Text('4'), + child: Icon(Icons.star), + ), label: Text('Third'), ), ], diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart b/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart new file mode 100644 index 0000000000000..8249d5e62fff9 --- /dev/null +++ b/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart @@ -0,0 +1,86 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [PaginatedDataTable]. + +class MyDataSource extends DataTableSource { + @override + int get rowCount => 3; + + @override + DataRow? getRow(int index) { + switch (index) { + case 0: return const DataRow( + cells: [ + DataCell(Text('Sarah')), + DataCell(Text('19')), + DataCell(Text('Student')), + ], + ); + case 1: return const DataRow( + cells: [ + DataCell(Text('Janine')), + DataCell(Text('43')), + DataCell(Text('Professor')), + ], + ); + case 2: return const DataRow( + cells: [ + DataCell(Text('William')), + DataCell(Text('27')), + DataCell(Text('Associate Professor')), + ], + ); + default: return null; + } + } + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => 0; +} + +final DataTableSource dataSource = MyDataSource(); + +void main() => runApp(const DataTableExampleApp()); + +class DataTableExampleApp extends StatelessWidget { + const DataTableExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SingleChildScrollView( + padding: EdgeInsets.all(12.0), + child: DataTableExample(), + ), + ); + } +} + +class DataTableExample extends StatelessWidget { + const DataTableExample({super.key}); + + @override + Widget build(BuildContext context) { + return PaginatedDataTable( + columns: const [ + DataColumn( + label: Text('Name'), + ), + DataColumn( + label: Text('Age'), + ), + DataColumn( + label: Text('Role'), + ), + ], + source: dataSource, + ); + } +} diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart b/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart new file mode 100644 index 0000000000000..bc4eff6a93ac4 --- /dev/null +++ b/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart @@ -0,0 +1,307 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [PaginatedDataTable]. + +class MyDataSource extends DataTableSource { + static const List _displayIndexToRawIndex = [ 0, 3, 4, 5, 6 ]; + + late List>> sortedData; + void setData(List>> rawData, int sortColumn, bool sortAscending) { + sortedData = rawData.toList()..sort((List> a, List> b) { + final Comparable cellA = a[_displayIndexToRawIndex[sortColumn]]; + final Comparable cellB = b[_displayIndexToRawIndex[sortColumn]]; + return cellA.compareTo(cellB) * (sortAscending ? 1 : -1); + }); + notifyListeners(); + } + + @override + int get rowCount => sortedData.length; + + static DataCell cellFor(Object data) { + String value; + if (data is DateTime) { + value = '${data.year}-${data.month.toString().padLeft(2, '0')}-${data.day.toString().padLeft(2, '0')}'; + } else { + value = data.toString(); + } + return DataCell(Text(value)); + } + + @override + DataRow? getRow(int index) { + return DataRow.byIndex( + index: sortedData[index][0] as int, + cells: [ + cellFor('S${sortedData[index][1]}E${sortedData[index][2].toString().padLeft(2, '0')}'), + cellFor(sortedData[index][3]), + cellFor(sortedData[index][4]), + cellFor(sortedData[index][5]), + cellFor(sortedData[index][6]), + ], + ); + } + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => 0; +} + +void main() => runApp(const DataTableExampleApp()); + +class DataTableExampleApp extends StatelessWidget { + const DataTableExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SingleChildScrollView( + padding: EdgeInsets.all(12.0), + child: DataTableExample(), + ), + ); + } +} + +class DataTableExample extends StatefulWidget { + const DataTableExample({super.key}); + + @override + State createState() => _DataTableExampleState(); +} + +class _DataTableExampleState extends State { + final MyDataSource dataSource = MyDataSource() + ..setData(episodes, 0, true); + + int _columnIndex = 0; + bool _columnAscending = true; + + void _sort(int columnIndex, bool ascending) { + setState(() { + _columnIndex = columnIndex; + _columnAscending = ascending; + dataSource.setData(episodes, _columnIndex, _columnAscending); + }); + } + + @override + Widget build(BuildContext context) { + return PaginatedDataTable( + sortColumnIndex: _columnIndex, + sortAscending: _columnAscending, + columns: [ + DataColumn( + label: const Text('Episode'), + onSort: _sort, + ), + DataColumn( + label: const Text('Title'), + onSort: _sort, + ), + DataColumn( + label: const Text('Director'), + onSort: _sort, + ), + DataColumn( + label: const Text('Writer(s)'), + onSort: _sort, + ), + DataColumn( + label: const Text('Air Date'), + onSort: _sort, + ), + ], + source: dataSource, + ); + } +} + +final List>> episodes = >>[ + >[ + 1, + 1, + 1, + 'Strange New Worlds', + 'Akiva Goldsman', + 'Akiva Goldsman, Alex Kurtzman, Jenny Lumet', + DateTime(2022, 5, 5), + ], + >[ + 2, + 1, + 2, + 'Children of the Comet', + 'Maja Vrvilo', + 'Henry Alonso Myers, Sarah Tarkoff', + DateTime(2022, 5, 12), + ], + >[ + 3, + 1, + 3, + 'Ghosts of Illyria', + 'Leslie Hope', + 'Akela Cooper, Bill Wolkoff', + DateTime(2022, 5, 19), + ], + >[ + 4, + 1, + 4, + 'Memento Mori', + 'Dan Liu', + 'Davy Perez, Beau DeMayo', + DateTime(2022, 5, 26), + ], + >[ + 5, + 1, + 5, + 'Spock Amok', + 'Rachel Leiterman', + 'Henry Alonso Myers, Robin Wasserman', + DateTime(2022, 6, 2), + ], + >[ + 6, + 1, + 6, + 'Lift Us Where Suffering Cannot Reach', + 'Andi Armaganian', + 'Robin Wasserman, Bill Wolkoff', + DateTime(2022, 6, 9), + ], + >[ + 7, + 1, + 7, + 'The Serene Squall', + 'Sydney Freeland', + 'Beau DeMayo, Sarah Tarkoff', + DateTime(2022, 6, 16), + ], + >[ + 8, + 1, + 8, + 'The Elysian Kingdom', + 'Amanda Row', + 'Akela Cooper, Onitra Johnson', + DateTime(2022, 6, 23), + ], + >[ + 9, + 1, + 9, + 'All Those Who Wander', + 'Christopher J. Byrne', + 'Davy Perez', + DateTime(2022, 6, 30), + ], + >[ + 10, + 2, + 10, + 'A Quality of Mercy', + 'Chris Fisher', + 'Henry Alonso Myers, Akiva Goldsman', + DateTime(2022, 7, 7), + ], + >[ + 11, + 2, + 1, + 'The Broken Circle', + 'Chris Fisher', + 'Henry Alonso Myers, Akiva Goldsman', + DateTime(2023, 6, 15), + ], + >[ + 12, + 2, + 2, + 'Ad Astra per Aspera', + 'Valerie Weiss', + 'Dana Horgan', + DateTime(2023, 6, 22), + ], + >[ + 13, + 2, + 3, + 'Tomorrow and Tomorrow and Tomorrow', + 'Amanda Row', + 'David Reed', + DateTime(2023, 6, 29), + ], + >[ + 14, + 2, + 4, + 'Among the Lotus Eaters', + 'Eduardo Sánchez', + 'Kirsten Beyer, Davy Perez', + DateTime(2023, 7, 6), + ], + >[ + 15, + 2, + 5, + 'Charades', + 'Jordan Canning', + 'Kathryn Lyn, Henry Alonso Myers', + DateTime(2023, 7, 13), + ], + >[ + 16, + 2, + 6, + 'Lost in Translation', + 'Dan Liu', + 'Onitra Johnson, David Reed', + DateTime(2023, 7, 20), + ], + >[ + 17, + 2, + 7, + 'Those Old Scientists', + 'Jonathan Frakes', + 'Kathryn Lyn, Bill Wolkoff', + DateTime(2023, 7, 22), + ], + >[ + 18, + 2, + 8, + 'Under the Cloak of War', + '', + 'Davy Perez', + DateTime(2023, 7, 27), + ], + >[ + 19, + 2, + 9, + 'Subspace Rhapsody', + '', + 'Dana Horgan, Bill Wolkoff', + DateTime(2023, 8, 3), + ], + >[ + 20, + 2, + 10, + 'Hegemony', + '', + 'Henry Alonso Myers', + DateTime(2023, 8, 10), + ], +]; diff --git a/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart b/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart index aec46093c8e84..c389d404ab762 100644 --- a/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart +++ b/examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; /// Flutter code sample for [RefreshIndicator]. @@ -13,8 +14,9 @@ class RefreshIndicatorExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: RefreshIndicatorExample(), + return MaterialApp( + scrollBehavior: const MaterialScrollBehavior().copyWith(dragDevices: PointerDeviceKind.values.toSet()), + home: const RefreshIndicatorExample(), ); } } @@ -40,17 +42,17 @@ class RefreshIndicatorExample extends StatelessWidget { // from the widget's children. // // By default this is set to `notification.depth == 0`, which ensures - // the only the scroll notifications from the first child are listened to. + // the only the scroll notifications from the first scroll view are listened to. // // Here setting `notification.depth == 1` triggers the refresh indicator // when overscrolling the nested scroll view. notificationPredicate: (ScrollNotification notification) { return notification.depth == 1; }, - child: SingleChildScrollView( - child: Column( - children: [ - Container( + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Container( height: 100, alignment: Alignment.center, color: Colors.pink[100], @@ -65,10 +67,12 @@ class RefreshIndicatorExample extends StatelessWidget { ], ), ), - Container( + ), + SliverToBoxAdapter( + child: Container( color: Colors.green[100], + height: 300, child: ListView.builder( - shrinkWrap: true, itemCount: 25, itemBuilder: (BuildContext context, int index) { return const ListTile( @@ -78,8 +82,17 @@ class RefreshIndicatorExample extends StatelessWidget { }, ), ), - ], - ), + ), + SliverList.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return const ListTile( + title: Text('Pull down here'), + subtitle: Text("Refresh indicator won't trigger"), + ); + } + ) + ], ), ), ); diff --git a/examples/api/lib/material/scaffold/scaffold_state.show_snack_bar.0.dart b/examples/api/lib/material/scaffold/scaffold_state.show_snack_bar.0.dart deleted file mode 100644 index 0f077982b9aba..0000000000000 --- a/examples/api/lib/material/scaffold/scaffold_state.show_snack_bar.0.dart +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -/// Flutter code sample for [ScaffoldState.showSnackBar]. - -void main() => runApp(const ShowSnackBarExampleApp()); - -class ShowSnackBarExampleApp extends StatelessWidget { - const ShowSnackBarExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(title: const Text('ScaffoldState Sample')), - body: const Center( - child: ShowSnackBarExample(), - ), - ), - ); - } -} - -class ShowSnackBarExample extends StatelessWidget { - const ShowSnackBarExample({super.key}); - - @override - Widget build(BuildContext context) { - return OutlinedButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('A SnackBar has been shown.'), - ), - ); - }, - child: const Text('Show SnackBar'), - ); - } -} diff --git a/examples/api/lib/material/scrollbar/scrollbar.0.dart b/examples/api/lib/material/scrollbar/scrollbar.0.dart index d8a09486b6cb0..07f1eb4646ad5 100644 --- a/examples/api/lib/material/scrollbar/scrollbar.0.dart +++ b/examples/api/lib/material/scrollbar/scrollbar.0.dart @@ -29,6 +29,7 @@ class ScrollbarExample extends StatelessWidget { Widget build(BuildContext context) { return Scrollbar( child: GridView.builder( + primary: true, itemCount: 120, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemBuilder: (BuildContext context, int index) { diff --git a/examples/api/lib/material/search_anchor/search_anchor.3.dart b/examples/api/lib/material/search_anchor/search_anchor.3.dart index 80694e04d11d0..e0eae6b16021f 100644 --- a/examples/api/lib/material/search_anchor/search_anchor.3.dart +++ b/examples/api/lib/material/search_anchor/search_anchor.3.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; -/// Flutter code sample for [SearchAnchor] that shows how to fetch the suggestions -/// from a remote API. +/// Flutter code sample for [SearchAnchor]. const Duration fakeAPIDuration = Duration(seconds: 1); @@ -60,7 +59,7 @@ class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor > { final List options = (await _FakeAPI.search(_searchingWithQuery!)).toList(); // If another search happened after this one, throw away these options. - // Use the previous options intead and wait for the newer request to + // Use the previous options instead and wait for the newer request to // finish. if (_searchingWithQuery != controller.text) { return _lastOptions; diff --git a/examples/api/lib/material/search_anchor/search_anchor.4.dart b/examples/api/lib/material/search_anchor/search_anchor.4.dart index 8417ce6113c55..d7e90ea12a066 100644 --- a/examples/api/lib/material/search_anchor/search_anchor.4.dart +++ b/examples/api/lib/material/search_anchor/search_anchor.4.dart @@ -6,8 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -/// Flutter code sample for [SearchAnchor] that demonstrates fetching the -/// suggestions asynchronously and debouncing the network calls. +/// Flutter code sample for [SearchAnchor]. const Duration fakeAPIDuration = Duration(seconds: 1); const Duration debounceDuration = Duration(milliseconds: 500); diff --git a/examples/api/lib/material/snack_bar/snack_bar.2.dart b/examples/api/lib/material/snack_bar/snack_bar.2.dart index 7a211be20723a..63fca4d7f9e22 100644 --- a/examples/api/lib/material/snack_bar/snack_bar.2.dart +++ b/examples/api/lib/material/snack_bar/snack_bar.2.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; -/// Flutter code sample for [SnackBar] with Material 3 specifications. +/// Flutter code sample for [SnackBar]. void main() => runApp(const SnackBarExampleApp()); -// A Material 3 [SnackBar] demonstrating an optional icon, in either floating -// or fixed format. +/// A Material 3 [SnackBar] demonstrating an optional icon, in either floating +/// or fixed format. class SnackBarExampleApp extends StatelessWidget { const SnackBarExampleApp({super.key}); diff --git a/examples/api/lib/material/switch/switch.3.dart b/examples/api/lib/material/switch/switch.3.dart index 023c3387d0099..b1ba74d7dfb79 100644 --- a/examples/api/lib/material/switch/switch.3.dart +++ b/examples/api/lib/material/switch/switch.3.dart @@ -16,7 +16,7 @@ class SwitchApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.light(useMaterial3: true).copyWith( - // Use the ambient [CupetinoThemeData] to style all widgets which would + // Use the ambient CupertinoThemeData to style all widgets which would // otherwise use iOS defaults. cupertinoOverrideTheme: const CupertinoThemeData(applyThemeToAll: true), ), @@ -54,7 +54,7 @@ class _SwitchExampleState extends State { }, ), Switch.adaptive( - // Don't use the ambient [CupetinoThemeData] to style this switch. + // Don't use the ambient CupertinoThemeData to style this switch. applyCupertinoTheme: false, value: light, onChanged: (bool value) { diff --git a/examples/api/lib/material/theme_data/theme_data.0.dart b/examples/api/lib/material/theme_data/theme_data.0.dart new file mode 100644 index 0000000000000..ceec42105d3fc --- /dev/null +++ b/examples/api/lib/material/theme_data/theme_data.0.dart @@ -0,0 +1,115 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(const ThemeDataExampleApp()); +} + +// This app's theme specifies an overall ColorScheme as well as overrides +// for the default configuration of FloatingActionButtons. To customize +// the appearance of other components, add additional component specific +// themes, rather than tweaking the color scheme. +// +// Creating an entire color scheme from a single seed color is a good +// way to ensure a visually appealing color palette where the default +// component colors have sufficient contrast for accessibility. Another +// good way to create an app's color scheme is to use +// ColorScheme.fromImageProvider. +// +// The color scheme reflects the platform's light or dark setting +// which is retrieved with `MediaQuery.platformBrightnessOf`. The color +// scheme's colors will be different for light and dark settings although +// they'll all be related to the seed color in both cases. +// +// Color scheme colors have been used where component defaults have +// been overidden so that the app will look good and remain accessible +// in both light and dark modes. +// +// Text styles are derived from the theme's textTheme (not the obsolete +// primaryTextTheme property) and then customized using copyWith. +// Using the _on_ version of a color scheme color as the foreground, +// as in `tertiary` and `onTertiary`, guarantees sufficient contrast +// for readability/accessibility. + +class ThemeDataExampleApp extends StatelessWidget { + const ThemeDataExampleApp({ super.key }); + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = ColorScheme.fromSeed( + brightness: MediaQuery.platformBrightnessOf(context), + seedColor: Colors.indigo, + ); + return MaterialApp( + title: 'ThemeData Demo', + theme: ThemeData( + colorScheme: colorScheme, + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: colorScheme.tertiary, + foregroundColor: colorScheme.onTertiary, + ), + ), + home: const Home(), + ); + } +} + +class Home extends StatefulWidget { + const Home({ super.key }); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + int buttonPressCount = 0; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final double pointCount = 8 + (buttonPressCount % 6); + + return Scaffold( + appBar: AppBar( + title: const Text('Press the + Button'), + ), + // An AnimatedContainer makes the decoration changes entertaining. + body: AnimatedContainer( + duration: const Duration(milliseconds: 500), + margin: const EdgeInsets.all(32), + alignment: Alignment.center, + decoration: ShapeDecoration( + color: colorScheme.tertiaryContainer, + shape: StarBorder( + points: pointCount, + pointRounding: 0.4, + valleyRounding: 0.6, + side: BorderSide( + width: 9, + color: colorScheme.tertiary + ), + ), + ), + child: Text( + '${pointCount.toInt()} Points', + style: theme.textTheme.headlineMedium!.copyWith( + color: colorScheme.onPrimaryContainer, + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + setState(() { + buttonPressCount += 1; + }); + }, + tooltip: "Change the shape's point count", + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/examples/api/lib/material/time_picker/show_time_picker.0.dart b/examples/api/lib/material/time_picker/show_time_picker.0.dart index 6e7b9cfee9cfc..d7d05702d6c49 100644 --- a/examples/api/lib/material/time_picker/show_time_picker.0.dart +++ b/examples/api/lib/material/time_picker/show_time_picker.0.dart @@ -129,7 +129,8 @@ class _TimePickerOptionsState extends State { gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 350, mainAxisSpacing: 4, - mainAxisExtent: 200 * MediaQuery.textScaleFactorOf(context), + // ignore: deprecated_member_use, https://github.com/flutter/flutter/issues/128825 + mainAxisExtent: 200 * MediaQuery.textScalerOf(context).textScaleFactor, crossAxisSpacing: 4, ), children: [ diff --git a/examples/api/lib/painting/image_provider/image_provider.0.dart b/examples/api/lib/painting/image_provider/image_provider.0.dart new file mode 100644 index 0000000000000..86349f15c0d0f --- /dev/null +++ b/examples/api/lib/painting/image_provider/image_provider.0.dart @@ -0,0 +1,104 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +@immutable +class CustomNetworkImage extends ImageProvider { + const CustomNetworkImage(this.url); + + final String url; + + @override + Future obtainKey(ImageConfiguration configuration) { + final Uri result = Uri.parse(url).replace( + queryParameters: { + 'dpr': '${configuration.devicePixelRatio}', + 'locale': '${configuration.locale?.toLanguageTag()}', + 'platform': '${configuration.platform?.name}', + 'width': '${configuration.size?.width}', + 'height': '${configuration.size?.height}', + 'bidi': '${configuration.textDirection?.name}', + }, + ); + return SynchronousFuture(result); + } + + static HttpClient get _httpClient { + HttpClient? client; + assert(() { + if (debugNetworkImageHttpClientProvider != null) { + client = debugNetworkImageHttpClientProvider!(); + } + return true; + }()); + return client ?? HttpClient()..autoUncompress = false; + } + + @override + ImageStreamCompleter loadImage(Uri key, ImageDecoderCallback decode) { + final StreamController chunkEvents = StreamController(); + debugPrint('Fetching "$key"...'); + return MultiFrameImageStreamCompleter( + codec: _httpClient.getUrl(key) + .then((HttpClientRequest request) => request.close()) + .then((HttpClientResponse response) { + return consolidateHttpClientResponseBytes( + response, + onBytesReceived: (int cumulative, int? total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + )); + }, + ); + }) + .catchError((Object e, StackTrace stack) { + scheduleMicrotask(() { + PaintingBinding.instance.imageCache.evict(key); + }); + return Future.error(e, stack); + }) + .whenComplete(chunkEvents.close) + .then(ui.ImmutableBuffer.fromUint8List) + .then(decode), + chunkEvents: chunkEvents.stream, + scale: 1.0, + debugLabel: '"key"', + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('URL', key), + ], + ); + } + + @override + String toString() => '${objectRuntimeType(this, 'CustomNetworkImage')}("$url")'; +} + +void main() => runApp(const ExampleApp()); + +class ExampleApp extends StatelessWidget { + const ExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Image( + image: const CustomNetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/flamingos.jpg'), + width: constraints.hasBoundedWidth ? constraints.maxWidth : null, + height: constraints.hasBoundedHeight ? constraints.maxHeight : null, + ); + }, + ), + ); + } +} diff --git a/examples/api/lib/painting/linear_border/linear_border.0.dart b/examples/api/lib/painting/linear_border/linear_border.0.dart index 991fa0fa02aa6..510a65236eb9f 100644 --- a/examples/api/lib/painting/linear_border/linear_border.0.dart +++ b/examples/api/lib/painting/linear_border/linear_border.0.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Examples of LinearBorder and LinearBorderEdge. - import 'package:flutter/material.dart'; +/// Examples for [LinearBorder] and [LinearBorderEdge]. + void main() { runApp(const ExampleApp()); } @@ -18,7 +18,8 @@ class ExampleApp extends StatelessWidget { return MaterialApp( theme: ThemeData.light(useMaterial3: true), home: const Directionality( - textDirection: TextDirection.ltr, // Or try rtl. + // TRY THIS: Switch to TextDirection.rtl to see how the borders change. + textDirection: TextDirection.ltr, child: Home(), ), ); diff --git a/examples/api/lib/rendering/box/parent_data.0.dart b/examples/api/lib/rendering/box/parent_data.0.dart new file mode 100644 index 0000000000000..0063f0e54427d --- /dev/null +++ b/examples/api/lib/rendering/box/parent_data.0.dart @@ -0,0 +1,394 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +void main() => runApp(const SampleApp()); + +class SampleApp extends StatefulWidget { + const SampleApp({super.key}); + + @override + State createState() => _SampleAppState(); +} + +class _SampleAppState extends State { + // This can be toggled using buttons in the UI to change which layout render object is used. + bool _compact = false; + + // This is the content we show in the rendering. + // + // Headline and Paragraph are simple custom widgets defined below. + // + // Any widget _could_ be specified here, and would render fine. + // The Headline and Paragraph widgets are used so that the renderer + // can distinguish between the kinds of content and use different + // spacing between different children. + static const List body = [ + Headline('Bugs that improve T for future bugs'), + Paragraph( + 'The best bugs to fix are those that make us more productive ' + 'in the future. Reducing test flakiness, reducing technical ' + 'debt, increasing the number of team members who are able to ' + 'review code confidently and well: this all makes future bugs ' + 'easier to fix, which is a huge multiplier to our overall ' + 'effectiveness and thus to developer happiness.', + ), + Headline('Bugs affecting more people are more valuable (maximize N)'), + Paragraph( + 'We will make more people happier if we fix a bug experienced by more people.' + ), + Paragraph( + 'One thing to be careful about is to think about the number of ' + 'people we are ignoring in our metrics. For example, if we had ' + 'a bug that prevented our product from working on Windows, we ' + 'would have no Windows users, so the bug would affect nobody. ' + 'However, fixing the bug would enable millions of developers ' + "to use our product, and that's the number that counts." + ), + Headline('Bugs with greater impact on developers are more valuable (maximize ΔH)'), + Paragraph( + 'A slight improvement to the user experience is less valuable ' + 'than a greater improvement. For example, if our application, ' + 'under certain conditions, shows a message with a typo, and ' + 'then crashes because of an off-by-one error in the code, ' + 'fixing the crash is a higher priority than fixing the typo.' + ), + ]; + + // This is the description of the demo's interface. + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Custom Render Boxes'), + // There are two buttons over to the top right of the demo that let you + // toggle between the two rendering modes. + actions: [ + IconButton( + icon: const Icon(Icons.density_small), + isSelected: _compact, + onPressed: () { + setState(() { _compact = true; }); + }, + ), + IconButton( + icon: const Icon(Icons.density_large), + isSelected: !_compact, + onPressed: () { + setState(() { _compact = false; }); + }, + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0), + // CompactLayout and OpenLayout are the two rendering widgets defined below. + child: _compact ? const CompactLayout(children: body) : const OpenLayout(children: body), + ), + ), + ); + } +} + +// Headline and Paragraph are just wrappers around the Text widget, but they +// also introduce a TextCategory widget that the CompactLayout and OpenLayout +// widgets can read to determine what kind of child is being rendered. + +class Headline extends StatelessWidget { + const Headline(this.text, { super.key }); + + final String text; + + @override + Widget build(BuildContext context) { + return TextCategory( + category: 'headline', + child: Text(text, style: Theme.of(context).textTheme.titleLarge), + ); + } +} + +class Paragraph extends StatelessWidget { + const Paragraph(this.text, { super.key }); + + final String text; + + @override + Widget build(BuildContext context) { + return TextCategory( + category: 'paragraph', + child: Text(text, style: Theme.of(context).textTheme.bodyLarge), + ); + } +} + +// This is the ParentDataWidget that allows us to specify what kind of child +// is being rendered. It allows information to be shared with the render object +// without violating the principle of agnostic composition (wherein parents should +// work with any child, not only support a fixed set of children). +class TextCategory extends ParentDataWidget { + const TextCategory({ super.key, required this.category, required super.child }); + + final String category; + + @override + void applyParentData(RenderObject renderObject) { + final TextFlowParentData parentData = renderObject.parentData! as TextFlowParentData; + if (parentData.category != category) { + parentData.category = category; + renderObject.parent!.markNeedsLayout(); + } + } + + @override + Type get debugTypicalAncestorWidgetClass => OpenLayout; +} + +// This is one of the two layout variants. It is a widget that defers to +// a render object defined below (RenderCompactLayout). +class CompactLayout extends MultiChildRenderObjectWidget { + const CompactLayout({ super.key, super.children }); + + @override + RenderCompactLayout createRenderObject(BuildContext context) { + return RenderCompactLayout(); + } + + @override + void updateRenderObject(BuildContext context, RenderCompactLayout renderObject) { + // nothing to update + } +} + +// This is the other of the two layout variants. It is a widget that defers to a +// render object defined below (RenderOpenLayout). +class OpenLayout extends MultiChildRenderObjectWidget { + const OpenLayout({ super.key, super.children }); + + @override + RenderOpenLayout createRenderObject(BuildContext context) { + return RenderOpenLayout(); + } + + @override + void updateRenderObject(BuildContext context, RenderOpenLayout renderObject) { + // nothing to update + } +} + +// This is the data structure that contains the kind of data that can be +// passed to the parent to label the child. It is literally stored on +// the RenderObject child, in its "parentData" field. +class TextFlowParentData extends ContainerBoxParentData { + String category = ''; +} + +// This is the bulk of the layout logic. (It's similar to RenderListBody, +// but only supports vertical layout.) It has no properties. +// +// This is an abstract class that is then extended by RenderCompactLayout and +// RenderOpenLayout to get different layouts based on the children's categories, +// as stored in the ParentData structure defined above. +// +// The documentation for the RenderBox class and its members provides much +// more detail on how to implement each of the methods below. +abstract class RenderTextFlow extends RenderBox + with ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderTextFlow({ List? children }) { + addAll(children); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! TextFlowParentData) { + child.parentData = TextFlowParentData(); + } + } + + // This is the function that is overridden by the subclasses to do the + // actual decision about the space to use between children. + double spacingBetween(String before, String after); + + // The next few functions are the layout functions. In each case we walk the + // children, calling each one to determine the geometry of the child, and use + // that to determine the layout. + + // The first two functions compute the intrinsic width of the render object, + // as seen when using the IntrinsicWidth widget. + // + // They essentially defer to the widest child. + + @override + double computeMinIntrinsicWidth(double height) { + double width = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final double childWidth = child.getMinIntrinsicWidth(height); + if (childWidth > width) { + width = childWidth; + } + child = childAfter(child); + } + return width; + } + + @override + double computeMaxIntrinsicWidth(double height) { + double width = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final double childWidth = child.getMaxIntrinsicWidth(height); + if (childWidth > width) { + width = childWidth; + } + child = childAfter(child); + } + return width; + } + + // The next two functions compute the intrinsic height of the render object, + // as seen when using the IntrinsicHeight widget. + // + // They add up the height contributed by each child. + // + // They have to take into account the categories of the children and the + // spacing that will be added, hence the slightly more elaborate logic. + + @override + double computeMinIntrinsicHeight(double width) { + String? previousCategory; + double height = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final String category = (child.parentData! as TextFlowParentData).category; + if (previousCategory != null) { + height += spacingBetween(previousCategory, category); + } + height += child.getMinIntrinsicHeight(width); + previousCategory = category; + child = childAfter(child); + } + return height; + } + + @override + double computeMaxIntrinsicHeight(double width) { + String? previousCategory; + double height = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final String category = (child.parentData! as TextFlowParentData).category; + if (previousCategory != null) { + height += spacingBetween(previousCategory, category); + } + height += child.getMaxIntrinsicHeight(width); + previousCategory = category; + child = childAfter(child); + } + return height; + } + + // This function implements the baseline logic. Because this class does + // nothing special, we just defer to the default implementation in the + // RenderBoxContainerDefaultsMixin utility class. + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToFirstActualBaseline(baseline); + } + + // Next we have a function similar to the intrinsic methods, but for both axes + // at the same time. + + @override + Size computeDryLayout(BoxConstraints constraints) { + final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); + String? previousCategory; + double y = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final String category = (child.parentData! as TextFlowParentData).category; + if (previousCategory != null) { + y += spacingBetween(previousCategory, category); + } + final Size childSize = child.getDryLayout(innerConstraints); + y += childSize.height; + previousCategory = category; + child = childAfter(child); + } + return constraints.constrain(Size(constraints.maxWidth, y)); + } + + // This is the core of the layout logic. Most of the time, this is the only + // function that will be called. It computes the size and position of each + // child, and stores it (in the parent data, as it happens!) for use during + // the paint phase. + + @override + void performLayout() { + final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth); + String? previousCategory; + double y = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final String category = (child.parentData! as TextFlowParentData).category; + if (previousCategory != null) { + // This is where we call the function that computes the spacing between + // the different children. The arguments are the categories, obtained + // from the parentData property of each child. + y += spacingBetween(previousCategory, category); + } + child.layout(innerConstraints, parentUsesSize: true); + (child.parentData! as TextFlowParentData).offset = Offset(0.0, y); + y += child.size.height; + previousCategory = category; + child = childAfter(child); + } + size = constraints.constrain(Size(constraints.maxWidth, y)); + } + + // Hit testing is normal for this widget, so we defer to the default implementation. + @override + bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + return defaultHitTestChildren(result, position: position); + } + + // Painting is normal for this widget, so we defer to the default + // implementation. The default implementation expects to find the positions + // configured in the parentData property of each child, which is why we + // configure it that way in performLayout above. + @override + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + } +} + +// Finally we have the two render objects that implement the two layouts in this demo. + +class RenderOpenLayout extends RenderTextFlow { + @override + double spacingBetween(String before, String after) { + if (after == 'headline') { + return 20.0; + } + if (before == 'headline') { + return 5.0; + } + return 10.0; + } +} + +class RenderCompactLayout extends RenderTextFlow { + @override + double spacingBetween(String before, String after) { + if (after == 'headline') { + return 4.0; + } + return 2.0; + } +} diff --git a/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart b/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart index 3be60f244dee3..77b09d3038e1d 100644 --- a/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart +++ b/examples/api/lib/services/keyboard_key/logical_keyboard_key.0.dart @@ -74,8 +74,8 @@ class _MyKeyExampleState extends State { child: Focus( focusNode: _focusNode, onKey: _handleKeyEvent, - child: AnimatedBuilder( - animation: _focusNode, + child: ListenableBuilder( + listenable: _focusNode, builder: (BuildContext context, Widget? child) { if (!_focusNode.hasFocus) { return GestureDetector( diff --git a/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart b/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart index c5eaf1cd25165..6b569409bbbad 100644 --- a/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart +++ b/examples/api/lib/services/keyboard_key/physical_keyboard_key.0.dart @@ -74,8 +74,8 @@ class _MyPhysicalKeyExampleState extends State { child: Focus( focusNode: _focusNode, onKey: _handleKeyEvent, - child: AnimatedBuilder( - animation: _focusNode, + child: ListenableBuilder( + listenable: _focusNode, builder: (BuildContext context, Widget? child) { if (!_focusNode.hasFocus) { return GestureDetector( diff --git a/examples/api/lib/widgets/actions/actions.0.dart b/examples/api/lib/widgets/actions/actions.0.dart index e8f3366e22671..9752b07cfa6b3 100644 --- a/examples/api/lib/widgets/actions/actions.0.dart +++ b/examples/api/lib/widgets/actions/actions.0.dart @@ -91,8 +91,8 @@ class _SaveButtonState extends State { @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.valueNotifier, + return ListenableBuilder( + listenable: widget.valueNotifier, builder: (BuildContext context, Widget? child) { return TextButton.icon( icon: const Icon(Icons.save), @@ -146,8 +146,8 @@ class _ActionsExampleState extends State { Actions.invoke(context, ModifyIntent(++count)); }, ), - AnimatedBuilder( - animation: model.data, + ListenableBuilder( + listenable: model.data, builder: (BuildContext context, Widget? child) { return Padding( padding: const EdgeInsets.all(8.0), diff --git a/examples/api/lib/widgets/form/form.1.dart b/examples/api/lib/widgets/form/form.1.dart new file mode 100644 index 0000000000000..b5e8b6e09ca1f --- /dev/null +++ b/examples/api/lib/widgets/form/form.1.dart @@ -0,0 +1,166 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// This sample demonstrates showing a confirmation dialog when the user +/// attempts to navigate away from a page with unsaved [Form] data. + +void main() => runApp(const FormApp()); + +class FormApp extends StatelessWidget { + const FormApp({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Confirmation Dialog Example'), + ), + body: Center( + child: _SaveableForm(), + ), + ), + ); + } +} + +class _SaveableForm extends StatefulWidget { + @override + State<_SaveableForm> createState() => _SaveableFormState(); +} + +class _SaveableFormState extends State<_SaveableForm> { + final TextEditingController _controller = TextEditingController(); + String _savedValue = ''; + bool _isDirty = false; + + @override + void initState() { + super.initState(); + _controller.addListener(_onChanged); + } + + @override + void dispose() { + _controller.removeListener(_onChanged); + super.dispose(); + } + + void _onChanged() { + final bool nextIsDirty = _savedValue != _controller.text; + if (nextIsDirty == _isDirty) { + return; + } + setState(() { + _isDirty = nextIsDirty; + }); + } + + Future _showDialog() async { + final bool? shouldDiscard = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Are you sure?'), + content: const Text('Any unsaved changes will be lost!'), + actions: [ + TextButton( + child: const Text('Yes, discard my changes'), + onPressed: () { + Navigator.pop(context, true); + }, + ), + TextButton( + child: const Text('No, continue editing'), + onPressed: () { + Navigator.pop(context, false); + }, + ), + ], + ); + }, + ); + + if (shouldDiscard ?? false) { + // Since this is the root route, quit the app where possible by invoking + // the SystemNavigator. If this wasn't the root route, then + // Navigator.maybePop could be used instead. + // See https://github.com/flutter/flutter/issues/11490 + SystemNavigator.pop(); + } + } + + void _save(String? value) { + setState(() { + _savedValue = value ?? ''; + }); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('If the field below is unsaved, a confirmation dialog will be shown on back.'), + const SizedBox(height: 20.0), + Form( + canPop: !_isDirty, + onPopInvoked: (bool didPop) { + if (didPop) { + return; + } + _showDialog(); + }, + autovalidateMode: AutovalidateMode.always, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: _controller, + onFieldSubmitted: (String? value) { + _save(value); + }, + ), + TextButton( + onPressed: () { + _save(_controller.text); + }, + child: Row( + children: [ + const Text('Save'), + if (_controller.text.isNotEmpty) + Icon( + _isDirty ? Icons.warning : Icons.check, + ), + ], + ), + ), + ], + ), + ), + TextButton( + onPressed: () { + if (_isDirty) { + _showDialog(); + return; + } + // Since this is the root route, quit the app where possible by + // invoking the SystemNavigator. If this wasn't the root route, + // then Navigator.maybePop could be used instead. + // See https://github.com/flutter/flutter/issues/11490 + SystemNavigator.pop(); + }, + child: const Text('Go back'), + ), + ], + ), + ); + } +} diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart new file mode 100644 index 0000000000000..d81b74f65f714 --- /dev/null +++ b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart @@ -0,0 +1,164 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// This sample demonstrates using [NavigatorPopHandler] to handle system back +/// gestures when there are nested [Navigator] widgets by delegating to the +/// current [Navigator]. + +void main() => runApp(const NavigatorPopHandlerApp()); + +class NavigatorPopHandlerApp extends StatelessWidget { + const NavigatorPopHandlerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + initialRoute: '/', + routes: { + '/': (BuildContext context) => _HomePage(), + '/nested_navigators': (BuildContext context) => const NestedNavigatorsPage(), + }, + ); + } +} + +class _HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Nested Navigators Example'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Home Page'), + const Text('A system back gesture here will exit the app.'), + const SizedBox(height: 20.0), + ListTile( + title: const Text('Nested Navigator route'), + subtitle: const Text('This route has another Navigator widget in addition to the one inside MaterialApp above.'), + onTap: () { + Navigator.of(context).pushNamed('/nested_navigators'); + }, + ), + ], + ), + ), + ); + } +} + +class NestedNavigatorsPage extends StatefulWidget { + const NestedNavigatorsPage({super.key}); + + @override + State createState() => _NestedNavigatorsPageState(); +} + +class _NestedNavigatorsPageState extends State { + final GlobalKey _nestedNavigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return NavigatorPopHandler( + onPop: () { + _nestedNavigatorKey.currentState!.maybePop(); + }, + child: Navigator( + key: _nestedNavigatorKey, + initialRoute: 'nested_navigators/one', + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case 'nested_navigators/one': + final BuildContext rootContext = context; + return MaterialPageRoute( + builder: (BuildContext context) => NestedNavigatorsPageOne( + onBack: () { + Navigator.of(rootContext).pop(); + }, + ), + ); + case 'nested_navigators/one/another_one': + return MaterialPageRoute( + builder: (BuildContext context) => const NestedNavigatorsPageTwo( + ), + ); + default: + throw Exception('Invalid route: ${settings.name}'); + } + }, + ), + ); + } +} + +class NestedNavigatorsPageOne extends StatelessWidget { + const NestedNavigatorsPageOne({ + required this.onBack, + super.key, + }); + + final VoidCallback onBack; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Nested Navigators Page One'), + const Text('A system back here returns to the home page.'), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('nested_navigators/one/another_one'); + }, + child: const Text('Go to another route in this nested Navigator'), + ), + TextButton( + // Can't use Navigator.of(context).pop() because this is the root + // route, so it can't be popped. The Navigator above this needs to + // be popped. + onPressed: onBack, + child: const Text('Go back'), + ), + ], + ), + ), + ); + } +} + +class NestedNavigatorsPageTwo extends StatelessWidget { + const NestedNavigatorsPageTwo({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.withBlue(180), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Nested Navigators Page Two'), + const Text('A system back here will go back to Nested Navigators Page One'), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Go back'), + ), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart new file mode 100644 index 0000000000000..04fbdedd34e56 --- /dev/null +++ b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart @@ -0,0 +1,250 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This sample demonstrates nested navigation in a bottom navigation bar. + +import 'package:flutter/material.dart'; + +// There are three possible tabs. +enum _Tab { + home, + one, + two, +} + +// Each tab has two possible pages. +enum _TabPage { + home, + one, +} + +typedef _TabPageCallback = void Function(List<_TabPage> pages); + +void main() => runApp(const NavigatorPopHandlerApp()); + +class NavigatorPopHandlerApp extends StatelessWidget { + const NavigatorPopHandlerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + initialRoute: '/home', + routes: { + '/home': (BuildContext context) => const _BottomNavPage( + ), + }, + ); + } +} + +class _BottomNavPage extends StatefulWidget { + const _BottomNavPage(); + + @override + State<_BottomNavPage> createState() => _BottomNavPageState(); +} + +class _BottomNavPageState extends State<_BottomNavPage> { + _Tab _tab = _Tab.home; + + final GlobalKey _tabHomeKey = GlobalKey(); + final GlobalKey _tabOneKey = GlobalKey(); + final GlobalKey _tabTwoKey = GlobalKey(); + + List<_TabPage> _tabHomePages = <_TabPage>[_TabPage.home]; + List<_TabPage> _tabOnePages = <_TabPage>[_TabPage.home]; + List<_TabPage> _tabTwoPages = <_TabPage>[_TabPage.home]; + + BottomNavigationBarItem _itemForPage(_Tab page) { + switch (page) { + case _Tab.home: + return const BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Go to Home', + ); + case _Tab.one: + return const BottomNavigationBarItem( + icon: Icon(Icons.one_k), + label: 'Go to One', + ); + case _Tab.two: + return const BottomNavigationBarItem( + icon: Icon(Icons.two_k), + label: 'Go to Two', + ); + } + } + + Widget _getPage(_Tab page) { + switch (page) { + case _Tab.home: + return _BottomNavTab( + key: _tabHomeKey, + title: 'Home Tab', + color: Colors.grey, + pages: _tabHomePages, + onChangedPages: (List<_TabPage> pages) { + setState(() { + _tabHomePages = pages; + }); + }, + ); + case _Tab.one: + return _BottomNavTab( + key: _tabOneKey, + title: 'Tab One', + color: Colors.amber, + pages: _tabOnePages, + onChangedPages: (List<_TabPage> pages) { + setState(() { + _tabOnePages = pages; + }); + }, + ); + case _Tab.two: + return _BottomNavTab( + key: _tabTwoKey, + title: 'Tab Two', + color: Colors.blueGrey, + pages: _tabTwoPages, + onChangedPages: (List<_TabPage> pages) { + setState(() { + _tabTwoPages = pages; + }); + }, + ); + } + } + + void _onItemTapped(int index) { + setState(() { + _tab = _Tab.values.elementAt(index); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: _getPage(_tab), + ), + bottomNavigationBar: BottomNavigationBar( + items: _Tab.values.map(_itemForPage).toList(), + currentIndex: _Tab.values.indexOf(_tab), + selectedItemColor: Colors.amber[800], + onTap: _onItemTapped, + ), + ); + } +} + +class _BottomNavTab extends StatefulWidget { + const _BottomNavTab({ + super.key, + required this.color, + required this.onChangedPages, + required this.pages, + required this.title, + }); + + final Color color; + final _TabPageCallback onChangedPages; + final List<_TabPage> pages; + final String title; + + @override + State<_BottomNavTab> createState() => _BottomNavTabState(); +} + +class _BottomNavTabState extends State<_BottomNavTab> { + final GlobalKey _navigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return NavigatorPopHandler( + onPop: () { + _navigatorKey.currentState?.maybePop(); + }, + child: Navigator( + key: _navigatorKey, + onPopPage: (Route route, void result) { + if (!route.didPop(null)) { + return false; + } + widget.onChangedPages(<_TabPage>[ + ...widget.pages, + ]..removeLast()); + return true; + }, + pages: widget.pages.map((_TabPage page) { + switch (page) { + case _TabPage.home: + return MaterialPage( + child: _LinksPage( + title: 'Bottom nav - tab ${widget.title} - route $page', + backgroundColor: widget.color, + buttons: [ + TextButton( + onPressed: () { + widget.onChangedPages(<_TabPage>[ + ...widget.pages, + _TabPage.one, + ]); + }, + child: const Text('Go to another route in this nested Navigator'), + ), + ], + ), + ); + case _TabPage.one: + return MaterialPage( + child: _LinksPage( + backgroundColor: widget.color, + title: 'Bottom nav - tab ${widget.title} - route $page', + buttons: [ + TextButton( + onPressed: () { + widget.onChangedPages(<_TabPage>[ + ...widget.pages, + ]..removeLast()); + }, + child: const Text('Go back'), + ), + ], + ), + ); + } + }).toList(), + ), + ); + } +} + +class _LinksPage extends StatelessWidget { + const _LinksPage ({ + required this.backgroundColor, + this.buttons = const [], + required this.title, + }); + + final Color backgroundColor; + final List buttons; + final String title; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title), + ...buttons, + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/overlay/overlay.0.dart b/examples/api/lib/widgets/overlay/overlay.0.dart index 41a769c9e8563..2a69e58f3d8a9 100644 --- a/examples/api/lib/widgets/overlay/overlay.0.dart +++ b/examples/api/lib/widgets/overlay/overlay.0.dart @@ -138,6 +138,7 @@ class _OverlayExampleState extends State { // Remove the OverlayEntry. void removeHighlightOverlay() { overlayEntry?.remove(); + overlayEntry?.dispose(); overlayEntry = null; } diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart b/examples/api/lib/widgets/pop_scope/pop_scope.0.dart new file mode 100644 index 0000000000000..6d144bd088de1 --- /dev/null +++ b/examples/api/lib/widgets/pop_scope/pop_scope.0.dart @@ -0,0 +1,128 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This sample demonstrates showing a confirmation dialog before navigating +// away from a page. + +import 'package:flutter/material.dart'; + +void main() => runApp(const NavigatorPopHandlerApp()); + +class NavigatorPopHandlerApp extends StatelessWidget { + const NavigatorPopHandlerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + initialRoute: '/home', + routes: { + '/home': (BuildContext context) => const _HomePage(), + '/two': (BuildContext context) => const _PageTwo(), + }, + ); + } +} + +class _HomePage extends StatefulWidget { + const _HomePage(); + + @override + State<_HomePage> createState() => _HomePageState(); +} + +class _HomePageState extends State<_HomePage> { + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Page One'), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/two'); + }, + child: const Text('Next page'), + ), + ], + ), + ), + ); + } +} + +class _PageTwo extends StatefulWidget { + const _PageTwo(); + + @override + State<_PageTwo> createState() => _PageTwoState(); +} + +class _PageTwoState extends State<_PageTwo> { + void _showBackDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Are you sure?'), + content: const Text( + 'Are you sure you want to leave this page?', + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Nevermind'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Leave'), + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Page Two'), + PopScope( + canPop: false, + onPopInvoked: (bool didPop) { + if (didPop) { + return; + } + _showBackDialog(); + }, + child: TextButton( + onPressed: () { + _showBackDialog(); + }, + child: const Text('Go back'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/scroll_view/grid_view.0.dart b/examples/api/lib/widgets/scroll_view/grid_view.0.dart new file mode 100644 index 0000000000000..218429fde5ef2 --- /dev/null +++ b/examples/api/lib/widgets/scroll_view/grid_view.0.dart @@ -0,0 +1,190 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +void main() => runApp(const GridViewExampleApp()); + +class GridViewExampleApp extends StatelessWidget { + const GridViewExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Padding( + padding: const EdgeInsets.all(20.0), + child: Card( + elevation: 8.0, + child: GridView.builder( + padding: const EdgeInsets.all(12.0), + gridDelegate: CustomGridDelegate(dimension: 240.0), + // Try uncommenting some of these properties to see the effect on the grid: + // itemCount: 20, // The default is that the number of grid tiles is infinite. + // scrollDirection: Axis.horizontal, // The default is vertical. + // reverse: true, // The default is false, going down (or left to right). + itemBuilder: (BuildContext context, int index) { + final math.Random random = math.Random(index); + return GridTile( + header: GridTileBar( + title: Text('$index', style: const TextStyle(color: Colors.black)), + ), + child: Container( + margin: const EdgeInsets.all(12.0), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + gradient: const RadialGradient( + colors: [ Color(0x0F88EEFF), Color(0x2F0099BB) ], + ), + ), + child: FlutterLogo( + style: FlutterLogoStyle.values[random.nextInt(FlutterLogoStyle.values.length)], + ), + ), + ); + }, + ), + ), + ), + ); + } +} + +class CustomGridDelegate extends SliverGridDelegate { + CustomGridDelegate({ required this.dimension }); + + // This is the desired height of each row (and width of each square). + // When there is not enough room, we shrink this to the width of the scroll view. + final double dimension; + + // The layout is two rows of squares, then one very wide cell, repeat. + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + // Determine how many squares we can fit per row. + int count = constraints.crossAxisExtent ~/ dimension; + if (count < 1) { + count = 1; // Always fit at least one regardless. + } + final double squareDimension = constraints.crossAxisExtent / count; + return CustomGridLayout( + crossAxisCount: count, + fullRowPeriod: 3, // Number of rows per block (one of which is the full row). + dimension: squareDimension, + ); + } + + @override + bool shouldRelayout(CustomGridDelegate oldDelegate) { + return dimension != oldDelegate.dimension; + } +} + +class CustomGridLayout extends SliverGridLayout { + const CustomGridLayout({ + required this.crossAxisCount, + required this.dimension, + required this.fullRowPeriod, + }) : assert(crossAxisCount > 0), + assert(fullRowPeriod > 1), + loopLength = crossAxisCount * (fullRowPeriod - 1) + 1, + loopHeight = fullRowPeriod * dimension; + + final int crossAxisCount; + final double dimension; + final int fullRowPeriod; + + // Computed values. + final int loopLength; + final double loopHeight; + + @override + double computeMaxScrollOffset(int childCount) { + // This returns the scroll offset of the end side of the childCount'th child. + // In the case of this example, this method is not used, since the grid is + // infinite. However, if one set an itemCount on the GridView above, this + // function would be used to determine how far to allow the user to scroll. + if (childCount == 0 || dimension == 0) { + return 0; + } + return (childCount ~/ loopLength) * loopHeight + + ((childCount % loopLength) ~/ crossAxisCount) * dimension; + } + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + // This returns the position of the index'th tile. + // + // The SliverGridGeometry object returned from this method has four + // properties. For a grid that scrolls down, as in this example, the four + // properties are equivalent to x,y,width,height. However, since the + // GridView is direction agnostic, the names used for SliverGridGeometry are + // also direction-agnostic. + // + // Try changing the scrollDirection and reverse properties on the GridView + // to see how this algorithm works in any direction (and why, therefore, the + // names are direction-agnostic). + final int loop = index ~/ loopLength; + final int loopIndex = index % loopLength; + if (loopIndex == loopLength - 1) { + // Full width case. + return SliverGridGeometry( + scrollOffset: (loop + 1) * loopHeight - dimension, // "y" + crossAxisOffset: 0, // "x" + mainAxisExtent: dimension, // "height" + crossAxisExtent: crossAxisCount * dimension, // "width" + ); + } + // Square case. + final int rowIndex = loopIndex ~/ crossAxisCount; + final int columnIndex = loopIndex % crossAxisCount; + return SliverGridGeometry( + scrollOffset: (loop * loopHeight) + (rowIndex * dimension), // "y" + crossAxisOffset: columnIndex * dimension, // "x" + mainAxisExtent: dimension, // "height" + crossAxisExtent: dimension, // "width" + ); + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + // This returns the first index that is visible for a given scrollOffset. + // + // The GridView only asks for the geometry of children that are visible + // between the scroll offset passed to getMinChildIndexForScrollOffset and + // the scroll offset passed to getMaxChildIndexForScrollOffset. + // + // It is the responsibility of the SliverGridLayout to ensure that + // getGeometryForChildIndex is consistent with getMinChildIndexForScrollOffset + // and getMaxChildIndexForScrollOffset. + // + // Not every child between the minimum child index and the maximum child + // index need be visible (some may have scroll offsets that are outside the + // view; this happens commonly when the grid view places tiles out of + // order). However, doing this means the grid view is less efficient, as it + // will do work for children that are not visible. It is preferred that the + // children are returned in the order that they are laid out. + final int rows = scrollOffset ~/ dimension; + final int loops = rows ~/ fullRowPeriod; + final int extra = rows % fullRowPeriod; + return loops * loopLength + extra * crossAxisCount; + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + // (See commentary above.) + final int rows = scrollOffset ~/ dimension; + final int loops = rows ~/ fullRowPeriod; + final int extra = rows % fullRowPeriod; + final int count = loops * loopLength + extra * crossAxisCount; + if (extra == fullRowPeriod - 1) { + return count; + } + return count + crossAxisCount - 1; + } +} diff --git a/examples/api/lib/widgets/scroll_view/list_view.0.dart b/examples/api/lib/widgets/scroll_view/list_view.0.dart index 954995f4b9d00..870420fdfd8f1 100644 --- a/examples/api/lib/widgets/scroll_view/list_view.0.dart +++ b/examples/api/lib/widgets/scroll_view/list_view.0.dart @@ -136,7 +136,7 @@ class GridBuilder extends StatefulWidget { }); final bool isSelectionMode; - final Function(bool)? onSelectionChange; + final ValueChanged? onSelectionChange; final List selectedList; @override @@ -189,7 +189,7 @@ class ListBuilder extends StatefulWidget { final bool isSelectionMode; final List selectedList; - final Function(bool)? onSelectionChange; + final ValueChanged? onSelectionChange; @override State createState() => _ListBuilderState(); diff --git a/examples/api/lib/widgets/shortcuts/shortcuts.1.dart b/examples/api/lib/widgets/shortcuts/shortcuts.1.dart index 6f1281d0fc592..8d59f00601fcd 100644 --- a/examples/api/lib/widgets/shortcuts/shortcuts.1.dart +++ b/examples/api/lib/widgets/shortcuts/shortcuts.1.dart @@ -100,8 +100,8 @@ class _ShortcutsExampleState extends State { children: [ const Text('Add to the counter by pressing the up arrow key'), const Text('Subtract from the counter by pressing the down arrow key'), - AnimatedBuilder( - animation: model, + ListenableBuilder( + listenable: model, builder: (BuildContext context, Widget? child) { return Text('count: ${model.count}'); }, diff --git a/examples/api/lib/widgets/transitions/listenable_builder.3.dart b/examples/api/lib/widgets/transitions/listenable_builder.3.dart index 9d65da22a6c92..8bc7ae1ed454c 100644 --- a/examples/api/lib/widgets/transitions/listenable_builder.3.dart +++ b/examples/api/lib/widgets/transitions/listenable_builder.3.dart @@ -39,7 +39,7 @@ class _ListenableBuilderExampleState extends State { appBar: AppBar(title: const Text('ListenableBuilder Example')), body: ListBody(listNotifier: _listNotifier), floatingActionButton: FloatingActionButton( - onPressed: () => _listNotifier.add(_random.nextInt(1 << 32)), // 1 << 32 is the maximum supported value + onPressed: () => _listNotifier.add(_random.nextInt(1 << 31)), // 1 << 31 is the maximum supported value child: const Icon(Icons.add), ), ), diff --git a/examples/api/lib/widgets/transitions/matrix_transition.0.dart b/examples/api/lib/widgets/transitions/matrix_transition.0.dart new file mode 100644 index 0000000000000..2d687a31e7a70 --- /dev/null +++ b/examples/api/lib/widgets/transitions/matrix_transition.0.dart @@ -0,0 +1,75 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [MatrixTransition]. + +void main() => runApp(const MatrixTransitionExampleApp()); + +class MatrixTransitionExampleApp extends StatelessWidget { + const MatrixTransitionExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: MatrixTransitionExample(), + ); + } +} + +class MatrixTransitionExample extends StatefulWidget { + const MatrixTransitionExample({super.key}); + + @override + State createState() => _MatrixTransitionExampleState(); +} + +/// [AnimationController]s can be created with `vsync: this` because of +/// [TickerProviderStateMixin]. +class _MatrixTransitionExampleState extends State with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(); + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: MatrixTransition( + animation: _animation, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: FlutterLogo(size: 150.0), + ), + onTransform: (double value) { + return Matrix4.identity() + ..setEntry(3, 2, 0.004) + ..rotateY(pi * 2.0 * value); + }, + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart b/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart deleted file mode 100644 index 46dafa57b98ce..0000000000000 --- a/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -/// Flutter code sample for [WillPopScope]. - -void main() => runApp(const WillPopScopeExampleApp()); - -class WillPopScopeExampleApp extends StatelessWidget { - const WillPopScopeExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: WillPopScopeExample(), - ); - } -} - -class WillPopScopeExample extends StatefulWidget { - const WillPopScopeExample({super.key}); - - @override - State createState() => _WillPopScopeExampleState(); -} - -class _WillPopScopeExampleState extends State { - bool shouldPop = true; - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Scaffold( - appBar: AppBar( - title: const Text('Flutter WillPopScope demo'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - OutlinedButton( - child: const Text('Push'), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return const WillPopScopeExample(); - }, - ), - ); - }, - ), - OutlinedButton( - child: Text('shouldPop: $shouldPop'), - onPressed: () { - setState( - () { - shouldPop = !shouldPop; - }, - ); - }, - ), - const Text('Push to a new screen, then tap on shouldPop ' - 'button to toggle its value. Press the back ' - 'button in the appBar to check its behavior ' - 'for different values of shouldPop'), - ], - ), - ), - ), - ); - } -} diff --git a/examples/api/pubspec.yaml b/examples/api/pubspec.yaml index eed8da11cee1a..4322738a0196a 100644 --- a/examples/api/pubspec.yaml +++ b/examples/api/pubspec.yaml @@ -7,20 +7,20 @@ publish_to: 'none' version: 1.0.0 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=2.5.0-6.0.pre.30 <3.0.0" dependencies: - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: integration_test: @@ -29,12 +29,16 @@ dev_dependencies: sdk: flutter flutter_goldens: sdk: flutter + flutter_localizations: + sdk: flutter flutter_test: sdk: flutter - test: 1.24.3 + flutter_web_plugins: + sdk: flutter + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -48,6 +52,7 @@ dev_dependencies: glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_multi_server: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + intl: 0.18.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -56,7 +61,7 @@ dev_dependencies: node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -67,22 +72,22 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 0b3d +# PUBSPEC CHECKSUM: f0d8 diff --git a/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart b/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart new file mode 100644 index 0000000000000..991d8ef493795 --- /dev/null +++ b/examples/api/test/gestures/tap_and_drag/tap_and_drag.0_test.dart @@ -0,0 +1,88 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/gestures/tap_and_drag/tap_and_drag.0.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Single tap + drag should not change the scale of child', (WidgetTester tester) async { + await tester.pumpWidget( + const example.TapAndDragToZoomApp(), + ); + + double getScale() { + final RenderBox box = tester.renderObject(find.byType(Container).first); + return box.getTransformTo(null)[0]; + } + + final Finder containerFinder = find.byType(Container).first; + final Offset centerOfChild = tester.getCenter(containerFinder); + + expect(getScale(), 1.0); + + // Single tap + drag down. + final TestGesture gesture = await tester.startGesture(centerOfChild); + await tester.pump(); + await gesture.moveTo(centerOfChild + const Offset(0, 100.0)); + await tester.pump(); + expect(getScale(), 1.0); + + // Single tap + drag up. + await gesture.moveTo(centerOfChild); + await tester.pump(); + expect(getScale(), 1.0); + }); + + testWidgets('Double tap + drag should change the scale of the child', (WidgetTester tester) async { + await tester.pumpWidget( + const example.TapAndDragToZoomApp(), + ); + + double getScale() { + final RenderBox box = tester.renderObject(find.byType(Container).first); + return box.getTransformTo(null)[0]; + } + + final Finder containerFinder = find.byType(Container).first; + final Offset centerOfChild = tester.getCenter(containerFinder); + + expect(getScale(), 1.0); + + // Double tap + drag down to scale up. + final TestGesture gesture = await tester.startGesture(centerOfChild); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(centerOfChild); + await tester.pump(); + await gesture.moveTo(centerOfChild + const Offset(0, 100.0)); + await tester.pump(); + expect(getScale(), greaterThan(1.0)); + + // Scale is reset on drag end. + await gesture.up(); + await tester.pumpAndSettle(); + expect(getScale(), 1.0); + + // Double tap + drag up to scale down. + await gesture.down(centerOfChild); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(centerOfChild); + await tester.pump(); + await gesture.moveTo(centerOfChild + const Offset(0, -100.0)); + await tester.pump(); + expect(getScale(), lessThan(1.0)); + + // Scale is reset on drag end. + await gesture.up(); + await tester.pumpAndSettle(); + expect(getScale(), 1.0); + }); +} diff --git a/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart b/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart index 31b89ee89dd03..a7147873fbe4a 100644 --- a/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart +++ b/examples/api/test/material/context_menu/selectable_region_toolbar_builder.0_test.dart @@ -24,6 +24,9 @@ void main() { // Right clicking the Text in the SelectionArea shows the custom context // menu. + final TestGesture primaryMouseButtonGesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); final TestGesture gesture = await tester.startGesture( tester.getCenter(find.text(example.text)), kind: PointerDeviceKind.mouse, @@ -37,7 +40,9 @@ void main() { expect(find.text('Print'), findsOneWidget); // Tap to dismiss. - await tester.tapAt(tester.getCenter(find.byType(Scaffold))); + await primaryMouseButtonGesture.down(tester.getCenter(find.byType(Scaffold))); + await tester.pump(); + await primaryMouseButtonGesture.up(); await tester.pumpAndSettle(); expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); diff --git a/examples/api/test/material/data_table/data_table.1_test.dart b/examples/api/test/material/data_table/data_table.1_test.dart new file mode 100644 index 0000000000000..6b14a8cefb721 --- /dev/null +++ b/examples/api/test/material/data_table/data_table.1_test.dart @@ -0,0 +1,24 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/data_table/data_table.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('DataTable is scrollable', (WidgetTester tester) async { + await tester.pumpWidget( + const example.DataTableExampleApp(), + ); + + expect(find.byType(SingleChildScrollView), findsOneWidget); + + expect(tester.getTopLeft(find.text('Row 5')), const Offset(66.0, 366.0)); + + await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Row 5')), const Offset(66.0, 186.0)); + }); +} diff --git a/examples/api/test/material/drawer/drawer.0_test.dart b/examples/api/test/material/drawer/drawer.0_test.dart new file mode 100644 index 0000000000000..6b942022173e1 --- /dev/null +++ b/examples/api/test/material/drawer/drawer.0_test.dart @@ -0,0 +1,43 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/drawer/drawer.0.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Navigation bar updates destination on tap', + (WidgetTester tester) async { + await tester.pumpWidget( + const example.DrawerApp(), + ); + + await tester.tap(find.byIcon(Icons.menu)); + await tester.pumpAndSettle(); + + /// NavigationDestinations must be rendered + expect(find.text('Messages'), findsOneWidget); + expect(find.text('Profile'), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + + /// Initial index must be zero + expect(find.text('Page: '), findsOneWidget); + + /// Switch to second tab + await tester.tap(find.ancestor(of: find.text('Messages'), matching: find.byType(InkWell))); + await tester.pumpAndSettle(); + expect(find.text('Page: Messages'), findsOneWidget); + + /// Switch to third tab + await tester.tap(find.ancestor(of: find.text('Profile'), matching: find.byType(InkWell))); + await tester.pumpAndSettle(); + expect(find.text('Page: Profile'), findsOneWidget); + + /// Switch to fourth tab + await tester.tap(find.ancestor(of: find.text('Settings'), matching: find.byType(InkWell))); + await tester.pumpAndSettle(); + expect(find.text('Page: Settings'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart b/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart new file mode 100644 index 0000000000000..c1c662f0ec4c4 --- /dev/null +++ b/examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart @@ -0,0 +1,77 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/dropdown_menu/dropdown_menu.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('DropdownMenu', (WidgetTester tester) async { + await tester.pumpWidget( + const example.DropdownMenuExample(), + ); + + expect(find.text('You selected a Blue Smile'), findsNothing); + + final Finder colorMenu = find.byType(DropdownMenu); + final Finder iconMenu = find.byType(DropdownMenu); + expect(colorMenu, findsOneWidget); + expect(iconMenu, findsOneWidget); + + Finder findMenuItem(String label) { + return find.widgetWithText(MenuItemButton, label).last; + } + + await tester.tap(colorMenu); + await tester.pumpAndSettle(); + expect(findMenuItem('Blue'), findsOneWidget); + expect(findMenuItem('Pink'), findsOneWidget); + expect(findMenuItem('Green'), findsOneWidget); + expect(findMenuItem('Orange'), findsOneWidget); + expect(findMenuItem('Grey'), findsOneWidget); + + await tester.tap(findMenuItem('Blue')); + + // The DropdownMenu's onSelected callback is delayed + // with SchedulerBinding.instance.addPostFrameCallback + // to give the focus a chance to return to where it was + // before the menu appeared. The pumpAndSettle() + // give the callback a chance to run. + await tester.pumpAndSettle(); + + await tester.tap(iconMenu); + await tester.pumpAndSettle(); + expect(findMenuItem('Smile'), findsOneWidget); + expect(findMenuItem('Cloud'), findsOneWidget); + expect(findMenuItem('Brush'), findsOneWidget); + expect(findMenuItem('Heart'), findsOneWidget); + + await tester.tap(findMenuItem('Smile')); + await tester.pumpAndSettle(); + + expect(find.text('You selected a Blue Smile'), findsOneWidget); + }); + + testWidgets('DropdownMenu has focus when tapping on the text field', (WidgetTester tester) async { + await tester.pumpWidget( + const example.DropdownMenuExample(), + ); + + // Make sure the dropdown menus are there. + final Finder colorMenu = find.byType(DropdownMenu); + final Finder iconMenu = find.byType(DropdownMenu); + expect(colorMenu, findsOneWidget); + expect(iconMenu, findsOneWidget); + + // Tap on the color menu and make sure it is focused. + await tester.tap(colorMenu); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(colorMenu)).hasFocus, isTrue); + + // Tap on the icon menu and make sure it is focused. + await tester.tap(iconMenu); + await tester.pumpAndSettle(); + expect(FocusScope.of(tester.element(iconMenu)).hasFocus, isTrue); + }); +} diff --git a/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart b/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart new file mode 100644 index 0000000000000..773c991e9cc08 --- /dev/null +++ b/examples/api/test/material/dropdown_menu/dropdown_menu_entry_label_widget.0_test.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('DropdownEntryLabelWidget appears', (WidgetTester tester) async { + await tester.pumpWidget( + const example.DropdownMenuEntryLabelWidgetExampleApp(), + ); + + const String longText = 'is a color that sings of hope, A hue that shines like gold. It is the color of dreams, A shade that never grows old.'; + Finder findMenuItemText(String label) { + final String labelText = '$label $longText\n'; + return find.descendant( + of: find.widgetWithText(MenuItemButton, labelText), + matching: find.byType(Text), + ).last; + } + + // Open the menu + await tester.tap(find.byType(TextField)); + expect(findMenuItemText('Blue'), findsOneWidget); + expect(findMenuItemText('Pink'), findsOneWidget); + expect(findMenuItemText('Green'), findsOneWidget); + expect(findMenuItemText('Yellow'), findsOneWidget); + expect(findMenuItemText('Grey'), findsOneWidget); + + // Close the menu + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + }); +} diff --git a/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart b/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart new file mode 100644 index 0000000000000..fc64f447c9d3d --- /dev/null +++ b/examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart @@ -0,0 +1,75 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/expansion_panel/expansion_panel_list.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ExpansionPanel can be expanded', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ExpansionPanelListExampleApp(), + ); + + // Verify the first tile is collapsed. + expect(tester.widget(find.byType(ExpandIcon).first).isExpanded, false); + + // Tap to expand the first tile. + await tester.tap(find.byType(ExpandIcon).first); + await tester.pumpAndSettle(); + + // Verify that the first tile is expanded. + expect(tester.widget(find.byType(ExpandIcon).first).isExpanded, true); + }); + + testWidgets('Tap to delete a ExpansionPanel', (WidgetTester tester) async { + const int index = 3; + + await tester.pumpWidget( + const example.ExpansionPanelListExampleApp(), + ); + + expect(find.widgetWithText(ListTile, 'Panel $index'), findsOneWidget); + expect(tester.widget(find.byType(ExpandIcon).at(index)).isExpanded, false); + + // Tap to expand the tile at index 3. + await tester.tap(find.byType(ExpandIcon).at(index)); + await tester.pumpAndSettle(); + + expect(tester.widget(find.byType(ExpandIcon).at(index)).isExpanded, true); + + // Tap to delete the tile at index 3. + await tester.tap(find.byIcon(Icons.delete).at(index)); + await tester.pumpAndSettle(); + + // Verify that the tile at index 3 is deleted. + expect(find.widgetWithText(ListTile, 'Panel $index'), findsNothing); + }); + + testWidgets('ExpansionPanelList is scrollable', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ExpansionPanelListExampleApp(), + ); + + expect(find.byType(SingleChildScrollView), findsOneWidget); + + // Expand all the tiles. + for (int i = 0; i < 8; i++) { + await tester.tap(find.byType(ExpandIcon).at(i)); + } + await tester.pumpAndSettle(); + + // Check panel 3 tile position. + Offset tilePosition = tester.getBottomLeft(find.widgetWithText(ListTile, 'Panel 3')); + expect(tilePosition.dy, 656.0); + + // Scroll up. + await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300)); + await tester.pumpAndSettle(); + + // Verify panel 3 tile position is updated after scrolling. + tilePosition = tester.getBottomLeft(find.widgetWithText(ListTile, 'Panel 3')); + expect(tilePosition.dy, 376.0); + }); +} diff --git a/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart b/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart index eb6ba0e8e7c0c..d503e5df27b7a 100644 --- a/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart +++ b/examples/api/test/material/floating_action_button/floating_action_button.1_test.dart @@ -17,7 +17,7 @@ void main() { const example.FloatingActionButtonExampleApp(), ); - final ThemeData theme = ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true); + final ThemeData theme = ThemeData(useMaterial3: true); expect(find.byType(FloatingActionButton), findsNWidgets(4)); expect(find.byIcon(Icons.add), findsNWidgets(4)); @@ -42,7 +42,7 @@ void main() { final Finder extendedFABMaterialButton = find.byType(RawMaterialButton).at(3); final RenderBox extendedFABRenderBox = tester.renderObject(extendedFABMaterialButton); - expect(extendedFABRenderBox.size, const Size(111.0, 56.0)); + expect(extendedFABRenderBox.size, within(distance: 0.01, from: const Size(110.3, 56.0))); expect(getRawMaterialButtonWidget(extendedFABMaterialButton).fillColor, theme.colorScheme.primaryContainer); expect(getRawMaterialButtonWidget(extendedFABMaterialButton).shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0))); }); diff --git a/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart b/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart new file mode 100644 index 0000000000000..1d4e9a26234ac --- /dev/null +++ b/examples/api/test/material/floating_action_button/floating_action_button.2_test.dart @@ -0,0 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/floating_action_button/floating_action_button.2.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('FloatingActionButton variants', (WidgetTester tester) async { + await tester.pumpWidget( + const example.FloatingActionButtonExampleApp(), + ); + + FloatingActionButton getFAB(Finder finder) { + return tester.widget(finder); + } + + final ColorScheme colorScheme = ThemeData(useMaterial3: true).colorScheme; + + // Test the FAB with surface color mapping. + FloatingActionButton fab = getFAB(find.byType(FloatingActionButton).at(0)); + expect(fab.foregroundColor, colorScheme.primary); + expect(fab.backgroundColor, colorScheme.surface); + + // Test the FAB with secondary color mapping. + fab = getFAB(find.byType(FloatingActionButton).at(1)); + expect(fab.foregroundColor, colorScheme.onSecondaryContainer); + expect(fab.backgroundColor, colorScheme.secondaryContainer); + + // Test the FAB with tertiary color mapping. + fab = getFAB(find.byType(FloatingActionButton).at(2)); + expect(fab.foregroundColor, colorScheme.onTertiaryContainer); + expect(fab.backgroundColor, colorScheme.tertiaryContainer); + }); +} diff --git a/examples/api/test/material/input_chip/input_chip.1_test.dart b/examples/api/test/material/input_chip/input_chip.1_test.dart new file mode 100644 index 0000000000000..e55a483efd761 --- /dev/null +++ b/examples/api/test/material/input_chip/input_chip.1_test.dart @@ -0,0 +1,62 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/input_chip/input_chip.1.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final String replacementChar = String.fromCharCode( + example.ChipsInputEditingController.kObjectReplacementChar); + + testWidgets('User input generates InputChips', (WidgetTester tester) async { + await tester.pumpWidget( + const example.EditableChipFieldApp(), + ); + await tester.pumpAndSettle(); + + expect(find.byType(example.EditableChipFieldApp), findsOneWidget); + expect(find.byType(example.ChipsInput), findsOneWidget); + expect(find.byType(InputChip), findsOneWidget); + + example.ChipsInputState state = + tester.state(find.byType(example.ChipsInput)); + expect(state.controller.textWithoutReplacements.isEmpty, true); + + await tester.tap(find.byType(example.ChipsInput)); + await tester.pumpAndSettle(); + expect(tester.testTextInput.isVisible, true); + // Simulating text typing on the input field. + tester.testTextInput.enterText('${replacementChar}ham'); + await tester.pumpAndSettle(); + expect(find.byType(InputChip), findsOneWidget); + + state = tester.state(find.byType(example.ChipsInput)); + await tester.pumpAndSettle(); + expect(state.controller.textWithoutReplacements, 'ham'); + + // Add new InputChip by sending the "done" action. + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + expect(state.controller.textWithoutReplacements.isEmpty, true); + + expect(find.byType(InputChip), findsNWidgets(2)); + + // Simulate item deletion. + await tester.tap(find.descendant( + of: find.byType(InputChip), + matching: find.byType(InkWell).last, + )); + await tester.pumpAndSettle(); + expect(find.byType(InputChip), findsOneWidget); + + await tester.tap(find.descendant( + of: find.byType(InputChip), + matching: find.byType(InkWell).last, + )); + await tester.pumpAndSettle(); + expect(find.byType(InputChip), findsNothing); + }); +} diff --git a/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart b/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart index 278d19af9ca8a..5251505868429 100644 --- a/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart +++ b/examples/api/test/material/menu_anchor/checkbox_menu_button.0_test.dart @@ -13,15 +13,29 @@ void main() { ); await tester.tap(find.byType(TextButton)); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text('Show Message'), findsOneWidget); expect(find.text(example.MenuApp.kMessage), findsNothing); await tester.tap(find.text('Show Message')); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text('Show Message'), findsNothing); expect(find.text(example.MenuApp.kMessage), findsOneWidget); }); + + testWidgets('MenuAnchor is wrapped in a SafeArea', (WidgetTester tester) async { + const double safeAreaPadding = 100.0; + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.symmetric(vertical: safeAreaPadding), + ), + child: example.MenuApp(), + ), + ); + + expect(tester.getTopLeft(find.byType(MenuAnchor)), const Offset(0.0, safeAreaPadding)); + }); } diff --git a/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart b/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart index 0268dcab84ee0..5e69f5d8c1b4a 100644 --- a/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart +++ b/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart @@ -27,10 +27,11 @@ void main() { await tester.pump(); expect(find.text('About', findRichText: true), findsOneWidget); - expect( - tester.getRect(findMenu('About')), - equals(const Rect.fromLTRB(4.0, 48.0, 111.0, 208.0)), - ); + expect(tester.getRect(findMenu('About')).left, equals(4.0)); + expect(tester.getRect(findMenu('About')).top, equals(48.0)); + expect(tester.getRect(findMenu('About')).right, closeTo(98.5, 0.1)); + expect(tester.getRect(findMenu('About')).bottom, equals(208.0)); + expect(find.text('Save', findRichText: true), findsOneWidget); expect(find.text('Quit', findRichText: true), findsOneWidget); expect(find.text('Magnify', findRichText: true), findsNothing); @@ -51,4 +52,18 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Close'), findsNothing); }); + + testWidgets('MenuBar is wrapped in a SafeArea', (WidgetTester tester) async { + const double safeAreaPadding = 100.0; + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.symmetric(vertical: safeAreaPadding), + ), + child: example.MenuAcceleratorApp(), + ), + ); + + expect(tester.getTopLeft(find.byType(MenuBar)), const Offset(0.0, safeAreaPadding)); + }); } diff --git a/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart b/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart index 54016dcf25732..11a6becc68004 100644 --- a/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart +++ b/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart @@ -41,7 +41,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text(example.MenuApp.kMessage), findsOneWidget); expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget); @@ -104,4 +104,18 @@ void main() { expect(find.text('Last Selected: ${example.MenuEntry.colorBlue.label}'), findsOneWidget); }); + + testWidgets('MenuAnchor is wrapped in a SafeArea', (WidgetTester tester) async { + const double safeAreaPadding = 100.0; + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.symmetric(vertical: safeAreaPadding), + ), + child: example.MenuApp(), + ), + ); + + expect(tester.getTopLeft(find.byType(MenuAnchor)), const Offset(0.0, safeAreaPadding)); + }); } diff --git a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart index b9dc670d1c8fc..fdb3eeea4e4ee 100644 --- a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart +++ b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart @@ -20,14 +20,20 @@ void main() { await tester.pumpWidget(const example.ContextMenuApp()); await tester.tapAt(const Offset(100, 200), buttons: kSecondaryButton); - await tester.pump(); - expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(100.0, 200.0, 433.0, 360.0))); + await tester.pumpAndSettle(); + expect(tester.getRect(findMenu()).left, equals(100.0)); + expect(tester.getRect(findMenu()).top, equals(200.0)); + expect(tester.getRect(findMenu()).right, closeTo(389.8, 0.1)); + expect(tester.getRect(findMenu()).bottom, equals(360.0)); // Make sure tapping in a different place causes the menu to move. await tester.tapAt(const Offset(200, 100), buttons: kSecondaryButton); await tester.pump(); - expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(200.0, 100.0, 533.0, 260.0))); + expect(tester.getRect(findMenu()).left, equals(200.0)); + expect(tester.getRect(findMenu()).top, equals(100.0)); + expect(tester.getRect(findMenu()).right, closeTo(489.8, 0.1)); + expect(tester.getRect(findMenu()).bottom, equals(260.0)); expect(find.text(example.MenuEntry.about.label), findsOneWidget); expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget); @@ -46,7 +52,7 @@ void main() { expect(find.text('Background Color'), findsOneWidget); await tester.tap(find.text('Background Color')); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget); expect(find.text(example.MenuEntry.colorGreen.label), findsOneWidget); @@ -54,7 +60,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text(example.ContextMenuApp.kMessage), findsOneWidget); expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget); diff --git a/examples/api/test/material/menu_anchor/menu_bar.0_test.dart b/examples/api/test/material/menu_anchor/menu_bar.0_test.dart index b508ba444497f..1ba69519c1b07 100644 --- a/examples/api/test/material/menu_anchor/menu_bar.0_test.dart +++ b/examples/api/test/material/menu_anchor/menu_bar.0_test.dart @@ -20,7 +20,7 @@ void main() { final Finder menuButtonFinder = find.byType(SubmenuButton).first; await tester.tap(menuButtonFinder); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text('About'), findsOneWidget); expect(find.text('Show Message'), findsOneWidget); @@ -34,7 +34,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text('About'), findsOneWidget); expect(find.text('Show Message'), findsOneWidget); @@ -46,7 +46,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pump(); + await tester.pumpAndSettle(); expect(find.text(example.MenuBarApp.kMessage), findsOneWidget); expect(find.text('Last Selected: Show Message'), findsOneWidget); @@ -91,4 +91,18 @@ void main() { expect(find.text('Last Selected: Blue Background'), findsOneWidget); }); + + testWidgets('MenuBar is wrapped in a SafeArea', (WidgetTester tester) async { + const double safeAreaPadding = 100.0; + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.symmetric(vertical: safeAreaPadding), + ), + child: example.MenuBarApp(), + ), + ); + + expect(tester.getTopLeft(find.byType(MenuBar)), const Offset(0.0, safeAreaPadding)); + }); } diff --git a/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart b/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart index ee33556142f7a..bbc91423e8b5b 100644 --- a/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart +++ b/examples/api/test/material/menu_anchor/radio_menu_button.0_test.dart @@ -24,7 +24,7 @@ void main() { expect(tester.widget(find.byType(Container)).color, equals(Colors.red)); await tester.tap(find.text('Green Background')); - await tester.pump(); + await tester.pumpAndSettle(); expect(tester.widget(find.byType(Container)).color, equals(Colors.green)); }); @@ -74,4 +74,18 @@ void main() { expect(tester.widget>(find.descendant(of: find.byType(RadioMenuButton).at(2), matching: find.byType(Radio))).groupValue, equals(Colors.blue)); expect(tester.widget(find.byType(Container)).color, equals(Colors.blue)); }); + + testWidgets('MenuAnchor is wrapped in a SafeArea', (WidgetTester tester) async { + const double safeAreaPadding = 100.0; + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.symmetric(vertical: safeAreaPadding), + ), + child: example.MenuApp(), + ), + ); + + expect(tester.getTopLeft(find.byType(MenuAnchor)), const Offset(0.0, safeAreaPadding)); + }); } diff --git a/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart b/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart index f18598a397cf0..d89d8db5a20b3 100644 --- a/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart +++ b/examples/api/test/material/navigation_bar/navigation_bar.0_test.dart @@ -17,20 +17,36 @@ void main() { /// NavigationDestinations must be rendered expect(find.text('Home'), findsOneWidget); - expect(find.text('Business'), findsOneWidget); - expect(find.text('School'), findsOneWidget); + expect(find.text('Notifications'), findsOneWidget); + expect(find.text('Messages'), findsOneWidget); - /// initial index must be zero + /// Test notification badge. + final Badge notificationBadge = tester.firstWidget(find.ancestor( + of: find.byIcon(Icons.notifications_sharp), + matching: find.byType(Badge), + )); + expect(notificationBadge.label, null); + + /// Test messages badge. + final Badge messagesBadge = tester.firstWidget(find.ancestor( + of: find.byIcon(Icons.messenger_sharp), + matching: find.byType(Badge), + )); + expect(messagesBadge.label, isNotNull); + + /// Initial index must be zero expect(navigationBarWidget.selectedIndex, 0); + expect(find.text('Home page'), findsOneWidget); - /// switch to second tab - await tester.tap(find.text('Business')); + /// Switch to second tab + await tester.tap(find.text('Notifications')); await tester.pumpAndSettle(); - expect(find.text('Page 2'), findsOneWidget); + expect(find.text('This is a notification'), findsNWidgets(2)); - /// switch to third tab - await tester.tap(find.text('School')); + /// Switch to third tab + await tester.tap(find.text('Messages')); await tester.pumpAndSettle(); - expect(find.text('Page 3'), findsOneWidget); + expect(find.text('Hi!'), findsOneWidget); + expect(find.text('Hello'), findsOneWidget); }); } diff --git a/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart b/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart new file mode 100644 index 0000000000000..a7e9916d827fc --- /dev/null +++ b/examples/api/test/material/navigation_drawer/navigation_drawer.0_test.dart @@ -0,0 +1,41 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/navigation_drawer/navigation_drawer.0.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Navigation bar updates destination on tap', + (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigationDrawerApp(), + ); + + await tester.tap(find.text('Open Drawer')); + await tester.pumpAndSettle(); + + final NavigationDrawer navigationDrawerWidget = tester.firstWidget(find.byType(NavigationDrawer)); + + /// NavigationDestinations must be rendered + expect(find.text('Messages'), findsNWidgets(2)); + expect(find.text('Profile'), findsNWidgets(2)); + expect(find.text('Settings'), findsNWidgets(2)); + + /// Initial index must be zero + expect(navigationDrawerWidget.selectedIndex, 0); + expect(find.text('Page Index = 0'), findsOneWidget); + + /// Switch to second tab + await tester.tap(find.ancestor(of: find.text('Profile'), matching: find.byType(InkWell))); + await tester.pumpAndSettle(); + expect(find.text('Page Index = 1'), findsOneWidget); + + /// Switch to fourth tab + await tester.tap(find.ancestor(of: find.text('Settings'), matching: find.byType(InkWell))); + await tester.pumpAndSettle(); + expect(find.text('Page Index = 2'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart b/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart index 1c3eee14820e0..d3526d4adc430 100644 --- a/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart +++ b/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart @@ -94,4 +94,24 @@ void main() { expect(find.byType(FloatingActionButton), findsOneWidget); expect(find.byType(IconButton), findsOneWidget); }); + + testWidgets('Destinations have badge', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigationRailExampleApp(), + ); + + // Test badge wthout label. + final Badge notificationBadge = tester.firstWidget(find.ancestor( + of: find.byIcon(Icons.bookmark_border), + matching: find.byType(Badge), + )); + expect(notificationBadge.label, null); + + // Test badge with label. + final Badge messagesBadge = tester.firstWidget(find.ancestor( + of: find.byIcon(Icons.star_border), + matching: find.byType(Badge), + )); + expect(messagesBadge.label, isNotNull); + }); } diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart b/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart new file mode 100644 index 0000000000000..31084fc2634e8 --- /dev/null +++ b/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/material/paginated_data_table/paginated_data_table.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('PaginatedDataTable 0', (WidgetTester tester) async { + await tester.pumpWidget(const example.DataTableExampleApp()); + expect(find.text('Associate Professor'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart b/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart new file mode 100644 index 0000000000000..7a3bb9168586a --- /dev/null +++ b/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart @@ -0,0 +1,23 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/paginated_data_table/paginated_data_table.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('PaginatedDataTable 1', (WidgetTester tester) async { + await tester.pumpWidget(const example.DataTableExampleApp()); + expect(find.text('Strange New Worlds'), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_upward).at(1)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsNothing); + await tester.tap(find.byIcon(Icons.chevron_right)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_upward).at(1)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsNothing); + }); +} diff --git a/examples/api/test/material/scrollbar/scrollbar.0_test.dart b/examples/api/test/material/scrollbar/scrollbar.0_test.dart new file mode 100644 index 0000000000000..330f4f9408fcf --- /dev/null +++ b/examples/api/test/material/scrollbar/scrollbar.0_test.dart @@ -0,0 +1,21 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/scrollbar/scrollbar.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Scrollbar.0 works well on all platforms', (WidgetTester tester) async { + + await tester.pumpWidget( + const example.ScrollbarExampleApp(), + ); + + final Finder buttonFinder = find.byType(Scrollbar); + await tester.drag(buttonFinder.last, const Offset(0, 100.0)); + + expect(tester.takeException(), isNull); + }, variant: TargetPlatformVariant.all()); +} diff --git a/examples/api/test/material/theme_data/theme_data.0_test.dart b/examples/api/test/material/theme_data/theme_data.0_test.dart new file mode 100644 index 0000000000000..bb1de2263877d --- /dev/null +++ b/examples/api/test/material/theme_data/theme_data.0_test.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/theme_data/theme_data.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + + testWidgets('ThemeData basics', (WidgetTester tester) async { + await tester.pumpWidget(const example.ThemeDataExampleApp()); + + final ColorScheme colorScheme = ColorScheme.fromSeed( + seedColor: Colors.indigo, + ); + + final Material fabMaterial = tester.widget( + find.descendant(of: find.byType(FloatingActionButton), matching: find.byType(Material)), + ); + expect(fabMaterial.color, colorScheme.tertiary); + + final RichText iconRichText = tester.widget( + find.descendant(of: find.byIcon(Icons.add), matching: find.byType(RichText)), + ); + expect(iconRichText.text.style!.color, colorScheme.onTertiary); + + expect(find.text('8 Points'), isNotNull); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + expect(find.text('9 Points'), isNotNull); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + expect(find.text('10 Points'), isNotNull); + }); +} diff --git a/examples/api/test/painting/image_provider/image_provider.0_test.dart b/examples/api/test/painting/image_provider/image_provider.0_test.dart new file mode 100644 index 0000000000000..0b1ce43c0aee6 --- /dev/null +++ b/examples/api/test/painting/image_provider/image_provider.0_test.dart @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_api_samples/painting/image_provider/image_provider.0.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('$CustomNetworkImage', (WidgetTester tester) async { + const String expectedUrl = 'https://flutter.github.io/assets-for-api-docs/assets/widgets/flamingos.jpg?dpr=3.0&locale=en-US&platform=android&width=800.0&height=600.0&bidi=ltr'; + final List log = []; + final DebugPrintCallback originalDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { log.add('$message'); }; + await tester.pumpWidget(const ExampleApp()); + expect(tester.takeException().toString(), 'Exception: Invalid image data'); + expect(log, ['Fetching "$expectedUrl"...']); + debugPrint = originalDebugPrint; + }); +} diff --git a/examples/api/test/rendering/box/parent_data.0_test.dart b/examples/api/test/rendering/box/parent_data.0_test.dart new file mode 100644 index 0000000000000..91c4a1beb4274 --- /dev/null +++ b/examples/api/test/rendering/box/parent_data.0_test.dart @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/rendering/box/parent_data.0.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('parent data example', (WidgetTester tester) async { + await tester.pumpWidget(const SampleApp()); + expect(tester.getTopLeft(find.byType(Headline).at(2)), const Offset(30.0, 728.0)); + await tester.tap(find.byIcon(Icons.density_small)); + await tester.pump(); + expect(tester.getTopLeft(find.byType(Headline).at(2)), const Offset(30.0, 682.0)); + }); +} diff --git a/examples/api/test/widgets/form/form.1_test.dart b/examples/api/test/widgets/form/form.1_test.dart new file mode 100644 index 0000000000000..f9ccd71d521e8 --- /dev/null +++ b/examples/api/test/widgets/form/form.1_test.dart @@ -0,0 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/form/form.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Can go back when form is clean', (WidgetTester tester) async { + await tester.pumpWidget( + const example.FormApp(), + ); + + expect(find.text('Are you sure?'), findsNothing); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure?'), findsNothing); + }); + + testWidgets('Cannot go back when form is dirty', (WidgetTester tester) async { + await tester.pumpWidget( + const example.FormApp(), + ); + + expect(find.text('Are you sure?'), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'some new text'); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure?'), findsOneWidget); + }); +} diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart new file mode 100644 index 0000000000000..88f29223f60a4 --- /dev/null +++ b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart @@ -0,0 +1,48 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +import '../navigator_utils.dart'; + +void main() { + testWidgets('Can go back with system back gesture', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Nested Navigators Example'), findsOneWidget); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsNothing); + + await tester.tap(find.text('Nested Navigator route')); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsOneWidget); + expect(find.text('Nested Navigators Page Two'), findsNothing); + + await tester.tap(find.text('Go to another route in this nested Navigator')); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsOneWidget); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsOneWidget); + expect(find.text('Nested Navigators Page Two'), findsNothing); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsOneWidget); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsNothing); + }); +} diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart new file mode 100644 index 0000000000000..a6ea0ac82806f --- /dev/null +++ b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart @@ -0,0 +1,38 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +import '../navigator_utils.dart'; + +void main() { + testWidgets("System back gesture operates on current tab's nested Navigator", (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + + // Go to the next route in this tab. + await tester.tap(find.text('Go to another route in this nested Navigator')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + + // Go to another tab. + await tester.tap(find.text('Go to One')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget); + + // Return to the home tab. The navigation state is preserved. + await tester.tap(find.text('Go to Home')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + + // A back pops the navigation stack of the current tab's nested Navigator. + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + }); +} diff --git a/examples/api/test/widgets/navigator_utils.dart b/examples/api/test/widgets/navigator_utils.dart new file mode 100644 index 0000000000000..46f1f9b1ac469 --- /dev/null +++ b/examples/api/test/widgets/navigator_utils.dart @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Simulates a system back, like a back gesture on Android. +/// +/// Sends the same platform channel message that the engine sends when it +/// receives a system back. +Future simulateSystemBack() { + return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/navigation', + const JSONMessageCodec().encodeMessage({ + 'method': 'popRoute', + }), + (ByteData? _) {}, + ); +} diff --git a/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart b/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart new file mode 100644 index 0000000000000..ac334fc3222f2 --- /dev/null +++ b/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart @@ -0,0 +1,66 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/widgets/pop_scope/pop_scope.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +import '../navigator_utils.dart'; + +void main() { + testWidgets('Can choose to stay on page', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Page One'), findsOneWidget); + expect(find.text('Page Two'), findsNothing); + expect(find.text('Are you sure?'), findsNothing); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + expect(find.text('Are you sure?'), findsNothing); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + expect(find.text('Are you sure?'), findsOneWidget); + + await tester.tap(find.text('Nevermind')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + expect(find.text('Are you sure?'), findsNothing); + }); + + testWidgets('Can choose to go back', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Page One'), findsOneWidget); + expect(find.text('Page Two'), findsNothing); + expect(find.text('Are you sure?'), findsNothing); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + expect(find.text('Are you sure?'), findsNothing); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + expect(find.text('Are you sure?'), findsOneWidget); + + await tester.tap(find.text('Leave')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsOneWidget); + expect(find.text('Page Two'), findsNothing); + expect(find.text('Are you sure?'), findsNothing); + }); +} diff --git a/examples/api/test/widgets/scroll_view/grid_view.0_test.dart b/examples/api/test/widgets/scroll_view/grid_view.0_test.dart new file mode 100644 index 0000000000000..9368a091d3b38 --- /dev/null +++ b/examples/api/test/widgets/scroll_view/grid_view.0_test.dart @@ -0,0 +1,32 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; +import 'package:flutter_api_samples/widgets/scroll_view/grid_view.0.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('$CustomGridLayout', (WidgetTester tester) async { + const CustomGridLayout layout = CustomGridLayout( + crossAxisCount: 2, + fullRowPeriod: 3, + dimension: 100, + ); + final List scrollOffsets = List.generate(10, (int i) => layout.computeMaxScrollOffset(i)); + expect(scrollOffsets, [0.0, 0.0, 100.0, 100.0, 200.0, 300.0, 300.0, 400.0, 400.0, 500.0]); + final List minOffsets = List.generate(10, (int i) => layout.getMinChildIndexForScrollOffset(i * 80.0)); + expect(minOffsets, [0, 0, 2, 4, 5, 7, 7, 9, 10, 12]); + final List maxOffsets = List.generate(10, (int i) => layout.getMaxChildIndexForScrollOffset(i * 80.0)); + expect(maxOffsets, [1, 1, 3, 4, 6, 8, 8, 9, 11, 13]); + final List offsets = List.generate(20, (int i) => layout.getGeometryForChildIndex(i)); + offsets.reduce((SliverGridGeometry a, SliverGridGeometry b) { + if (a.scrollOffset == b.scrollOffset) { + expect(a.crossAxisOffset, lessThan(b.crossAxisOffset)); + } else { + expect(a.scrollOffset, lessThan(b.scrollOffset)); + } + return b; + }); + }); +} diff --git a/examples/api/test/widgets/transitions/listenable_builder.3_test.dart b/examples/api/test/widgets/transitions/listenable_builder.3_test.dart new file mode 100644 index 0000000000000..a618a4999b207 --- /dev/null +++ b/examples/api/test/widgets/transitions/listenable_builder.3_test.dart @@ -0,0 +1,34 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/transitions/listenable_builder.3.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Tapping FAB adds to values', (WidgetTester tester) async { + await tester.pumpWidget(const example.ListenableBuilderExample()); + + final Finder listContent = find.byWidgetPredicate((Widget widget) => widget is example.ListBody); + + expect(find.text('Current values:'), findsOneWidget); + expect(find.byIcon(Icons.add), findsOneWidget); + expect( + (tester.widget(listContent) as example.ListBody).listNotifier.values.isEmpty, + isTrue, + ); + + await tester.tap(find.byType(FloatingActionButton).first); + await tester.pumpAndSettle(); + expect( + (tester.widget(listContent) as example.ListBody).listNotifier.values.isEmpty, + isFalse, + ); + expect( + (tester.widget(listContent) as example.ListBody).listNotifier.values, + [1464685455], + ); + expect(find.text('1464685455'), findsOneWidget); + }); +} diff --git a/examples/api/test/widgets/transitions/matrix_transition.0_test.dart b/examples/api/test/widgets/transitions/matrix_transition.0_test.dart new file mode 100644 index 0000000000000..afa0ddabf6ba3 --- /dev/null +++ b/examples/api/test/widgets/transitions/matrix_transition.0_test.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/transitions/matrix_transition.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Shows Flutter logo inside a MatrixTransition', (WidgetTester tester) async { + await tester.pumpWidget( + const example.MatrixTransitionExampleApp(), + ); + + final Finder transformFinder = find.ancestor( + of: find.byType(FlutterLogo), + matching: find.byType(MatrixTransition), + ); + expect(transformFinder, findsOneWidget); + }); + + testWidgets('MatrixTransition animates', (WidgetTester tester) async { + await tester.pumpWidget( + const example.MatrixTransitionExampleApp(), + ); + + final Finder transformFinder = find.ancestor( + of: find.byType(FlutterLogo), + matching: find.byType(Transform), + ); + + Transform transformBox = tester.widget(transformFinder); + Matrix4 actualTransform = transformBox.transform; + + // Check initial transform. + expect(actualTransform, Matrix4.fromList([ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.004, 1.0, + ])..transpose()); + + // Animate half way. + await tester.pump(const Duration(seconds: 1)); + transformBox = tester.widget(transformFinder); + actualTransform = transformBox.transform; + + // The transform should be updated. + expect(actualTransform, isNot(Matrix4.fromList([ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.004, 1.0, + ])..transpose())); + }); +} diff --git a/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart b/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart deleted file mode 100644 index ad05943108708..0000000000000 --- a/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_api_samples/widgets/will_pop_scope/will_pop_scope.0.dart' as example; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('pressing shouldPop button changes shouldPop', (WidgetTester tester) async { - await tester.pumpWidget( - const example.WillPopScopeExampleApp(), - ); - - final Finder buttonFinder = find.text('shouldPop: true'); - expect(buttonFinder, findsOneWidget); - await tester.tap(buttonFinder); - await tester.pump(); - expect(find.text('shouldPop: false'), findsOneWidget); - }); - testWidgets('pressing Push button pushes route', (WidgetTester tester) async { - await tester.pumpWidget( - const example.WillPopScopeExampleApp(), - ); - - final Finder buttonFinder = find.text('Push'); - expect(buttonFinder, findsOneWidget); - expect(find.byType(example.WillPopScopeExample), findsOneWidget); - await tester.tap(buttonFinder); - await tester.pumpAndSettle(); - expect(find.byType(example.WillPopScopeExample, skipOffstage: false), findsNWidgets(2)); - }); -} diff --git a/examples/api/windows/flutter/CMakeLists.txt b/examples/api/windows/flutter/CMakeLists.txt index b2e4bd8d658b2..903f4899d6fce 100644 --- a/examples/api/windows/flutter/CMakeLists.txt +++ b/examples/api/windows/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/flutter_view/android/gradle.properties b/examples/flutter_view/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/flutter_view/android/gradle.properties +++ b/examples/flutter_view/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/flutter_view/pubspec.yaml b/examples/flutter_view/pubspec.yaml index e8d77492ba79b..407113ff8d39e 100644 --- a/examples/flutter_view/pubspec.yaml +++ b/examples/flutter_view/pubspec.yaml @@ -2,22 +2,22 @@ name: flutter_view description: A new flutter project. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true assets: - assets/flutter-mark-square-64.png -# PUBSPEC CHECKSUM: a9c0 +# PUBSPEC CHECKSUM: 081a diff --git a/examples/flutter_view/windows/flutter/CMakeLists.txt b/examples/flutter_view/windows/flutter/CMakeLists.txt index 10873dd1af99c..c8f7abf1ebea9 100644 --- a/examples/flutter_view/windows/flutter/CMakeLists.txt +++ b/examples/flutter_view/windows/flutter/CMakeLists.txt @@ -14,6 +14,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -96,7 +101,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/hello_world/android/build.gradle b/examples/hello_world/android/build.gradle index 2de20623576a8..a38857e025b8d 100644 --- a/examples/hello_world/android/build.gradle +++ b/examples/hello_world/android/build.gradle @@ -7,14 +7,13 @@ // See dev/tools/bin/generate_gradle_lockfiles.dart. buildscript { - ext.kotlin_version = '1.5.31' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/examples/hello_world/android/buildscript-gradle.lockfile b/examples/hello_world/android/buildscript-gradle.lockfile index efe13277310ee..14350adbcb457 100644 --- a/examples/hello_world/android/buildscript-gradle.lockfile +++ b/examples/hello_world/android/buildscript-gradle.lockfile @@ -1,163 +1,29 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -androidx.databinding:databinding-common:7.2.0=classpath -androidx.databinding:databinding-compiler-common:7.2.0=classpath -com.android.databinding:baseLibrary:7.2.0=classpath -com.android.tools.analytics-library:crash:30.2.0=classpath -com.android.tools.analytics-library:protos:30.2.0=classpath -com.android.tools.analytics-library:shared:30.2.0=classpath -com.android.tools.analytics-library:tracker:30.2.0=classpath -com.android.tools.build.jetifier:jetifier-core:1.0.0-beta09=classpath -com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta09=classpath -com.android.tools.build:aapt2-proto:7.2.0-7984345=classpath -com.android.tools.build:aaptcompiler:7.2.0=classpath -com.android.tools.build:apksig:7.2.0=classpath -com.android.tools.build:apkzlib:7.2.0=classpath -com.android.tools.build:builder-model:7.2.0=classpath -com.android.tools.build:builder-test-api:7.2.0=classpath -com.android.tools.build:builder:7.2.0=classpath -com.android.tools.build:bundletool:1.8.2=classpath -com.android.tools.build:gradle-api:7.2.0=classpath -com.android.tools.build:gradle:7.2.0=classpath -com.android.tools.build:manifest-merger:30.2.0=classpath -com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api=classpath -com.android.tools.ddms:ddmlib:30.2.0=classpath -com.android.tools.layoutlib:layoutlib-api:30.2.0=classpath -com.android.tools.lint:lint-model:30.2.0=classpath -com.android.tools.lint:lint-typedef-remover:30.2.0=classpath -com.android.tools.utp:android-device-provider-ddmlib-proto:30.2.0=classpath -com.android.tools.utp:android-device-provider-gradle-proto:30.2.0=classpath -com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.2.0=classpath -com.android.tools.utp:android-test-plugin-host-coverage-proto:30.2.0=classpath -com.android.tools.utp:android-test-plugin-host-retention-proto:30.2.0=classpath -com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.2.0=classpath -com.android.tools:annotations:30.2.0=classpath -com.android.tools:common:30.2.0=classpath -com.android.tools:dvlib:30.2.0=classpath -com.android.tools:repository:30.2.0=classpath -com.android.tools:sdk-common:30.2.0=classpath -com.android.tools:sdklib:30.2.0=classpath -com.android:signflinger:7.2.0=classpath -com.android:zipflinger:7.2.0=classpath -com.fasterxml.jackson.core:jackson-annotations:2.11.1=classpath -com.fasterxml.jackson.core:jackson-core:2.11.1=classpath -com.fasterxml.jackson.core:jackson-databind:2.11.1=classpath -com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1=classpath -com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.1=classpath -com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1=classpath -com.fasterxml.woodstox:woodstox-core:6.2.1=classpath -com.github.gundy:semver4j:0.16.4=classpath -com.google.android:annotations:4.1.1.4=classpath -com.google.api.grpc:proto-google-common-protos:1.12.0=classpath -com.google.auto.value:auto-value-annotations:1.6.2=classpath -com.google.code.findbugs:jsr305:3.0.2=classpath -com.google.code.gson:gson:2.8.6=classpath -com.google.crypto.tink:tink:1.3.0-rc2=classpath -com.google.dagger:dagger:2.28.3=classpath -com.google.errorprone:error_prone_annotations:2.3.4=classpath -com.google.flatbuffers:flatbuffers-java:1.12.0=classpath -com.google.guava:failureaccess:1.0.1=classpath -com.google.guava:guava:30.1-jre=classpath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath -com.google.j2objc:j2objc-annotations:1.3=classpath -com.google.jimfs:jimfs:1.1=classpath -com.google.protobuf:protobuf-java-util:3.10.0=classpath -com.google.protobuf:protobuf-java:3.10.0=classpath -com.google.testing.platform:core-proto:0.0.8-alpha07=classpath -com.googlecode.json-simple:json-simple:1.1=classpath -com.googlecode.juniversalchardet:juniversalchardet:1.0.3=classpath -com.squareup:javapoet:1.10.0=classpath -com.squareup:javawriter:2.5.0=classpath -com.sun.activation:javax.activation:1.2.0=classpath -com.sun.istack:istack-commons-runtime:3.0.8=classpath -com.sun.xml.fastinfoset:FastInfoset:1.2.16=classpath -commons-codec:commons-codec:1.11=classpath -commons-io:commons-io:2.4=classpath -commons-logging:commons-logging:1.2=classpath -de.undercouch:gradle-download-task:4.1.1=classpath -io.grpc:grpc-api:1.21.1=classpath -io.grpc:grpc-context:1.21.1=classpath -io.grpc:grpc-core:1.21.1=classpath -io.grpc:grpc-netty:1.21.1=classpath -io.grpc:grpc-protobuf-lite:1.21.1=classpath -io.grpc:grpc-protobuf:1.21.1=classpath -io.grpc:grpc-stub:1.21.1=classpath -io.netty:netty-buffer:4.1.34.Final=classpath -io.netty:netty-codec-http2:4.1.34.Final=classpath -io.netty:netty-codec-http:4.1.34.Final=classpath -io.netty:netty-codec-socks:4.1.34.Final=classpath -io.netty:netty-codec:4.1.34.Final=classpath -io.netty:netty-common:4.1.34.Final=classpath -io.netty:netty-handler-proxy:4.1.34.Final=classpath -io.netty:netty-handler:4.1.34.Final=classpath -io.netty:netty-resolver:4.1.34.Final=classpath -io.netty:netty-transport:4.1.34.Final=classpath -io.opencensus:opencensus-api:0.21.0=classpath -io.opencensus:opencensus-contrib-grpc-metrics:0.21.0=classpath -it.unimi.dsi:fastutil:8.4.0=classpath -jakarta.activation:jakarta.activation-api:1.2.1=classpath -jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=classpath -javax.inject:javax.inject:1=classpath -net.java.dev.jna:jna-platform:5.6.0=classpath -net.java.dev.jna:jna:5.6.0=classpath -net.sf.jopt-simple:jopt-simple:4.9=classpath -net.sf.kxml:kxml2:2.3.0=classpath -org.apache.commons:commons-compress:1.20=classpath -org.apache.httpcomponents:httpclient:4.5.9=classpath -org.apache.httpcomponents:httpcore:4.4.11=classpath -org.apache.httpcomponents:httpmime:4.5.6=classpath -org.bitbucket.b_c:jose4j:0.7.0=classpath -org.bouncycastle:bcpkix-jdk15on:1.56=classpath -org.bouncycastle:bcprov-jdk15on:1.56=classpath -org.checkerframework:checker-qual:3.5.0=classpath -org.codehaus.mojo:animal-sniffer-annotations:1.17=classpath -org.codehaus.woodstox:stax2-api:4.2.1=classpath -org.glassfish.jaxb:jaxb-runtime:2.3.2=classpath -org.glassfish.jaxb:txw2:2.3.2=classpath -org.jdom:jdom2:2.0.6=classpath -org.jetbrains.dokka:dokka-core:1.4.32=classpath -org.jetbrains.intellij.deps:trove4j:1.0.20181211=classpath -org.jetbrains.kotlin:kotlin-android-extensions:1.5.31=classpath -org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.31=classpath -org.jetbrains.kotlin:kotlin-build-common:1.5.31=classpath -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=classpath -org.jetbrains.kotlin:kotlin-compiler-runner:1.5.31=classpath -org.jetbrains.kotlin:kotlin-daemon-client:1.5.31=classpath -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.5.31=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31=classpath -org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.5.31=classpath -org.jetbrains.kotlin:kotlin-native-utils:1.5.31=classpath -org.jetbrains.kotlin:kotlin-project-model:1.5.31=classpath -org.jetbrains.kotlin:kotlin-reflect:1.5.31=classpath -org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=classpath -org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=classpath -org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=classpath -org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath -org.jetbrains.kotlin:kotlin-stdlib:1.5.31=classpath -org.jetbrains.kotlin:kotlin-tooling-metadata:1.5.31=classpath -org.jetbrains.kotlin:kotlin-util-io:1.5.31=classpath -org.jetbrains.kotlin:kotlin-util-klib:1.5.31=classpath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=classpath +org.jetbrains.kotlin:kotlin-android-extensions:1.9.0=classpath +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.0=classpath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.0=classpath +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.0=classpath +org.jetbrains.kotlin:kotlin-daemon-client:1.9.0=classpath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0=classpath +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.0=classpath +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.0=classpath +org.jetbrains.kotlin:kotlin-native-utils:1.9.0=classpath +org.jetbrains.kotlin:kotlin-project-model:1.9.0=classpath +org.jetbrains.kotlin:kotlin-scripting-common:1.9.0=classpath +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.0=classpath +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.0=classpath +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.0=classpath +org.jetbrains.kotlin:kotlin-tooling-core:1.9.0=classpath +org.jetbrains.kotlin:kotlin-util-io:1.9.0=classpath +org.jetbrains.kotlin:kotlin-util-klib:1.9.0=classpath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=classpath -org.jetbrains:annotations:13.0=classpath -org.jetbrains:markdown-jvm:0.2.1=classpath -org.jetbrains:markdown:0.2.1=classpath -org.json:json:20180813=classpath -org.jsoup:jsoup:1.13.1=classpath -org.jvnet.staxex:stax-ex:1.8.1=classpath -org.ow2.asm:asm-analysis:9.1=classpath -org.ow2.asm:asm-commons:9.1=classpath -org.ow2.asm:asm-tree:9.1=classpath -org.ow2.asm:asm-util:9.1=classpath -org.ow2.asm:asm:9.1=classpath -org.slf4j:slf4j-api:1.7.30=classpath -org.tensorflow:tensorflow-lite-metadata:0.1.0-rc2=classpath -xerces:xercesImpl:2.12.0=classpath -xml-apis:xml-apis:1.4.01=classpath empty= diff --git a/examples/hello_world/android/gradle.properties b/examples/hello_world/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/hello_world/android/gradle.properties +++ b/examples/hello_world/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/hello_world/android/gradle/wrapper/gradle-wrapper.properties b/examples/hello_world/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6f35..ec915a81eb454 100644 --- a/examples/hello_world/android/gradle/wrapper/gradle-wrapper.properties +++ b/examples/hello_world/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip diff --git a/examples/hello_world/android/project-app.lockfile b/examples/hello_world/android/project-app.lockfile index e160a81ffd42a..0d87887fa38ec 100644 --- a/examples/hello_world/android/project-app.lockfile +++ b/examples/hello_world/android/project-app.lockfile @@ -87,10 +87,10 @@ org.codehaus.groovy:groovy-all:2.4.15=lintClassPath org.codehaus.mojo:animal-sniffer-annotations:1.18=lintClassPath org.glassfish.jaxb:jaxb-runtime:2.3.1=lintClassPath org.glassfish.jaxb:txw2:2.3.1=lintClassPath -org.jacoco:org.jacoco.agent:0.8.7=androidJacocoAnt -org.jacoco:org.jacoco.ant:0.8.7=androidJacocoAnt -org.jacoco:org.jacoco.core:0.8.7=androidJacocoAnt -org.jacoco:org.jacoco.report:0.8.7=androidJacocoAnt +org.jacoco:org.jacoco.agent:0.8.8=androidJacocoAnt +org.jacoco:org.jacoco.ant:0.8.8=androidJacocoAnt +org.jacoco:org.jacoco.core:0.8.8=androidJacocoAnt +org.jacoco:org.jacoco.report:0.8.8=androidJacocoAnt org.jetbrains.kotlin:kotlin-reflect:1.3.72=lintClassPath org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72=lintClassPath org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -107,12 +107,12 @@ org.jetbrains.trove4j:trove4j:20160824=lintClassPath org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jvnet.staxex:stax-ex:1.8=lintClassPath org.ow2.asm:asm-analysis:7.0=lintClassPath -org.ow2.asm:asm-analysis:9.1=androidJacocoAnt +org.ow2.asm:asm-analysis:9.2=androidJacocoAnt org.ow2.asm:asm-commons:7.0=lintClassPath -org.ow2.asm:asm-commons:9.1=androidJacocoAnt +org.ow2.asm:asm-commons:9.2=androidJacocoAnt org.ow2.asm:asm-tree:7.0=lintClassPath -org.ow2.asm:asm-tree:9.1=androidJacocoAnt +org.ow2.asm:asm-tree:9.2=androidJacocoAnt org.ow2.asm:asm-util:7.0=lintClassPath org.ow2.asm:asm:7.0=lintClassPath -org.ow2.asm:asm:9.1=androidJacocoAnt +org.ow2.asm:asm:9.2=androidJacocoAnt empty=androidApis,androidJdkImage,androidTestUtil,compile,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestRuntimeClasspath,debugAnnotationProcessorClasspath,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugWearBundling,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileReverseMetadataValues,profileUnitTestAnnotationProcessorClasspath,profileWearBundling,releaseAnnotationProcessorClasspath,releaseReverseMetadataValues,releaseUnitTestAnnotationProcessorClasspath,releaseWearBundling,testCompile diff --git a/examples/hello_world/android/settings.gradle b/examples/hello_world/android/settings.gradle index 4b92919bbe50f..47426ccdb58f8 100644 --- a/examples/hello_world/android/settings.gradle +++ b/examples/hello_world/android/settings.gradle @@ -19,6 +19,12 @@ pluginManagement { // Flutter Gradle Plugin ships together with the Flutter SDK includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + plugins { // Flutter Gradle Plugin's ID is defined in /packages/flutter_tools/gradle/build.gradle.kts. // We set `apply false`, because we don't want to apply the plugin to @@ -27,6 +33,9 @@ pluginManagement { } } -include ':app' +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.4.2" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ':app' diff --git a/examples/hello_world/pubspec.yaml b/examples/hello_world/pubspec.yaml index 637943e4bf191..2befaff6d42e9 100644 --- a/examples/hello_world/pubspec.yaml +++ b/examples/hello_world/pubspec.yaml @@ -1,28 +1,28 @@ name: hello_world environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_driver: sdk: flutter flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -53,19 +53,19 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/examples/hello_world/windows/flutter/CMakeLists.txt b/examples/hello_world/windows/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/examples/hello_world/windows/flutter/CMakeLists.txt +++ b/examples/hello_world/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/image_list/android/gradle.properties b/examples/image_list/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/image_list/android/gradle.properties +++ b/examples/image_list/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/image_list/pubspec.yaml b/examples/image_list/pubspec.yaml index ab1be6fd8ec80..05e2efeec22ae 100644 --- a/examples/image_list/pubspec.yaml +++ b/examples/image_list/pubspec.yaml @@ -4,7 +4,7 @@ description: Simple Flutter project used for benchmarking image loading over net version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -12,14 +12,14 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -37,11 +37,11 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -54,4 +54,4 @@ flutter: assets: - images/coast.jpg -# PUBSPEC CHECKSUM: f5e9 +# PUBSPEC CHECKSUM: 7b47 diff --git a/examples/layers/android/gradle.properties b/examples/layers/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/layers/android/gradle.properties +++ b/examples/layers/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/layers/pubspec.yaml b/examples/layers/pubspec.yaml index 1e09ede979b4b..f9e606d8df83e 100644 --- a/examples/layers/pubspec.yaml +++ b/examples/layers/pubspec.yaml @@ -1,18 +1,18 @@ name: flutter_examples_layers environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -25,15 +25,15 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: assets: - services/data.json uses-material-design: true -# PUBSPEC CHECKSUM: b642 +# PUBSPEC CHECKSUM: dc9e diff --git a/examples/layers/rendering/custom_coordinate_systems.dart b/examples/layers/rendering/custom_coordinate_systems.dart index 28aa4137892fa..d7fd16186e5d0 100644 --- a/examples/layers/rendering/custom_coordinate_systems.dart +++ b/examples/layers/rendering/custom_coordinate_systems.dart @@ -6,6 +6,7 @@ // system. Most of the guts of this examples are in src/sector_layout.dart. import 'package:flutter/rendering.dart'; +import 'src/binding.dart'; import 'src/sector_layout.dart'; RenderBox buildSectorExample() { @@ -21,5 +22,5 @@ RenderBox buildSectorExample() { } void main() { - RenderingFlutterBinding(root: buildSectorExample()).scheduleFrame(); + ViewRenderingFlutterBinding(root: buildSectorExample()).scheduleFrame(); } diff --git a/examples/layers/rendering/flex_layout.dart b/examples/layers/rendering/flex_layout.dart index 43c29509fc957..845a232fa1819 100644 --- a/examples/layers/rendering/flex_layout.dart +++ b/examples/layers/rendering/flex_layout.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; +import 'src/binding.dart'; import 'src/solid_color_box.dart'; void main() { @@ -86,5 +87,5 @@ void main() { child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)), ); - RenderingFlutterBinding(root: root).scheduleFrame(); + ViewRenderingFlutterBinding(root: root).scheduleFrame(); } diff --git a/examples/layers/rendering/hello_world.dart b/examples/layers/rendering/hello_world.dart index 03b4801e28252..cb49f94d373b2 100644 --- a/examples/layers/rendering/hello_world.dart +++ b/examples/layers/rendering/hello_world.dart @@ -7,9 +7,11 @@ import 'package:flutter/rendering.dart'; +import 'src/binding.dart'; + void main() { - // We use RenderingFlutterBinding to attach the render tree to the window. - RenderingFlutterBinding( + // We use ViewRenderingFlutterBinding to attach the render tree to the window. + ViewRenderingFlutterBinding( // The root of our render tree is a RenderPositionedBox, which centers its // child both vertically and horizontally. root: RenderPositionedBox( diff --git a/examples/layers/rendering/spinning_square.dart b/examples/layers/rendering/spinning_square.dart index 68b1359139304..cb833069fdecc 100644 --- a/examples/layers/rendering/spinning_square.dart +++ b/examples/layers/rendering/spinning_square.dart @@ -11,6 +11,8 @@ import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'src/binding.dart'; + class NonStopVSync implements TickerProvider { const NonStopVSync(); @override @@ -42,7 +44,7 @@ void main() { child: spin, ); // and attach it to the window. - RenderingFlutterBinding(root: root); + ViewRenderingFlutterBinding(root: root); // To make the square spin, we use an animation that repeats every 1800 // milliseconds. diff --git a/examples/layers/rendering/src/binding.dart b/examples/layers/rendering/src/binding.dart new file mode 100644 index 0000000000000..ae8cedcc90dde --- /dev/null +++ b/examples/layers/rendering/src/binding.dart @@ -0,0 +1,69 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; + +/// An extension of [RenderingFlutterBinding] that owns and manages a +/// [renderView]. +/// +/// Unlike [RenderingFlutterBinding], this binding also creates and owns a +/// [renderView] to simplify bootstrapping for apps that have a dedicated main +/// view. +class ViewRenderingFlutterBinding extends RenderingFlutterBinding { + /// Creates a binding for the rendering layer. + /// + /// The `root` render box is attached directly to the [renderView] and is + /// given constraints that require it to fill the window. The [renderView] + /// itself is attached to the [rootPipelineOwner]. + /// + /// This binding does not automatically schedule any frames. Callers are + /// responsible for deciding when to first call [scheduleFrame]. + ViewRenderingFlutterBinding({ RenderBox? root }) : _root = root; + + @override + void initInstances() { + super.initInstances(); + // TODO(goderbauer): Create window if embedder doesn't provide an implicit view. + assert(PlatformDispatcher.instance.implicitView != null); + _renderView = initRenderView(PlatformDispatcher.instance.implicitView!); + _renderView.child = _root; + _root = null; + } + + RenderBox? _root; + + @override + RenderView get renderView => _renderView; + late RenderView _renderView; + + /// Creates a [RenderView] object to be the root of the + /// [RenderObject] rendering tree, and initializes it so that it + /// will be rendered when the next frame is requested. + /// + /// Called automatically when the binding is created. + RenderView initRenderView(FlutterView view) { + final RenderView renderView = RenderView(view: view); + rootPipelineOwner.rootNode = renderView; + addRenderView(renderView); + renderView.prepareInitialFrame(); + return renderView; + } + + @override + PipelineOwner createRootPipelineOwner() { + return PipelineOwner( + onSemanticsOwnerCreated: () { + renderView.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { + renderView.updateSemantics(update); + }, + onSemanticsOwnerDisposed: () { + renderView.clearSemantics(); + }, + ); + } +} diff --git a/examples/layers/rendering/src/sector_layout.dart b/examples/layers/rendering/src/sector_layout.dart index e2716171f7969..a8bfa641f94dc 100644 --- a/examples/layers/rendering/src/sector_layout.dart +++ b/examples/layers/rendering/src/sector_layout.dart @@ -636,8 +636,6 @@ class SectorHitTestResult extends HitTestResult { /// A hit test entry used by [RenderSector]. class SectorHitTestEntry extends HitTestEntry { /// Creates a box hit test entry. - /// - /// The [radius] and [theta] argument must not be null. SectorHitTestEntry(RenderSector super.target, { required this.radius, required this.theta }); @override diff --git a/examples/layers/rendering/touch_input.dart b/examples/layers/rendering/touch_input.dart index 3747eb43dfc14..37a944528447d 100644 --- a/examples/layers/rendering/touch_input.dart +++ b/examples/layers/rendering/touch_input.dart @@ -8,6 +8,8 @@ import 'package:flutter/material.dart'; // Imported just for its color palette. import 'package:flutter/rendering.dart'; +import 'src/binding.dart'; + // Material design colors. :p List _kColors = [ Colors.teal, @@ -133,5 +135,5 @@ void main() { ..left = 20.0; // Finally, we attach the render tree we've built to the screen. - RenderingFlutterBinding(root: stack).scheduleFrame(); + ViewRenderingFlutterBinding(root: stack).scheduleFrame(); } diff --git a/examples/layers/widgets/spinning_mixed.dart b/examples/layers/widgets/spinning_mixed.dart index cdde8f7b6e3dd..6d3223c7b0065 100644 --- a/examples/layers/widgets/spinning_mixed.dart +++ b/examples/layers/widgets/spinning_mixed.dart @@ -52,10 +52,10 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( - child: Row( + child: const Row( children: [ - Image.network('https://flutter.dev/images/favicon.png'), - const Text('PRESS ME'), + FlutterLogo(), + Text('PRESS ME'), ], ), onPressed: () { @@ -102,6 +102,16 @@ void main() { transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center); final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox); - binding.renderView.child = root; + // TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into. + assert(binding.platformDispatcher.implicitView != null); + final RenderView view = RenderView( + view: binding.platformDispatcher.implicitView!, + child: root, + ); + final PipelineOwner pipelineOwner = PipelineOwner()..rootNode = view; + binding.rootPipelineOwner.adoptChild(pipelineOwner); + binding.addRenderView(view); + view.prepareInitialFrame(); + binding.addPersistentFrameCallback(rotate); } diff --git a/examples/layers/windows/flutter/CMakeLists.txt b/examples/layers/windows/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/examples/layers/windows/flutter/CMakeLists.txt +++ b/examples/layers/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/platform_channel/android/gradle.properties b/examples/platform_channel/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/platform_channel/android/gradle.properties +++ b/examples/platform_channel/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/platform_channel/pubspec.yaml b/examples/platform_channel/pubspec.yaml index 415db71426f59..5d7a463419ffb 100644 --- a/examples/platform_channel/pubspec.yaml +++ b/examples/platform_channel/pubspec.yaml @@ -1,28 +1,28 @@ name: platform_channel environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -53,22 +53,22 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/examples/platform_channel/windows/flutter/CMakeLists.txt b/examples/platform_channel/windows/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/examples/platform_channel/windows/flutter/CMakeLists.txt +++ b/examples/platform_channel/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/platform_channel_swift/pubspec.yaml b/examples/platform_channel_swift/pubspec.yaml index 728d5ecaa7286..103ab89a822f3 100644 --- a/examples/platform_channel_swift/pubspec.yaml +++ b/examples/platform_channel_swift/pubspec.yaml @@ -1,28 +1,28 @@ name: platform_channel_swift environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -53,22 +53,22 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 968e +# PUBSPEC CHECKSUM: 9dec diff --git a/examples/platform_view/android/gradle.properties b/examples/platform_view/android/gradle.properties index 94adc3a3f97aa..598d13fee4463 100644 --- a/examples/platform_view/android/gradle.properties +++ b/examples/platform_view/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/examples/platform_view/lib/main.dart b/examples/platform_view/lib/main.dart index 198dcdf2435e0..6e0e8904eaa6e 100644 --- a/examples/platform_view/lib/main.dart +++ b/examples/platform_view/lib/main.dart @@ -57,8 +57,9 @@ class _MyHomePageState extends State { return const Text('Continue in Windows view'); case TargetPlatform.macOS: return const Text('Continue in macOS view'); - case TargetPlatform.fuchsia: case TargetPlatform.linux: + return const Text('Continue in Linux view'); + case TargetPlatform.fuchsia: throw UnimplementedError('Platform not yet implemented'); } } diff --git a/examples/platform_view/linux/CMakeLists.txt b/examples/platform_view/linux/CMakeLists.txt new file mode 100644 index 0000000000000..67f289712447f --- /dev/null +++ b/examples/platform_view/linux/CMakeLists.txt @@ -0,0 +1,140 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "platform_view") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "io.flutter.examples.platform_view") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/examples/platform_view/linux/flutter/CMakeLists.txt b/examples/platform_view/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..d5bd01648a96d --- /dev/null +++ b/examples/platform_view/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/examples/platform_view/linux/main.cc b/examples/platform_view/linux/main.cc new file mode 100644 index 0000000000000..281a29e16b599 --- /dev/null +++ b/examples/platform_view/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/examples/platform_view/linux/my_application.cc b/examples/platform_view/linux/my_application.cc new file mode 100644 index 0000000000000..403df877b0ac7 --- /dev/null +++ b/examples/platform_view/linux/my_application.cc @@ -0,0 +1,167 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include + +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + + // Channel to receive platform view requests from Flutter. + FlMethodChannel* platform_view_channel; + + // Main window. + GtkWindow* window; + + // Current counter. + int64_t counter; + + // Request in progress. + FlMethodCall* method_call; + + // Native window requested by Flutter. + GtkWindow* native_window; + + // Label to show count. + GtkLabel* counter_label; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +static void update_counter_label(MyApplication* self) { + g_autofree gchar* text = + g_strdup_printf("Button tapped %" G_GINT64_FORMAT " %s.", self->counter, + self->counter == 1 ? "time" : "times"); + gtk_label_set_text(self->counter_label, text); +} + +static void button_clicked_cb(MyApplication* self) { + self->counter++; + update_counter_label(self); +} + +static void native_window_delete_event_cb(MyApplication* self, + gint response_id) { + g_autoptr(FlValue) counter_value = fl_value_new_int(self->counter); + fl_method_call_respond_success(self->method_call, counter_value, nullptr); + g_clear_object(&self->method_call); +} + +// Handle request to switch the view. +static void handle_switch_view(MyApplication* self, FlMethodCall* method_call) { + FlValue* counter_value = fl_method_call_get_args(method_call); + if (fl_value_get_type(counter_value) != FL_VALUE_TYPE_INT) { + fl_method_call_respond_error(self->method_call, "Invalid args", + "Invalid switchView args", nullptr, nullptr); + return; + } + + self->counter = fl_value_get_int(counter_value); + self->method_call = FL_METHOD_CALL(g_object_ref(method_call)); + + // Show the same UI in a native window. + self->native_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + gtk_window_set_transient_for(self->native_window, self->window); + gtk_window_set_modal(self->native_window, TRUE); + gtk_window_set_destroy_with_parent(self->native_window, TRUE); + g_signal_connect_swapped(self->native_window, "delete-event", + G_CALLBACK(native_window_delete_event_cb), self); + + GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_margin_start(box, 24); + gtk_widget_set_margin_end(box, 24); + gtk_widget_set_margin_top(box, 24); + gtk_widget_set_margin_bottom(box, 24); + gtk_widget_show(box); + gtk_container_add(GTK_CONTAINER(self->native_window), box); + + self->counter_label = GTK_LABEL(gtk_label_new("")); + gtk_widget_show(GTK_WIDGET(self->counter_label)); + gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(self->counter_label)); + + GtkWidget* button = gtk_button_new_with_label("+"); + gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(button)), + "circular"); + gtk_widget_set_halign(button, GTK_ALIGN_CENTER); + gtk_widget_show(button); + gtk_container_add(GTK_CONTAINER(box), button); + g_signal_connect_swapped(button, "clicked", G_CALLBACK(button_clicked_cb), + self); + + update_counter_label(self); + + gtk_window_present(self->native_window); +} + +// Handle platform view requests from Flutter. +static void platform_view_channel_method_cb(FlMethodChannel* channel, + FlMethodCall* method_call, + gpointer user_data) { + MyApplication* self = MY_APPLICATION(user_data); + + const char* name = fl_method_call_get_name(method_call); + if (g_str_equal(name, "switchView")) { + handle_switch_view(self, method_call); + } else { + fl_method_call_respond_not_implemented(method_call, nullptr); + } +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + + g_clear_object(&self->platform_view_channel); + + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + self->window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + gtk_window_set_default_size(self->window, 1280, 720); + gtk_widget_show(GTK_WIDGET(self->window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(self->window), GTK_WIDGET(view)); + + // Create channel to handle platform view requests from Flutter. + FlEngine* engine = fl_view_get_engine(view); + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->platform_view_channel = fl_method_channel_new( + fl_engine_get_binary_messenger(engine), + "samples.flutter.io/platform_view", FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->platform_view_channel, + platform_view_channel_method_cb, + self, nullptr); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; + G_APPLICATION_CLASS(klass)->activate = my_application_activate; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/examples/platform_view/linux/my_application.h b/examples/platform_view/linux/my_application.h new file mode 100644 index 0000000000000..8c66ec485434d --- /dev/null +++ b/examples/platform_view/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/examples/platform_view/pubspec.yaml b/examples/platform_view/pubspec.yaml index 6d1111bc69050..6ccd26d43788c 100644 --- a/examples/platform_view/pubspec.yaml +++ b/examples/platform_view/pubspec.yaml @@ -1,18 +1,18 @@ name: platform_view environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -20,4 +20,4 @@ flutter: assets: - assets/flutter-mark-square-64.png -# PUBSPEC CHECKSUM: a9c0 +# PUBSPEC CHECKSUM: 081a diff --git a/examples/platform_view/windows/flutter/CMakeLists.txt b/examples/platform_view/windows/flutter/CMakeLists.txt index 10873dd1af99c..c8f7abf1ebea9 100644 --- a/examples/platform_view/windows/flutter/CMakeLists.txt +++ b/examples/platform_view/windows/flutter/CMakeLists.txt @@ -14,6 +14,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -96,7 +101,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/examples/splash/pubspec.yaml b/examples/splash/pubspec.yaml index 478640f70f7b5..4f3ec9a334fc0 100644 --- a/examples/splash/pubspec.yaml +++ b/examples/splash/pubspec.yaml @@ -1,18 +1,18 @@ name: splash environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -25,10 +25,10 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: b642 +# PUBSPEC CHECKSUM: dc9e diff --git a/examples/texture/pubspec.yaml b/examples/texture/pubspec.yaml index 20cbb697abf18..04b34856895e1 100644 --- a/examples/texture/pubspec.yaml +++ b/examples/texture/pubspec.yaml @@ -1,26 +1,26 @@ name: texture environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: sdk: flutter - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -51,17 +51,17 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 3549 +# PUBSPEC CHECKSUM: aaa7 diff --git a/packages/flutter/lib/fix_data/fix_material/fix_material.yaml b/packages/flutter/lib/fix_data/fix_material/fix_material.yaml index 0359c8da87f0c..fb8467c0c858f 100644 --- a/packages/flutter/lib/fix_data/fix_material/fix_material.yaml +++ b/packages/flutter/lib/fix_data/fix_material/fix_material.yaml @@ -897,4 +897,43 @@ transforms: oldName: 'showTrackOnHover' newName: 'trackVisibility' + # Changes made in https://github.com/flutter/flutter/pull/129942 + - title: "Migrate to 'Easing.legacy'" + date: 2023-07-04 + element: + uris: [ 'material.dart' ] + variable: 'standardEasing' + changes: + - kind: 'replacedBy' + newElement: + uris: [ 'material.dart' ] + field: legacy + inClass: Easing + + # Changes made in https://github.com/flutter/flutter/pull/129942 + - title: "Migrate to 'Easing.legacyAccelerate'" + date: 2023-07-04 + element: + uris: [ 'material.dart' ] + variable: 'accelerateEasing' + changes: + - kind: 'replacedBy' + newElement: + uris: [ 'material.dart' ] + field: legacyAccelerate + inClass: Easing + + # Changes made in https://github.com/flutter/flutter/pull/129942 + - title: "Migrate to 'Easing.legacyDecelerate'" + date: 2023-07-04 + element: + uris: [ 'material.dart' ] + variable: 'decelerateEasing' + changes: + - kind: 'replacedBy' + newElement: + uris: [ 'material.dart' ] + field: legacyDecelerate + inClass: Easing + # Before adding a new fix: read instructions at the top of this file. diff --git a/packages/flutter/lib/fix_data/fix_material/fix_theme_data.yaml b/packages/flutter/lib/fix_data/fix_material/fix_theme_data.yaml index fda29f5209ed6..f3742f88e7b74 100644 --- a/packages/flutter/lib/fix_data/fix_material/fix_theme_data.yaml +++ b/packages/flutter/lib/fix_data/fix_material/fix_theme_data.yaml @@ -1686,4 +1686,15 @@ transforms: kind: 'fragment' value: 'arguments[colorScheme]' + # Changes made in https://github.com/flutter/flutter/pull/131455 + - title: "Remove 'useMaterial3'" + date: 2023-07-27 + element: + uris: [ 'material.dart' ] + method: 'copyWith' + inClass: 'ThemeData' + changes: + - kind: 'removeParameter' + name: 'useMaterial3' + # Before adding a new fix: read instructions at the top of this file. diff --git a/packages/flutter/lib/fix_data/fix_painting.yaml b/packages/flutter/lib/fix_data/fix_painting.yaml index 0b88041a101c9..b83d86f28ce9c 100644 --- a/packages/flutter/lib/fix_data/fix_painting.yaml +++ b/packages/flutter/lib/fix_data/fix_painting.yaml @@ -18,6 +18,94 @@ # * Fixes in this file are from the Painting library. * version: 1 transforms: + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'painting.dart' ] + method: 'computeMaxIntrinsicWidth' + inClass: 'TextPainter' + changes: + - kind: 'addParameter' + index: 4 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'painting.dart' ] + method: 'computeWidth' + inClass: 'TextPainter' + changes: + - kind: 'addParameter' + index: 4 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'painting.dart' ] + constructor: '' + inClass: 'TextPainter' + changes: + - kind: 'addParameter' + index: 0 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'painting.dart' ] + method: 'getTextStyle' + inClass: 'TextStyle' + changes: + - kind: 'addParameter' + index: 0 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + # Changes made in https://github.com/flutter/flutter/pull/121152 - title: "Rename to 'fromViewPadding'" date: 2022-02-21 diff --git a/packages/flutter/lib/fix_data/fix_rendering.yaml b/packages/flutter/lib/fix_data/fix_rendering.yaml index efc56a8bc4275..75ae29fc5615d 100644 --- a/packages/flutter/lib/fix_data/fix_rendering.yaml +++ b/packages/flutter/lib/fix_data/fix_rendering.yaml @@ -18,6 +18,50 @@ # * Fixes in this file are from the Rendering library. * version: 1 transforms: + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'rendering.dart' ] + constructor: '' + inClass: 'RenderParagraph' + changes: + - kind: 'addParameter' + index: 5 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'rendering.dart' ] + constructor: '' + inClass: 'RenderEditable' + changes: + - kind: 'addParameter' + index: 15 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + # Changes made in https://github.com/flutter/flutter/pull/66305 - title: "Migrate to 'clipBehavior'" date: 2020-09-22 diff --git a/packages/flutter/lib/fix_data/fix_widgets/fix_media_query.yaml b/packages/flutter/lib/fix_data/fix_widgets/fix_media_query.yaml new file mode 100644 index 0000000000000..c7092e9311b45 --- /dev/null +++ b/packages/flutter/lib/fix_data/fix_widgets/fix_media_query.yaml @@ -0,0 +1,65 @@ +# Copyright 2014 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# For details regarding the *Flutter Fix* feature, see +# https://flutter.dev/docs/development/tools/flutter-fix + +# Please add new fixes to the top of the file, separated by one blank line +# from other fixes. In a comment, include a link to the PR where the change +# requiring the fix was made. + +# Every fix must be tested. See the flutter/packages/flutter/test_fixes/README.md +# file for instructions on testing these data driven fixes. + +# For documentation about this file format, see +# https://dart.dev/go/data-driven-fixes. + +# * Fixes in this file are for MediaQuery and MediaQueryData from the widgets library. * +version: 1 +transforms: + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ] + method: 'copyWith' + inClass: 'MediaQueryData' + changes: + - kind: 'addParameter' + index: 3 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ] + constructor: '' + inClass: 'MediaQueryData' + changes: + - kind: 'addParameter' + index: 3 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + +# Before adding a new fix: read instructions at the top of this file. diff --git a/packages/flutter/lib/fix_data/fix_widgets/fix_rich_text.yaml b/packages/flutter/lib/fix_data/fix_widgets/fix_rich_text.yaml new file mode 100644 index 0000000000000..1f3f5cc014837 --- /dev/null +++ b/packages/flutter/lib/fix_data/fix_widgets/fix_rich_text.yaml @@ -0,0 +1,43 @@ +# Copyright 2014 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# For details regarding the *Flutter Fix* feature, see +# https://flutter.dev/docs/development/tools/flutter-fix + +# Please add new fixes to the top of the file, separated by one blank line +# from other fixes. In a comment, include a link to the PR where the change +# requiring the fix was made. + +# Every fix must be tested. See the flutter/packages/flutter/test_fixes/README.md +# file for instructions on testing these data driven fixes. + +# For documentation about this file format, see +# https://dart.dev/go/data-driven-fixes. + +# * Fixes in this file are for RichText from the widgets library. * +version: 1 +transforms: + # Change made in https://github.com/flutter/flutter/pull/128522 + - title: "Migrate to 'textScaler'" + date: 2023-06-09 + element: + uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ] + constructor: '' + inClass: 'RichText' + changes: + - kind: 'addParameter' + index: 7 + name: 'textScaler' + style: optional_named + argumentValue: + expression: 'TextScaler.linear({% textScaleFactor %})' + requiredIf: "textScaleFactor != ''" + - kind: 'removeParameter' + name: 'textScaleFactor' + variables: + textScaleFactor: + kind: 'fragment' + value: 'arguments[textScaleFactor]' + +# Before adding a new fix: read instructions at the top of this file. diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index 43ac8133870bf..378b206d74265 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -34,7 +34,6 @@ export 'src/foundation/diagnostics.dart'; export 'src/foundation/isolates.dart'; export 'src/foundation/key.dart'; export 'src/foundation/licenses.dart'; -export 'src/foundation/math.dart'; export 'src/foundation/memory_allocations.dart'; export 'src/foundation/node.dart'; export 'src/foundation/object.dart'; diff --git a/packages/flutter/lib/gestures.dart b/packages/flutter/lib/gestures.dart index be2f30debde65..8a126894494c6 100644 --- a/packages/flutter/lib/gestures.dart +++ b/packages/flutter/lib/gestures.dart @@ -30,5 +30,6 @@ export 'src/gestures/recognizer.dart'; export 'src/gestures/resampler.dart'; export 'src/gestures/scale.dart'; export 'src/gestures/tap.dart'; +export 'src/gestures/tap_and_drag.dart'; export 'src/gestures/team.dart'; export 'src/gestures/velocity_tracker.dart'; diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 8ca80a7364088..66153358ca668 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -10,14 +10,10 @@ /// /// See also: /// -/// * [flutter.dev/widgets/material](https://flutter.dev/widgets/material) +/// * [docs.flutter.dev/ui/widgets/material](https://docs.flutter.dev/ui/widgets/material) /// for a catalog of commonly-used Material component widgets. -/// * [material.io/design](https://material.io/design/) -/// for an introduction to Material Design. -/// * [material.io/components](https://material.io/components?platform=flutter) -/// for the Material 2 specification. -/// * [m3.material.io](https://m3.material.io) -/// for the Material 3 specification. +/// * [m3.material.io](https://m3.material.io/) for the Material 3 specification +/// * [m2.material.io](https://m2.material.io/) for the Material 2 specification library material; export 'src/material/about.dart'; @@ -126,6 +122,7 @@ export 'src/material/menu_button_theme.dart'; export 'src/material/menu_style.dart'; export 'src/material/menu_theme.dart'; export 'src/material/mergeable_material.dart'; +export 'src/material/motion.dart'; export 'src/material/navigation_bar.dart'; export 'src/material/navigation_bar_theme.dart'; export 'src/material/navigation_drawer.dart'; diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index 8a78b6bf4a186..2fa89c23cf8bc 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -60,5 +60,6 @@ export 'src/painting/stadium_border.dart'; export 'src/painting/star_border.dart'; export 'src/painting/strut_style.dart'; export 'src/painting/text_painter.dart'; +export 'src/painting/text_scaler.dart'; export 'src/painting/text_span.dart'; export 'src/painting/text_style.dart'; diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart index 9cfde5e909806..cdd209fd1f8bb 100644 --- a/packages/flutter/lib/services.dart +++ b/packages/flutter/lib/services.dart @@ -19,6 +19,7 @@ export 'src/services/browser_context_menu.dart'; export 'src/services/clipboard.dart'; export 'src/services/debug.dart'; export 'src/services/deferred_component.dart'; +export 'src/services/flavor.dart'; export 'src/services/font_loader.dart'; export 'src/services/haptic_feedback.dart'; export 'src/services/hardware_keyboard.dart'; diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index 24df24fb8cbff..2a5f7e9850fb8 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -230,9 +230,9 @@ class AnimationController extends Animation /// value at which this animation is deemed to be completed. It cannot be /// null. /// - /// * `vsync` is the [TickerProvider] for the current context. It can be - /// changed by calling [resync]. It is required and must not be null. See - /// [TickerProvider] for advice on obtaining a ticker provider. + /// * `vsync` is the required [TickerProvider] for the current context. It can + /// be changed by calling [resync]. See [TickerProvider] for advice on + /// obtaining a ticker provider. AnimationController({ double? value, this.duration, @@ -258,9 +258,9 @@ class AnimationController extends Animation /// * [debugLabel] is a string to help identify this animation during /// debugging (used by [toString]). /// - /// * `vsync` is the [TickerProvider] for the current context. It can be - /// changed by calling [resync]. It is required and must not be null. See - /// [TickerProvider] for advice on obtaining a ticker provider. + /// * `vsync` is the required [TickerProvider] for the current context. It can + /// be changed by calling [resync]. See [TickerProvider] for advice on + /// obtaining a ticker provider. /// /// This constructor is most useful for animations that will be driven using a /// physics simulation, especially when the physics simulation has no diff --git a/packages/flutter/lib/src/animation/animations.dart b/packages/flutter/lib/src/animation/animations.dart index de2f7c04fb7de..4fb2d6732fd1d 100644 --- a/packages/flutter/lib/src/animation/animations.dart +++ b/packages/flutter/lib/src/animation/animations.dart @@ -269,8 +269,6 @@ class ReverseAnimation extends Animation with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin { /// Creates a reverse animation. - /// - /// The parent argument must not be null. ReverseAnimation(this.parent); /// The animation whose value and direction this animation is reversing. @@ -376,8 +374,6 @@ class ReverseAnimation extends Animation /// [Curve]. class CurvedAnimation extends Animation with AnimationWithParentMixin { /// Creates a curved animation. - /// - /// The parent and curve arguments must not be null. CurvedAnimation({ required this.parent, required this.curve, @@ -631,8 +627,9 @@ class TrainHoppingAnimation extends Animation /// animation otherwise. abstract class CompoundAnimation extends Animation with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { - /// Creates a CompoundAnimation. Both arguments must be non-null. Either can - /// be a CompoundAnimation itself to combine multiple animations. + /// Creates a [CompoundAnimation]. + /// + /// Either argument can be a [CompoundAnimation] itself to combine multiple animations. CompoundAnimation({ required this.first, required this.next, @@ -720,8 +717,8 @@ class AnimationMean extends CompoundAnimation { class AnimationMax extends CompoundAnimation { /// Creates an [AnimationMax]. /// - /// Both arguments must be non-null. Either can be an [AnimationMax] itself - /// to combine multiple animations. + /// Either argument can be an [AnimationMax] itself to combine multiple + /// animations. AnimationMax(Animation first, Animation next) : super(first: first, next: next); @override @@ -735,8 +732,8 @@ class AnimationMax extends CompoundAnimation { class AnimationMin extends CompoundAnimation { /// Creates an [AnimationMin]. /// - /// Both arguments must be non-null. Either can be an [AnimationMin] itself - /// to combine multiple animations. + /// Either argument can be an [AnimationMin] itself to combine multiple + /// animations. AnimationMin(Animation first, Animation next) : super(first: first, next: next); @override diff --git a/packages/flutter/lib/src/animation/curves.dart b/packages/flutter/lib/src/animation/curves.dart index fa75eadd965a7..aaa09951ab27a 100644 --- a/packages/flutter/lib/src/animation/curves.dart +++ b/packages/flutter/lib/src/animation/curves.dart @@ -127,8 +127,6 @@ class _Linear extends Curve { /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_sawtooth.mp4} class SawTooth extends Curve { /// Creates a sawtooth curve. - /// - /// The [count] argument must not be null. const SawTooth(this.count); /// The number of repetitions of the sawtooth pattern in the unit interval. @@ -157,8 +155,6 @@ class SawTooth extends Curve { /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_interval.mp4} class Interval extends Curve { /// Creates an interval curve. - /// - /// The arguments must not be null. const Interval(this.begin, this.end, { this.curve = Curves.linear }); /// The largest value for which this interval is 0.0. @@ -202,8 +198,6 @@ class Interval extends Curve { /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_threshold.mp4} class Threshold extends Curve { /// Creates a threshold curve. - /// - /// The [threshold] argument must not be null. const Threshold(this.threshold); /// The value before which the curve is 0.0 and after which the curve is 1.0. @@ -302,8 +296,6 @@ class Cubic extends Curve { /// /// Rather than creating a new instance, consider using one of the common /// cubic curves in [Curves]. - /// - /// The [a] (x1), [b] (y1), [c] (x2) and [d] (y2) arguments must not be null. const Cubic(this.a, this.b, this.c, this.d); /// The x coordinate of the first control point. @@ -599,8 +591,6 @@ abstract class Curve2D extends ParametricCurve { /// * [Curve2D], a parametric curve that maps a double parameter to a 2D location. class Curve2DSample { /// Creates an object that holds a sample; used with [Curve2D] subclasses. - /// - /// All arguments must not be null. const Curve2DSample(this.t, this.value); /// The parametric location of this sample point along the curve. @@ -659,8 +649,7 @@ class CatmullRomSpline extends Curve2D { /// control point. The default is chosen so that the slope of the line at the /// ends matches that of the first or last line segment in the control points. /// - /// The `tension` and `controlPoints` arguments must not be null, and the - /// `controlPoints` list must contain at least four control points to + /// The `controlPoints` list must contain at least four control points to /// interpolate. /// /// The internal curve data structures are lazily computed the first time @@ -705,6 +694,27 @@ class CatmullRomSpline extends Curve2D { Offset? startHandle, Offset? endHandle, }) { + assert( + startHandle == null || startHandle.isFinite, + 'The provided startHandle of CatmullRomSpline must be finite. The ' + 'startHandle given was $startHandle.' + ); + assert( + endHandle == null || endHandle.isFinite, + 'The provided endHandle of CatmullRomSpline must be finite. The endHandle ' + 'given was $endHandle.' + ); + assert(() { + for (int index = 0; index < controlPoints.length; index++) { + if (!controlPoints[index].isFinite) { + throw FlutterError( + 'The provided CatmullRomSpline control point at index $index is not ' + 'finite. The control point given was ${controlPoints[index]}.' + ); + } + } + return true; + }()); // If not specified, select the first and last control points (which are // handles: they are not intersected by the resulting curve) so that they // extend the first and last segments, respectively. @@ -839,8 +849,6 @@ class CatmullRomCurve extends Curve { /// [transform] is called. If you would rather pre-compute the curve, use /// [CatmullRomCurve.precompute] instead. /// - /// All of the arguments must not be null. - /// /// See also: /// /// * This [paper on using Catmull-Rom splines](http://faculty.cs.tamu.edu/schaefer/research/cr_cad.pdf). @@ -1123,8 +1131,6 @@ class CatmullRomCurve extends Curve { /// * [CurvedAnimation], which can take a separate curve and reverse curve. class FlippedCurve extends Curve { /// Creates a flipped curve. - /// - /// The [curve] argument must not be null. const FlippedCurve(this.curve); /// The curve that is being flipped. @@ -1354,6 +1360,7 @@ class ElasticInOutCurve extends Curve { /// /// * [Curve], the interface implemented by the constants available from the /// [Curves] class. +/// * [Easing], for the Material animation curves. abstract final class Curves { /// A linear animation curve. /// @@ -1741,7 +1748,7 @@ abstract final class Curves { /// /// See also: /// - /// * [standardEasing], the name for this curve in the Material specification. + /// * [Easing.legacy], the name for this curve in the Material specification. static const Cubic fastOutSlowIn = Cubic(0.4, 0.0, 0.2, 1.0); /// A cubic animation curve that starts quickly, slows down, and then ends diff --git a/packages/flutter/lib/src/animation/tween.dart b/packages/flutter/lib/src/animation/tween.dart index 1f0bb5d387a22..14cb5f0d77bda 100644 --- a/packages/flutter/lib/src/animation/tween.dart +++ b/packages/flutter/lib/src/animation/tween.dart @@ -547,8 +547,6 @@ class ConstantTween extends Tween { /// [AnimationController]. class CurveTween extends Animatable { /// Creates a curve tween. - /// - /// The [curve] argument must not be null. CurveTween({ required this.curve }); /// The curve to use when transforming the value of the animation. diff --git a/packages/flutter/lib/src/animation/tween_sequence.dart b/packages/flutter/lib/src/animation/tween_sequence.dart index a8e3208a5953a..8600ea836dbcb 100644 --- a/packages/flutter/lib/src/animation/tween_sequence.dart +++ b/packages/flutter/lib/src/animation/tween_sequence.dart @@ -122,7 +122,7 @@ class FlippedTweenSequence extends TweenSequence { class TweenSequenceItem { /// Construct a TweenSequenceItem. /// - /// The [tween] must not be null and [weight] must be greater than 0.0. + /// The [weight] must be greater than 0.0. const TweenSequenceItem({ required this.tween, required this.weight, diff --git a/packages/flutter/lib/src/cupertino/activity_indicator.dart b/packages/flutter/lib/src/cupertino/activity_indicator.dart index bd5bacf6b76e1..b1c2e5ce62d45 100644 --- a/packages/flutter/lib/src/cupertino/activity_indicator.dart +++ b/packages/flutter/lib/src/cupertino/activity_indicator.dart @@ -67,7 +67,7 @@ class CupertinoActivityIndicator extends StatefulWidget { /// Radius of the spinner widget. /// - /// Defaults to 10px. Must be positive and cannot be null. + /// Defaults to 10 pixels. Must be positive. final double radius; /// Determines the percentage of spinner ticks that will be shown. Typical usage would @@ -75,7 +75,7 @@ class CupertinoActivityIndicator extends StatefulWidget { /// during pull-to-refresh when the drag-down action shows one tick at a time as /// the user continues to drag down. /// - /// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null. + /// Defaults to one. Must be between zero and one, inclusive. final double progress; @override diff --git a/packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart b/packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart index 33f5e502eabac..da1c8bb08239b 100644 --- a/packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart +++ b/packages/flutter/lib/src/cupertino/adaptive_text_selection_toolbar.dart @@ -94,6 +94,9 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget { required VoidCallback? onCut, required VoidCallback? onPaste, required VoidCallback? onSelectAll, + required VoidCallback? onLookUp, + required VoidCallback? onSearchWeb, + required VoidCallback? onShare, required VoidCallback? onLiveTextInput, required this.anchors, }) : children = null, @@ -103,6 +106,9 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget { onCut: onCut, onPaste: onPaste, onSelectAll: onSelectAll, + onLookUp: onLookUp, + onSearchWeb: onSearchWeb, + onShare: onShare, onLiveTextInput: onLiveTextInput ); diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart index 47e45b198b0b9..f68ffc92bcd09 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart @@ -145,8 +145,6 @@ class CupertinoApp extends StatefulWidget { /// unsupported route. /// /// This class creates an instance of [WidgetsApp]. - /// - /// The boolean arguments, [routes], and [navigatorObservers], must not be null. const CupertinoApp({ super.key, this.navigatorKey, @@ -157,6 +155,7 @@ class CupertinoApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, + this.onNavigationNotification, List this.navigatorObservers = const [], this.builder, this.title = '', @@ -202,6 +201,7 @@ class CupertinoApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, + this.onNavigationNotification, this.color, this.locale, this.localizationsDelegates, @@ -268,6 +268,9 @@ class CupertinoApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} final RouteFactory? onUnknownRoute; + /// {@macro flutter.widgets.widgetsApp.onNavigationNotification} + final NotificationListenerCallback? onNavigationNotification; + /// {@macro flutter.widgets.widgetsApp.navigatorObservers} final List? navigatorObservers; @@ -501,6 +504,12 @@ class _CupertinoAppState extends State { _heroController = CupertinoApp.createCupertinoHeroController(); } + @override + void dispose() { + _heroController.dispose(); + super.dispose(); + } + // Combine the default localization for Cupertino with the ones contributed // by the localizationsDelegates parameter, if any. Only the first delegate // of a particular LocalizationsDelegate.type is loaded so the @@ -573,6 +582,7 @@ class _CupertinoAppState extends State { onGenerateRoute: widget.onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes, onUnknownRoute: widget.onUnknownRoute, + onNavigationNotification: widget.onNavigationNotification, builder: widget.builder, title: widget.title, onGenerateTitle: widget.onGenerateTitle, diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 4bdbe86a99b22..6dd1bf3786404 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -37,13 +37,10 @@ const Color _kDefaultTabBarInactiveColor = CupertinoColors.inactiveGray; /// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by /// default), it will produce a blurring effect to the content behind it. /// -/// When used as [CupertinoTabScaffold.tabBar], by default [CupertinoTabBar] has -/// its text scale factor set to 1.0 and does not respond to text scale factor -/// changes from the operating system, to match the native iOS behavior. To override -/// this behavior, wrap each of the `navigationBar`'s components inside a [MediaQuery] -/// with the desired [MediaQueryData.textScaleFactor] value. The text scale factor -/// value from the operating system can be retrieved in many ways, such as querying -/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext]. +/// When used as [CupertinoTabScaffold.tabBar], by default [CupertinoTabBar] +/// disables text scaling to match the native iOS behavior. To override +/// this behavior, wrap each of the `navigationBar`'s components inside a +/// [MediaQuery] with the desired [TextScaler]. /// /// {@tool dartpad} /// This example shows a [CupertinoTabBar] placed in a [CupertinoTabScaffold]. @@ -82,8 +79,6 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { assert(height >= 0.0); /// The interactive items laid out within the bottom navigation bar. - /// - /// Must not be null. final List items; /// The callback that is called when a item is tapped. @@ -95,8 +90,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { /// The index into [items] of the current active item. /// - /// Must not be null and must inclusively be between 0 and the number of tabs - /// minus 1. + /// Must be between 0 and the number of tabs minus 1, inclusive. final int currentIndex; /// The background color of the tab bar. If it contains transparency, the @@ -116,7 +110,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { /// in the unselected state. /// /// Defaults to a [CupertinoDynamicColor] that matches the disabled foreground - /// color of the native `UITabBar` component. Cannot be null. + /// color of the native `UITabBar` component. final Color inactiveColor; /// The size of all of the [BottomNavigationBarItem] icons. @@ -124,13 +118,11 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { /// This value is used to configure the [IconTheme] for the navigation bar. /// When a [BottomNavigationBarItem.icon] widget is not an [Icon] the widget /// should configure itself to match the icon theme's size and color. - /// - /// Must not be null. final double iconSize; /// The height of the [CupertinoTabBar]. /// - /// Defaults to 50.0. Must not be null. + /// Defaults to 50. final double height; /// The border of the [CupertinoTabBar]. diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart index b5f71d54b9b81..73c90d01f1c25 100644 --- a/packages/flutter/lib/src/cupertino/button.dart +++ b/packages/flutter/lib/src/cupertino/button.dart @@ -95,7 +95,7 @@ class CupertinoButton extends StatefulWidget { /// Ignored if the [CupertinoButton] doesn't also have a [color]. /// /// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is - /// specified. Must not be null. + /// specified. final Color disabledColor; /// The callback that is called when the button is tapped or otherwise activated. diff --git a/packages/flutter/lib/src/cupertino/checkbox.dart b/packages/flutter/lib/src/cupertino/checkbox.dart index 9d37c7af9c03c..6df97362b2dfc 100644 --- a/packages/flutter/lib/src/cupertino/checkbox.dart +++ b/packages/flutter/lib/src/cupertino/checkbox.dart @@ -57,8 +57,6 @@ class CupertinoCheckbox extends StatefulWidget { /// can only be null if [tristate] is true. /// * [onChanged], which is called when the value of the checkbox should /// change. It can be set to null to disable the checkbox. - /// - /// The values of [tristate] and [autofocus] must not be null. const CupertinoCheckbox({ super.key, required this.value, diff --git a/packages/flutter/lib/src/cupertino/colors.dart b/packages/flutter/lib/src/cupertino/colors.dart index 7e0dfc1741734..7db9b9a489977 100644 --- a/packages/flutter/lib/src/cupertino/colors.dart +++ b/packages/flutter/lib/src/cupertino/colors.dart @@ -738,8 +738,6 @@ abstract final class CupertinoColors { class CupertinoDynamicColor extends Color with Diagnosticable { /// Creates an adaptive [Color] that changes its effective color based on the /// [BuildContext] given. The default effective color is [color]. - /// - /// All the colors must not be null. const CupertinoDynamicColor({ String? debugLabel, required Color color, @@ -768,8 +766,6 @@ class CupertinoDynamicColor extends Color with Diagnosticable { /// given [BuildContext]'s brightness (from [MediaQueryData.platformBrightness] /// or [CupertinoThemeData.brightness]) and accessibility contrast setting /// ([MediaQueryData.highContrast]). The default effective color is [color]. - /// - /// All the colors must not be null. const CupertinoDynamicColor.withBrightnessAndContrast({ String? debugLabel, required Color color, @@ -791,8 +787,6 @@ class CupertinoDynamicColor extends Color with Diagnosticable { /// Creates an adaptive [Color] that changes its effective color based on the given /// [BuildContext]'s brightness (from [MediaQueryData.platformBrightness] or /// [CupertinoThemeData.brightness]). The default effective color is [color]. - /// - /// All the colors must not be null. const CupertinoDynamicColor.withBrightness({ String? debugLabel, required Color color, @@ -828,8 +822,8 @@ class CupertinoDynamicColor extends Color with Diagnosticable { /// The current effective color. /// - /// Must not be null. Defaults to [color] if this [CupertinoDynamicColor] has - /// never been resolved. + /// Defaults to [color] if this [CupertinoDynamicColor] has never been + /// resolved. final Color _effectiveColor; @override @@ -1126,7 +1120,7 @@ class CupertinoDynamicColor extends Color with Diagnosticable { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); if (_debugLabel != null) { - properties.add(MessageProperty('debugLabel', _debugLabel!)); + properties.add(MessageProperty('debugLabel', _debugLabel)); } properties.add(createCupertinoColorProperty('color', color)); if (_isPlatformBrightnessDependent) { @@ -1158,8 +1152,6 @@ class CupertinoDynamicColor extends Color with Diagnosticable { } /// Creates a diagnostics property for [CupertinoDynamicColor]. -/// -/// The [showName], [style], and [level] arguments must not be null. DiagnosticsProperty createCupertinoColorProperty( String name, Color? value, { diff --git a/packages/flutter/lib/src/cupertino/context_menu.dart b/packages/flutter/lib/src/cupertino/context_menu.dart index f0d610c459b2b..385bae14fbb32 100644 --- a/packages/flutter/lib/src/cupertino/context_menu.dart +++ b/packages/flutter/lib/src/cupertino/context_menu.dart @@ -6,12 +6,13 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' show kMinFlingVelocity; +import 'package:flutter/gestures.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show HapticFeedback; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'localizations.dart'; // The scale of the child at the time that the CupertinoContextMenu opens. // This value was eyeballed from a physical device running iOS 13.1.2. @@ -131,9 +132,7 @@ enum _ContextMenuLocation { class CupertinoContextMenu extends StatefulWidget { /// Create a context menu. /// - /// [actions] is required and cannot be null or empty. - /// - /// [child] is required and cannot be null. + /// The [actions] parameter cannot be empty. CupertinoContextMenu({ super.key, required this.actions, @@ -152,9 +151,7 @@ class CupertinoContextMenu extends StatefulWidget { /// Use instead of the default constructor when it is needed to have a more /// custom animation. /// - /// [actions] is required and cannot be null or empty. - /// - /// [builder] is required. + /// The [actions] parameter cannot be empty. CupertinoContextMenu.builder({ super.key, required this.actions, @@ -388,7 +385,7 @@ class CupertinoContextMenu extends StatefulWidget { /// /// These actions are typically [CupertinoContextMenuAction]s. /// - /// This parameter cannot be null or empty. + /// This parameter must not be empty. final List actions; /// If true, clicking on the [CupertinoContextMenuAction]s will @@ -479,6 +476,7 @@ class _CupertinoContextMenuState extends State with Ticker OverlayEntry? _lastOverlayEntry; _ContextMenuRoute? _route; final double _midpoint = CupertinoContextMenu.animationOpensAt / 2; + late final TapGestureRecognizer _tapGestureRecognizer; @override void initState() { @@ -489,13 +487,20 @@ class _CupertinoContextMenuState extends State with Ticker upperBound: CupertinoContextMenu.animationOpensAt, ); _openController.addStatusListener(_onDecoyAnimationStatusChange); + _tapGestureRecognizer = TapGestureRecognizer() + ..onTapCancel = _onTapCancel + ..onTapDown = _onTapDown + ..onTapUp = _onTapUp + ..onTap = _onTap; } void _listenerCallback() { if (_openController.status != AnimationStatus.reverse && - _openController.value >= _midpoint && - widget.enableHapticFeedback) { - HapticFeedback.heavyImpact(); + _openController.value >= _midpoint) { + if (widget.enableHapticFeedback) { + HapticFeedback.heavyImpact(); + } + _tapGestureRecognizer.resolve(GestureDisposition.accepted); _openController.removeListener(_listenerCallback); } } @@ -535,7 +540,7 @@ class _CupertinoContextMenuState extends State with Ticker _route = _ContextMenuRoute( actions: widget.actions, - barrierLabel: 'Dismiss', + barrierLabel: CupertinoLocalizations.of(context).menuDismissLabel, filter: ui.ImageFilter.blur( sigmaX: 5.0, sigmaY: 5.0, @@ -563,6 +568,7 @@ class _CupertinoContextMenuState extends State with Ticker }); } _lastOverlayEntry?.remove(); + _lastOverlayEntry?.dispose(); _lastOverlayEntry = null; case AnimationStatus.completed: @@ -576,6 +582,7 @@ class _CupertinoContextMenuState extends State with Ticker // one frame. SchedulerBinding.instance.addPostFrameCallback((Duration _) { _lastOverlayEntry?.remove(); + _lastOverlayEntry?.dispose(); _lastOverlayEntry = null; _openController.reset(); }); @@ -662,11 +669,8 @@ class _CupertinoContextMenuState extends State with Ticker Widget build(BuildContext context) { return MouseRegion( cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, - child: GestureDetector( - onTapCancel: _onTapCancel, - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTap: _onTap, + child: Listener( + onPointerDown: _tapGestureRecognizer.addPointer, child: TickerMode( enabled: !_childHidden, child: Visibility.maintain( diff --git a/packages/flutter/lib/src/cupertino/date_picker.dart b/packages/flutter/lib/src/cupertino/date_picker.dart index 1b43e697bc540..dc15ea3e35d2b 100644 --- a/packages/flutter/lib/src/cupertino/date_picker.dart +++ b/packages/flutter/lib/src/cupertino/date_picker.dart @@ -237,17 +237,16 @@ class CupertinoDatePicker extends StatefulWidget { /// to [CupertinoDatePickerMode.dateAndTime]. /// /// [onDateTimeChanged] is the callback called when the selected date or time - /// changes and must not be null. When in [CupertinoDatePickerMode.time] mode, - /// the year, month and day will be the same as [initialDateTime]. When in + /// changes. When in [CupertinoDatePickerMode.time] mode, the year, month and + /// day will be the same as [initialDateTime]. When in /// [CupertinoDatePickerMode.date] mode, this callback will always report the /// start time of the currently selected day. When in /// [CupertinoDatePickerMode.monthYear] mode, the day and time will be the /// start time of the first day of the month. /// /// [initialDateTime] is the initial date time of the picker. Defaults to the - /// present date and time and must not be null. The present must conform to - /// the intervals set in [minimumDate], [maximumDate], [minimumYear], and - /// [maximumYear]. + /// present date and time. The present must conform to the intervals set in + /// [minimumDate], [maximumDate], [minimumYear], and [maximumYear]. /// /// [minimumDate] is the minimum selectable [DateTime] of the picker. When set /// to null, the picker does not limit the minimum [DateTime] the user can pick. @@ -262,7 +261,7 @@ class CupertinoDatePicker extends StatefulWidget { /// maximum time the user can pick if it's set to a date later than that. /// /// [minimumYear] is the minimum year that the picker can be scrolled to in - /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null. + /// [CupertinoDatePickerMode.date] mode. Defaults to 1. /// /// [maximumYear] is the maximum year that the picker can be scrolled to in /// [CupertinoDatePickerMode.date] mode. Null if there's no limit. @@ -331,14 +330,14 @@ class CupertinoDatePicker extends StatefulWidget { ); } - /// The mode of the date picker as one of [CupertinoDatePickerMode]. - /// Defaults to [CupertinoDatePickerMode.dateAndTime]. Cannot be null and - /// value cannot change after initial build. + /// The mode of the date picker as one of [CupertinoDatePickerMode]. Defaults + /// to [CupertinoDatePickerMode.dateAndTime]. Value cannot change after + /// initial build. final CupertinoDatePickerMode mode; /// The initial date and/or time of the picker. Defaults to the present date - /// and time and must not be null. The present must conform to the intervals - /// set in [minimumDate], [maximumDate], [minimumYear], and [maximumYear]. + /// and time. The present must conform to the intervals set in [minimumDate], + /// [maximumDate], [minimumYear], and [maximumYear]. /// /// Changing this value after the initial build will not affect the currently /// selected date time. @@ -375,7 +374,7 @@ class CupertinoDatePicker extends StatefulWidget { final DateTime? maximumDate; /// Minimum year that the picker can be scrolled to in - /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null. + /// [CupertinoDatePickerMode.date] mode. Defaults to 1. final int minimumYear; /// Maximum year that the picker can be scrolled to in @@ -399,8 +398,6 @@ class CupertinoDatePicker extends StatefulWidget { /// Callback called when the selected date and/or time changes. If the new /// selected [DateTime] is not valid, or is not in the [minimumDate] through /// [maximumDate] range, this callback will not be called. - /// - /// Must not be null. final ValueChanged onDateTimeChanged; /// Background color of date picker. @@ -438,8 +435,9 @@ class CupertinoDatePicker extends StatefulWidget { _PickerColumnType columnType, CupertinoLocalizations localizations, BuildContext context, - bool showDayOfWeek - ) { + bool showDayOfWeek, { + bool standaloneMonth = false, + }) { String longestText = ''; switch (columnType) { @@ -491,8 +489,10 @@ class CupertinoDatePicker extends StatefulWidget { } } case _PickerColumnType.month: - for (int i = 1; i <=12; i++) { - final String month = localizations.datePickerMonth(i); + for (int i = 1; i <= 12; i++) { + final String month = standaloneMonth + ? localizations.datePickerStandaloneMonth(i) + : localizations.datePickerMonth(i); if (longestText.length < month.length) { longestText = month; } @@ -1097,8 +1097,7 @@ class _CupertinoDatePickerDateTimeState extends State { final double maxPickerWidth = totalColumnWidths > _kPickerWidth ? totalColumnWidths : _kPickerWidth; - return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + return MediaQuery.withNoTextScaling( child: DefaultTextStyle.merge( style: _kDefaultPickerTextStyle, child: CustomMultiChildLayout( @@ -1281,11 +1280,12 @@ class _CupertinoDatePickerDateState extends State { final int month = index + 1; final bool isInvalidMonth = (widget.minimumDate?.year == selectedYear && widget.minimumDate!.month > month) || (widget.maximumDate?.year == selectedYear && widget.maximumDate!.month < month); + final String monthName = (widget.mode == CupertinoDatePickerMode.monthYear) ? localizations.datePickerStandaloneMonth(month) : localizations.datePickerMonth(month); return itemPositioningBuilder( context, Text( - localizations.datePickerMonth(month), + monthName, style: _themeTextStyle(context, isValid: !isInvalidMonth), ), ); @@ -1487,8 +1487,7 @@ class _CupertinoDatePickerDateState extends State { final double maxPickerWidth = totalColumnWidths > _kPickerWidth ? totalColumnWidths : _kPickerWidth; - return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + return MediaQuery.withNoTextScaling( child: DefaultTextStyle.merge( style: _kDefaultPickerTextStyle, child: CustomMultiChildLayout( @@ -1579,7 +1578,8 @@ class _CupertinoDatePickerMonthYearState extends State { } void _refreshEstimatedColumnWidths() { - estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context, false); + estimatedColumnWidths[_PickerColumnType.month.index] = + CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context, false, standaloneMonth: widget.mode == CupertinoDatePickerMode.monthYear); estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context, false); } @@ -1615,11 +1615,12 @@ class _CupertinoDatePickerMonthYearState extends State { final int month = index + 1; final bool isInvalidMonth = (widget.minimumDate?.year == selectedYear && widget.minimumDate!.month > month) || (widget.maximumDate?.year == selectedYear && widget.maximumDate!.month < month); + final String monthName = (widget.mode == CupertinoDatePickerMode.monthYear) ? localizations.datePickerStandaloneMonth(month) : localizations.datePickerMonth(month); return itemPositioningBuilder( context, Text( - localizations.datePickerMonth(month), + monthName, style: _themeTextStyle(context, isValid: !isInvalidMonth), ), ); @@ -1802,8 +1803,7 @@ class _CupertinoDatePickerMonthYearState extends State { final double maxPickerWidth = totalColumnWidths > _kPickerWidth ? totalColumnWidths : _kPickerWidth; - return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + return MediaQuery.withNoTextScaling( child: DefaultTextStyle.merge( style: _kDefaultPickerTextStyle, child: CustomMultiChildLayout( @@ -1887,7 +1887,7 @@ class CupertinoTimerPicker extends StatefulWidget { /// defaults to [CupertinoTimerPickerMode.hms]. /// /// [onTimerDurationChanged] is the callback called when the selected duration - /// changes and must not be null. + /// changes. /// /// [initialTimerDuration] defaults to 0 second and is limited from 0 second /// to 23 hours 59 minutes 59 seconds. @@ -1937,7 +1937,7 @@ class CupertinoTimerPicker extends StatefulWidget { /// Defines how the timer picker should be positioned within its parent. /// - /// This property must not be null. It defaults to [Alignment.center]. + /// Defaults to [Alignment.center]. final AlignmentGeometry alignment; /// Background color of timer picker. @@ -2502,10 +2502,9 @@ class _CupertinoTimerPickerState extends State { ]; } final CupertinoThemeData themeData = CupertinoTheme.of(context); - return MediaQuery( - // The native iOS picker's text scaling is fixed, so we will also fix it - // as well in our picker. - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + // The native iOS picker's text scaling is fixed, so we will also fix it + // as well in our picker. + return MediaQuery.withNoTextScaling( child: CupertinoTheme( data: themeData.copyWith( textTheme: themeData.textTheme.copyWith( diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart index 20f6d3a8eb23d..07c41c3cea2a9 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart @@ -31,8 +31,6 @@ const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB( /// A button in the style of the Mac context menu buttons. class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { /// Creates an instance of CupertinoDesktopTextSelectionToolbarButton. - /// - /// [child] cannot be null. const CupertinoDesktopTextSelectionToolbarButton({ super.key, required this.onPressed, @@ -51,8 +49,6 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { /// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] from /// the given [ContextMenuButtonItem]. - /// - /// [buttonItem] cannot be null. CupertinoDesktopTextSelectionToolbarButton.buttonItem({ super.key, required ContextMenuButtonItem this.buttonItem, diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index 0cb4cf8adc105..a5b586c5ca6d7 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -149,7 +149,7 @@ const double _kMaxRegularTextScaleFactor = 1.4; // Accessibility mode on iOS is determined by the text scale factor that the // user has selected. bool _isInAccessibilityMode(BuildContext context) { - final double? factor = MediaQuery.maybeTextScaleFactorOf(context); + final double? factor = MediaQuery.maybeTextScalerOf(context)?.textScaleFactor; return factor != null && factor > _kMaxRegularTextScaleFactor; } @@ -188,10 +188,8 @@ bool _isInAccessibilityMode(BuildContext context) { /// * [CupertinoDialogAction], which is an iOS-style dialog button. /// * [AlertDialog], a Material Design alert dialog. /// * -class CupertinoAlertDialog extends StatelessWidget { +class CupertinoAlertDialog extends StatefulWidget { /// Creates an iOS-style alert dialog. - /// - /// The [actions] must not be null. const CupertinoAlertDialog({ super.key, this.title, @@ -233,9 +231,6 @@ class CupertinoAlertDialog extends StatelessWidget { /// section when there are many actions. final ScrollController? scrollController; - ScrollController get _effectiveScrollController => - scrollController ?? ScrollController(); - /// A scroll controller that can be used to control the scrolling of the /// actions in the dialog. /// @@ -247,37 +242,49 @@ class CupertinoAlertDialog extends StatelessWidget { /// section when it is long. final ScrollController? actionScrollController; - ScrollController get _effectiveActionScrollController => - actionScrollController ?? ScrollController(); - /// {@macro flutter.material.dialog.insetAnimationDuration} final Duration insetAnimationDuration; /// {@macro flutter.material.dialog.insetAnimationCurve} final Curve insetAnimationCurve; + @override + State createState() => _CupertinoAlertDialogState(); +} + +class _CupertinoAlertDialogState extends State { + ScrollController? _backupScrollController; + + ScrollController? _backupActionScrollController; + + ScrollController get _effectiveScrollController => + widget.scrollController ?? (_backupScrollController ??= ScrollController()); + + ScrollController get _effectiveActionScrollController => + widget.actionScrollController ?? (_backupActionScrollController ??= ScrollController()); + Widget _buildContent(BuildContext context) { - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); + final double textScaleFactor = MediaQuery.textScalerOf(context).textScaleFactor; final List children = [ - if (title != null || content != null) + if (widget.title != null || widget.content != null) Flexible( flex: 3, child: _CupertinoAlertContentSection( - title: title, - message: content, + title: widget.title, + message: widget.content, scrollController: _effectiveScrollController, titlePadding: EdgeInsets.only( left: _kDialogEdgePadding, right: _kDialogEdgePadding, - bottom: content == null ? _kDialogEdgePadding : 1.0, + bottom: widget.content == null ? _kDialogEdgePadding : 1.0, top: _kDialogEdgePadding * textScaleFactor, ), messagePadding: EdgeInsets.only( left: _kDialogEdgePadding, right: _kDialogEdgePadding, bottom: _kDialogEdgePadding * textScaleFactor, - top: title == null ? _kDialogEdgePadding : 1.0, + top: widget.title == null ? _kDialogEdgePadding : 1.0, ), titleTextStyle: _kCupertinoDialogTitleStyle.copyWith( color: CupertinoDynamicColor.resolve(CupertinoColors.label, context), @@ -303,10 +310,10 @@ class CupertinoAlertDialog extends StatelessWidget { Widget actionSection = Container( height: 0.0, ); - if (actions.isNotEmpty) { + if (widget.actions.isNotEmpty) { actionSection = _CupertinoAlertActionSection( scrollController: _effectiveActionScrollController, - children: actions, + children: widget.actions, ); } @@ -317,14 +324,11 @@ class CupertinoAlertDialog extends StatelessWidget { Widget build(BuildContext context) { final CupertinoLocalizations localizations = CupertinoLocalizations.of(context); final bool isInAccessibilityMode = _isInAccessibilityMode(context); - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); return CupertinoUserInterfaceLevel( data: CupertinoUserInterfaceLevelData.elevated, - child: MediaQuery( - data: MediaQuery.of(context).copyWith( - // iOS does not shrink dialog content below a 1.0 scale factor - textScaleFactor: math.max(textScaleFactor, 1.0), - ), + child: MediaQuery.withClampedTextScaling( + // iOS does not shrink dialog content below a 1.0 scale factor + minScaleFactor: 1.0, child: ScrollConfiguration( // A CupertinoScrollbar is built-in below. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), @@ -333,8 +337,8 @@ class CupertinoAlertDialog extends StatelessWidget { return AnimatedPadding( padding: MediaQuery.viewInsetsOf(context) + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), - duration: insetAnimationDuration, - curve: insetAnimationCurve, + duration: widget.insetAnimationDuration, + curve: widget.insetAnimationCurve, child: MediaQuery.removeViewInsets( removeLeft: true, removeTop: true, @@ -371,6 +375,13 @@ class CupertinoAlertDialog extends StatelessWidget { ), ); } + + @override + void dispose() { + _backupScrollController?.dispose(); + _backupActionScrollController?.dispose(); + super.dispose(); + } } /// Rounded rectangle surface that looks like an iOS popup surface, e.g., alert dialog @@ -461,7 +472,7 @@ class CupertinoPopupSurface extends StatelessWidget { /// /// * [CupertinoActionSheetAction], which is an iOS-style action sheet button. /// * -class CupertinoActionSheet extends StatelessWidget { +class CupertinoActionSheet extends StatefulWidget { /// Creates an iOS-style action sheet. /// /// An action sheet must have a non-null value for at least one of the @@ -507,30 +518,46 @@ class CupertinoActionSheet extends StatelessWidget { /// short. final ScrollController? messageScrollController; - ScrollController get _effectiveMessageScrollController => - messageScrollController ?? ScrollController(); - /// A scroll controller that can be used to control the scrolling of the /// [actions] in the action sheet. /// /// This attribute is typically not needed. final ScrollController? actionScrollController; - ScrollController get _effectiveActionScrollController => - actionScrollController ?? ScrollController(); - /// The optional cancel button that is grouped separately from the other /// actions. /// /// Typically this is an [CupertinoActionSheetAction] widget. final Widget? cancelButton; + @override + State createState() => _CupertinoActionSheetState(); +} + +class _CupertinoActionSheetState extends State { + ScrollController? _backupMessageScrollController; + + ScrollController? _backupActionScrollController; + + ScrollController get _effectiveMessageScrollController => + widget.messageScrollController ?? (_backupMessageScrollController ??= ScrollController()); + + ScrollController get _effectiveActionScrollController => + widget.actionScrollController ?? (_backupActionScrollController ??= ScrollController()); + + @override + void dispose() { + _backupMessageScrollController?.dispose(); + _backupActionScrollController?.dispose(); + super.dispose(); + } + Widget _buildContent(BuildContext context) { final List content = []; - if (title != null || message != null) { + if (widget.title != null || widget.message != null) { final Widget titleSection = _CupertinoAlertContentSection( - title: title, - message: message, + title: widget.title, + message: widget.message, scrollController: _effectiveMessageScrollController, titlePadding: const EdgeInsets.only( left: _kActionSheetContentHorizontalPadding, @@ -541,13 +568,13 @@ class CupertinoActionSheet extends StatelessWidget { messagePadding: EdgeInsets.only( left: _kActionSheetContentHorizontalPadding, right: _kActionSheetContentHorizontalPadding, - bottom: title == null ? _kActionSheetContentVerticalPadding : 22.0, - top: title == null ? _kActionSheetContentVerticalPadding : 0.0, + bottom: widget.title == null ? _kActionSheetContentVerticalPadding : 22.0, + top: widget.title == null ? _kActionSheetContentVerticalPadding : 0.0, ), - titleTextStyle: message == null + titleTextStyle: widget.message == null ? _kActionSheetContentStyle : _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600), - messageTextStyle: title == null + messageTextStyle: widget.title == null ? _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600) : _kActionSheetContentStyle, additionalPaddingBetweenTitleAndMessage: const EdgeInsets.only(top: 8.0), @@ -566,26 +593,26 @@ class CupertinoActionSheet extends StatelessWidget { } Widget _buildActions() { - if (actions == null || actions!.isEmpty) { + if (widget.actions == null || widget.actions!.isEmpty) { return Container( height: 0.0, ); } return _CupertinoAlertActionSection( scrollController: _effectiveActionScrollController, - hasCancelButton: cancelButton != null, + hasCancelButton: widget.cancelButton != null, isActionSheet: true, - children: actions!, + children: widget.actions!, ); } Widget _buildCancelButton() { - final double cancelPadding = (actions != null || message != null || title != null) + final double cancelPadding = (widget.actions != null || widget.message != null || widget.title != null) ? _kActionSheetCancelButtonPadding : 0.0; return Padding( padding: EdgeInsets.only(top: cancelPadding), child: _CupertinoActionSheetCancelButton( - child: cancelButton, + child: widget.cancelButton, ), ); } @@ -608,7 +635,7 @@ class CupertinoActionSheet extends StatelessWidget { ), ), ), - if (cancelButton != null) _buildCancelButton(), + if (widget.cancelButton != null) _buildCancelButton(), ]; final Orientation orientation = MediaQuery.orientationOf(context); @@ -657,8 +684,6 @@ class CupertinoActionSheet extends StatelessWidget { /// more choices related to the current context. class CupertinoActionSheetAction extends StatelessWidget { /// Creates an action for an iOS-style action sheet. - /// - /// The [child] and [onPressed] arguments must not be null. const CupertinoActionSheetAction({ super.key, required this.onPressed, @@ -668,8 +693,6 @@ class CupertinoActionSheetAction extends StatelessWidget { }); /// The callback that is called when the button is tapped. - /// - /// This attribute must not be null. final VoidCallback onPressed; /// Whether this action is the default choice in the action sheet. @@ -1564,8 +1587,7 @@ class _ActionButtonParentDataWidget } @override - Type get debugTypicalAncestorWidgetClass => - _CupertinoDialogActionsRenderWidget; + Type get debugTypicalAncestorWidgetClass => _CupertinoDialogActionsRenderWidget; } // ParentData applied to individual action buttons that report whether or not @@ -1604,14 +1626,14 @@ class CupertinoDialogAction extends StatelessWidget { /// but more than one action can have this attribute set to true in the same /// [CupertinoAlertDialog]. /// - /// This parameters defaults to false and cannot be null. + /// This parameters defaults to false. final bool isDefaultAction; /// Whether this action destroys an object. /// /// For example, an action that deletes an email is destructive. /// - /// Defaults to false and cannot be null. + /// Defaults to false. final bool isDestructiveAction; /// [TextStyle] to apply to any text that appears in this button. @@ -1633,7 +1655,7 @@ class CupertinoDialogAction extends StatelessWidget { bool get enabled => onPressed != null; double _calculatePadding(BuildContext context) { - return 8.0 * MediaQuery.textScaleFactorOf(context); + return 8.0 * MediaQuery.textScalerOf(context).textScaleFactor; } // Dialog action content shrinks to fit, up to a certain point, and if it still @@ -1649,12 +1671,11 @@ class CupertinoDialogAction extends StatelessWidget { final double dialogWidth = isInAccessibilityMode ? _kAccessibilityCupertinoDialogWidth : _kCupertinoDialogWidth; - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); // The fontSizeRatio is the ratio of the current text size (including any // iOS scale factor) vs the minimum text size that we allow in action // buttons. This ratio information is used to automatically scale down action // button text to fit the available space. - final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kDialogMinButtonFontSize; + final double fontSizeRatio = MediaQuery.textScalerOf(context).scale(textStyle.fontSize!) / _kDialogMinButtonFontSize; final double padding = _calculatePadding(context); return IntrinsicHeight( diff --git a/packages/flutter/lib/src/cupertino/form_section.dart b/packages/flutter/lib/src/cupertino/form_section.dart index 6bd9fd4c10455..a381775ff9b63 100644 --- a/packages/flutter/lib/src/cupertino/form_section.dart +++ b/packages/flutter/lib/src/cupertino/form_section.dart @@ -193,7 +193,7 @@ class CupertinoFormSection extends StatelessWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; @override diff --git a/packages/flutter/lib/src/cupertino/localizations.dart b/packages/flutter/lib/src/cupertino/localizations.dart index 477fda7567db0..2262d55f45b71 100644 --- a/packages/flutter/lib/src/cupertino/localizations.dart +++ b/packages/flutter/lib/src/cupertino/localizations.dart @@ -57,7 +57,6 @@ enum DatePickerDateOrder { /// /// * [DefaultCupertinoLocalizations], the default, English-only, implementation /// of this interface. -// TODO(xster): Supply non-english strings. abstract class CupertinoLocalizations { /// Year that is shown in [CupertinoDatePicker] spinner corresponding to the /// given year index. @@ -76,9 +75,25 @@ abstract class CupertinoLocalizations { /// /// - US English: January /// - Korean: 1월 + /// - Russian: января // The global version uses date symbols data from the intl package. String datePickerMonth(int monthIndex); + /// Month that is shown in [CupertinoDatePicker] spinner corresponding to + /// the given month index in [CupertinoDatePickerMode.monthYear] mode. + /// + /// This is distinct from [datePickerMonth] because in some languages, like Russian, + /// the name of a month takes a different form depending + /// on whether it is preceded by a day or whether it stands alone. + /// + /// Examples: datePickerMonth(1) in: + /// + /// - US English: January + /// - Korean: 1월 + /// - Russian: Январь + // The global version uses date symbols data from the intl package. + String datePickerStandaloneMonth(int monthIndex); + /// Day of month that is shown in [CupertinoDatePicker] spinner corresponding /// to the given day index. /// @@ -245,6 +260,18 @@ abstract class CupertinoLocalizations { // The global version uses the translated string from the arb file. String get selectAllButtonLabel; + /// The term used for looking up a selection. + // The global version uses the translated string from the arb file. + String get lookUpButtonLabel; + + /// The term used for launching a web search on a selection. + // The global version uses the translated string from the arb file. + String get searchWebButtonLabel; + + /// The term used for launching a web search on a selection. + // The global version uses the translated string from the arb file. + String get shareButtonLabel; + /// The default placeholder used in [CupertinoSearchTextField]. // The global version uses the translated string from the arb file. String get searchTextFieldPlaceholderLabel; @@ -256,6 +283,10 @@ abstract class CupertinoLocalizations { /// user interaction with elements behind it. String get modalBarrierDismissLabel; + /// Label read out by accessibility tools (VoiceOver) for a context menu to + /// indicate that a tap outside dismisses the context menu. + String get menuDismissLabel; + /// The `CupertinoLocalizations` from the closest [Localizations] instance /// that encloses the given context. /// @@ -351,6 +382,9 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations { @override String datePickerMonth(int monthIndex) => _months[monthIndex - 1]; + @override + String datePickerStandaloneMonth(int monthIndex) => _months[monthIndex - 1]; + @override String datePickerDayOfMonth(int dayIndex, [int? weekDay]) { if (weekDay != null) { @@ -451,12 +485,24 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations { @override String get selectAllButtonLabel => 'Select All'; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get searchWebButtonLabel => 'Search Web'; + + @override + String get shareButtonLabel => 'Share...'; + @override String get searchTextFieldPlaceholderLabel => 'Search'; @override String get modalBarrierDismissLabel => 'Dismiss'; + @override + String get menuDismissLabel => 'Dismiss menu'; + /// Creates an object that provides US English resource values for the /// cupertino library widgets. /// diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index f33ea8d95c99f..665da8b7257cf 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -229,12 +229,9 @@ bool _isTransitionable(BuildContext context) { /// behavior for multiple navigation bars per route. /// /// When used in a [CupertinoPageScaffold], [CupertinoPageScaffold.navigationBar] -/// has its text scale factor set to 1.0 and does not respond to text scale factor -/// changes from the operating system, to match the native iOS behavior. To override -/// this behavior, wrap each of the `navigationBar`'s components inside a [MediaQuery] -/// with the desired [MediaQueryData.textScaleFactor] value. The text scale factor -/// value from the operating system can be retrieved in many ways, such as querying -/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext]. +/// disables text scaling to match the native iOS behavior. To override +/// this behavior, wrap each of the `navigationBar`'s components inside a +/// [MediaQuery] with the desired [TextScaler]. /// /// {@tool dartpad} /// This example shows a [CupertinoNavigationBar] placed in a [CupertinoPageScaffold]. @@ -296,8 +293,6 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// 3. Show a back chevron with the previous route's `title` if the current /// route is a [CupertinoPageRoute] and the previous route is also a /// [CupertinoPageRoute]. - /// - /// This value cannot be null. /// {@endtemplate} final bool automaticallyImplyLeading; @@ -306,8 +301,6 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// If true and [middle] is null, automatically fill in a [Text] widget with /// the current route's `title` if the route is a [CupertinoPageRoute]. /// If [middle] widget is not null, this parameter has no effect. - /// - /// This value cannot be null. final bool automaticallyImplyMiddle; /// {@template flutter.cupertino.CupertinoNavigationBar.previousPageTitle} @@ -398,7 +391,7 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// When set to true, only one navigation bar can be present per route unless /// [heroTag] is also set. /// - /// This value defaults to true and cannot be null. + /// This value defaults to true. /// {@endtemplate} final bool transitionBetweenRoutes; @@ -414,8 +407,8 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// navigation bars per route or to transition between multiple /// [Navigator]s. /// - /// Cannot be null. To disable Hero transitions for this navigation bar, - /// set [transitionBetweenRoutes] to false. + /// To disable Hero transitions for this navigation bar, set + /// [transitionBetweenRoutes] to false. /// {@endtemplate} final Object heroTag; @@ -555,13 +548,10 @@ class _CupertinoNavigationBarState extends State { /// Use [transitionBetweenRoutes] or [heroTag] to customize the transition /// behavior for multiple navigation bars per route. /// -/// [CupertinoSliverNavigationBar] has its text scale factor set to 1.0 by default -/// and does not respond to text scale factor changes from the operating system, -/// to match the native iOS behavior. To override this behavior, wrap each of the +/// [CupertinoSliverNavigationBar] by default disables text scaling to match the +/// native iOS behavior. To override this behavior, wrap each of the /// [CupertinoSliverNavigationBar]'s components inside a [MediaQuery] with the -/// desired [MediaQueryData.textScaleFactor] value. The text scale factor value -/// from the operating system can be retrieved in many ways, such as querying -/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext]. +/// desired [TextScaler]. /// /// The [stretch] parameter determines whether the nav bar should stretch to /// fill the over-scroll area. The nav bar can still expand and contract as the @@ -583,7 +573,8 @@ class _CupertinoNavigationBarState extends State { class CupertinoSliverNavigationBar extends StatefulWidget { /// Creates a navigation bar for scrolling lists. /// - /// The [largeTitle] argument is required and must not be null. + /// If [automaticallyImplyTitle] is false, then the [largeTitle] argument is + /// required. const CupertinoSliverNavigationBar({ super.key, this.largeTitle, @@ -644,8 +635,6 @@ class CupertinoSliverNavigationBar extends StatefulWidget { /// If true and [largeTitle] is null, automatically fill in a [Text] widget /// with the current route's `title` if the route is a [CupertinoPageRoute]. /// If [largeTitle] widget is not null, this parameter has no effect. - /// - /// This value cannot be null. final bool automaticallyImplyTitle; /// Controls whether [middle] widget should always be visible (even in @@ -741,8 +730,7 @@ class _CupertinoSliverNavigationBarState extends State { top: 0.0, left: 0.0, right: 0.0, - child: MediaQuery( - data: existingMediaQuery.copyWith(textScaleFactor: 1), + child: MediaQuery.withNoTextScaling( child: widget.navigationBar!, ), ), diff --git a/packages/flutter/lib/src/cupertino/picker.dart b/packages/flutter/lib/src/cupertino/picker.dart index 11f032b449749..ecbce297b9115 100644 --- a/packages/flutter/lib/src/cupertino/picker.dart +++ b/packages/flutter/lib/src/cupertino/picker.dart @@ -53,8 +53,7 @@ const double _kOverAndUnderCenterOpacity = 0.447; class CupertinoPicker extends StatefulWidget { /// Creates a picker from a concrete list of children. /// - /// The [diameterRatio] and [itemExtent] arguments must not be null. The - /// [itemExtent] must be greater than zero. + /// The [itemExtent] must be greater than zero. /// /// The [backgroundColor] defaults to null, which disables background painting entirely. /// (i.e. the picker is going to have a completely transparent background), to match @@ -99,11 +98,11 @@ class CupertinoPicker extends StatefulWidget { /// normally the builder is only called once for each index (except when /// rebuilding - the cache is cleared). /// - /// The [itemBuilder] argument must not be null. The [childCount] argument - /// reflects the number of children that will be provided by the [itemBuilder]. + /// The [childCount] argument reflects the number of children that will be + /// provided by the [itemBuilder]. /// {@macro flutter.widgets.ListWheelChildBuilderDelegate.childCount} /// - /// The [itemExtent] argument must be non-null and positive. + /// The [itemExtent] argument must be positive. /// /// The [backgroundColor] defaults to null, which disables background painting entirely. /// (i.e. the picker is going to have a completely transparent background), to match @@ -134,7 +133,7 @@ class CupertinoPicker extends StatefulWidget { /// /// For more details, see [ListWheelScrollView.diameterRatio]. /// - /// Must not be null and defaults to `1.1` to visually mimic iOS. + /// Defaults to 1.1 to visually mimic iOS. final double diameterRatio; /// Background color behind the children. @@ -334,13 +333,12 @@ class CupertinoPickerDefaultSelectionOverlay extends StatelessWidget { /// area (or the currently selected item, depending on how you described it /// elsewhere) of a [CupertinoPicker]. /// - /// The [background] argument default value is [CupertinoColors.tertiarySystemFill]. - /// It must be non-null. + /// The [background] argument default value is + /// [CupertinoColors.tertiarySystemFill]. /// /// The [capStartEdge] and [capEndEdge] arguments decide whether to add a /// default margin and use rounded corners on the left and right side of the - /// rectangular overlay. - /// Default to true and must not be null. + /// rectangular overlay, and they both default to true. const CupertinoPickerDefaultSelectionOverlay({ super.key, this.background = CupertinoColors.tertiarySystemFill, diff --git a/packages/flutter/lib/src/cupertino/refresh.dart b/packages/flutter/lib/src/cupertino/refresh.dart index 4d32e61484238..c4e1852e610d1 100644 --- a/packages/flutter/lib/src/cupertino/refresh.dart +++ b/packages/flutter/lib/src/cupertino/refresh.dart @@ -283,7 +283,7 @@ class CupertinoSliverRefreshControl extends StatefulWidget { /// Create a new refresh control for inserting into a list of slivers. /// /// The [refreshTriggerPullDistance] and [refreshIndicatorExtent] arguments - /// must not be null and must be >= 0. + /// must be greater than or equal to 0. /// /// The [builder] argument may be null, in which case no indicator UI will be /// shown but the [onRefresh] will still be invoked. By default, [builder] @@ -307,8 +307,8 @@ class CupertinoSliverRefreshControl extends StatefulWidget { /// The amount of overscroll the scrollable must be dragged to trigger a reload. /// - /// Must not be null, must be larger than 0.0 and larger than - /// [refreshIndicatorExtent]. Defaults to 100px when not specified. + /// Must be larger than zero and larger than [refreshIndicatorExtent]. + /// Defaults to 100 pixels when not specified. /// /// When overscrolled past this distance, [onRefresh] will be called if not /// null and the [builder] will build in the [RefreshIndicatorMode.armed] state. @@ -317,9 +317,9 @@ class CupertinoSliverRefreshControl extends StatefulWidget { /// The amount of space the refresh indicator sliver will keep holding while /// [onRefresh]'s [Future] is still running. /// - /// Must not be null and must be positive, but can be 0.0, in which case the - /// sliver will start retracting back to 0.0 as soon as the refresh is started. - /// Defaults to 60px when not specified. + /// Must be a positive number, but can be zero, in which case the sliver will + /// start retracting back to zero as soon as the refresh is started. Defaults + /// to 60 pixels when not specified. /// /// Must be smaller than [refreshTriggerPullDistance], since the sliver /// shouldn't grow further after triggering the refresh. diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index 1c3ad869f8c9c..d0c6be160d60a 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -121,6 +121,12 @@ mixin CupertinoRouteTransitionMixin on PageRoute { return _previousTitle!; } + @override + void dispose() { + _previousTitle?.dispose(); + super.dispose(); + } + @override void didChangePrevious(Route? previousRoute) { final String? previousTitleString = previousRoute is CupertinoRouteTransitionMixin @@ -196,7 +202,8 @@ mixin CupertinoRouteTransitionMixin on PageRoute { } // If attempts to dismiss this route might be vetoed such as in a page // with forms, then do not allow the user to dismiss the route with a swipe. - if (route.hasScopedWillPopCallback) { + if (route.hasScopedWillPopCallback + || route.popDisposition == RoutePopDisposition.doNotPop) { return false; } // Fullscreen dialogs aren't dismissible by back swipe. @@ -310,6 +317,9 @@ mixin CupertinoRouteTransitionMixin on PageRoute { /// the route is popped from the stack via [Navigator.pop] when an optional /// `result` can be provided. /// +/// If `barrierDismissible` is true, then pressing the escape key on the keyboard +/// will cause the current route to be popped with null as the value. +/// /// See also: /// /// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition @@ -333,6 +343,7 @@ class CupertinoPageRoute extends PageRoute with CupertinoRouteTransitionMi this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, + super.barrierDismissible = false, }) { assert(opaque); } @@ -702,8 +713,6 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureD /// detector controller is associated. class _CupertinoBackGestureController { /// Creates a controller for an iOS-style back gesture. - /// - /// The [navigator] and [controller] arguments must not be null. _CupertinoBackGestureController({ required this.navigator, required this.controller, @@ -724,8 +733,6 @@ class _CupertinoBackGestureController { /// [fractionalVelocity] as a fraction of screen width per second. void dragEnd(double velocity) { // Fling in the appropriate direction. - // AnimationController.fling is guaranteed to - // take at least one frame. // // This curve has been determined through rigorously eyeballing native iOS // animations. @@ -836,16 +843,16 @@ class _CupertinoEdgeShadowDecoration extends Decoration { return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map((Color color) => Color.lerp(null, color, t)!).toList()); } if (b == null) { - return a._colors == null ? a : _CupertinoEdgeShadowDecoration._(a._colors!.map((Color color) => Color.lerp(null, color, 1.0 - t)!).toList()); + return a._colors == null ? a : _CupertinoEdgeShadowDecoration._(a._colors.map((Color color) => Color.lerp(null, color, 1.0 - t)!).toList()); } assert(b._colors != null || a._colors != null); // If it ever becomes necessary, we could allow decorations with different // length' here, similarly to how it is handled in [LinearGradient.lerp]. - assert(b._colors == null || a._colors == null || a._colors!.length == b._colors!.length); + assert(b._colors == null || a._colors == null || a._colors.length == b._colors.length); return _CupertinoEdgeShadowDecoration._( [ for (int i = 0; i < b._colors!.length; i += 1) - Color.lerp(a._colors?[i], b._colors?[i], t)!, + Color.lerp(a._colors?[i], b._colors[i], t)!, ], ); } @@ -895,7 +902,7 @@ class _CupertinoEdgeShadowPainter extends BoxPainter { _CupertinoEdgeShadowPainter( this._decoration, super.onChanged, - ) : assert(_decoration._colors == null || _decoration._colors!.length > 1); + ) : assert(_decoration._colors == null || _decoration._colors.length > 1); final _CupertinoEdgeShadowDecoration _decoration; diff --git a/packages/flutter/lib/src/cupertino/search_field.dart b/packages/flutter/lib/src/cupertino/search_field.dart index ac868801bc08f..f8ec3713dcc1d 100644 --- a/packages/flutter/lib/src/cupertino/search_field.dart +++ b/packages/flutter/lib/src/cupertino/search_field.dart @@ -197,51 +197,51 @@ class CupertinoSearchTextField extends StatefulWidget { /// Sets the padding insets for the text and placeholder. /// - /// Cannot be null. Defaults to padding that replicates the - /// `UISearchTextField` look. The inset values were determined using the - /// comparison tool in https://github.com/flutter/platform_tests/. + /// Defaults to padding that replicates the `UISearchTextField` look. The + /// inset values were determined using the comparison tool in + /// https://github.com/flutter/platform_tests/. final EdgeInsetsGeometry padding; /// Sets the color for the suffix and prefix icons. /// - /// Cannot be null. Defaults to [CupertinoColors.secondaryLabel]. + /// Defaults to [CupertinoColors.secondaryLabel]. final Color itemColor; /// Sets the base icon size for the suffix and prefix icons. /// - /// Cannot be null. The size of the icon is scaled using the accessibility - /// font scale settings. Defaults to `20.0`. + /// The size of the icon is scaled using the accessibility font scale + /// settings. Defaults to `20.0`. final double itemSize; /// Sets the padding insets for the suffix. /// - /// Cannot be null. Defaults to padding that replicates the - /// `UISearchTextField` suffix look. The inset values were determined using - /// the comparison tool in https://github.com/flutter/platform_tests/. + /// Defaults to padding that replicates the `UISearchTextField` suffix look. + /// The inset values were determined using the comparison tool in + /// https://github.com/flutter/platform_tests/. final EdgeInsetsGeometry prefixInsets; /// Sets a prefix widget. /// - /// Cannot be null. Defaults to an [Icon] widget with the [CupertinoIcons.search] icon. + /// Defaults to an [Icon] widget with the [CupertinoIcons.search] icon. final Widget prefixIcon; /// Sets the padding insets for the prefix. /// - /// Cannot be null. Defaults to padding that replicates the - /// `UISearchTextField` prefix look. The inset values were determined using - /// the comparison tool in https://github.com/flutter/platform_tests/. + /// Defaults to padding that replicates the `UISearchTextField` prefix look. + /// The inset values were determined using the comparison tool in + /// https://github.com/flutter/platform_tests/. final EdgeInsetsGeometry suffixInsets; /// Sets the suffix widget's icon. /// - /// Cannot be null. Defaults to the X-Mark [CupertinoIcons.xmark_circle_fill]. - /// "To change the functionality of the suffix icon, provide a custom - /// onSuffixTap callback and specify an intuitive suffixIcon. + /// Defaults to the X-Mark [CupertinoIcons.xmark_circle_fill]. "To change the + /// functionality of the suffix icon, provide a custom onSuffixTap callback + /// and specify an intuitive suffixIcon. final Icon suffixIcon; /// Dictates when the X-Mark (suffix) should be visible. /// - /// Cannot be null. Defaults to only on when editing. + /// Defaults to only on when editing. final OverlayVisibilityMode suffixMode; /// Sets the X-Mark (suffix) action. @@ -400,8 +400,7 @@ class _CupertinoSearchTextFieldState extends State // The icon size will be scaled by a factor of the accessibility text scale, // to follow the behavior of `UISearchTextField`. - final double scaledIconSize = - MediaQuery.textScaleFactorOf(context) * widget.itemSize; + final double scaledIconSize = MediaQuery.textScalerOf(context).textScaleFactor * widget.itemSize; // If decoration was not provided, create a decoration with the provided // background color and border radius. @@ -445,7 +444,7 @@ class _CupertinoSearchTextFieldState extends State suffix: suffix, keyboardType: widget.keyboardType, onTap: widget.onTap, - enabled: widget.enabled, + enabled: widget.enabled ?? true, suffixMode: widget.suffixMode, placeholder: placeholder, placeholderStyle: placeholderStyle, diff --git a/packages/flutter/lib/src/cupertino/segmented_control.dart b/packages/flutter/lib/src/cupertino/segmented_control.dart index e1012fa8f54d7..dcb0cde6604a5 100644 --- a/packages/flutter/lib/src/cupertino/segmented_control.dart +++ b/packages/flutter/lib/src/cupertino/segmented_control.dart @@ -76,9 +76,9 @@ const Duration _kFadeDuration = Duration(milliseconds: 165); class CupertinoSegmentedControl extends StatefulWidget { /// Creates an iOS-style segmented control bar. /// - /// The [children] and [onValueChanged] arguments must not be null. The - /// [children] argument must be an ordered [Map] such as a [LinkedHashMap]. - /// Further, the length of the [children] list must be greater than one. + /// The [children] argument must be an ordered [Map] such as a + /// [LinkedHashMap]. Further, the length of the [children] list must be + /// greater than one. /// /// Each widget value in the map of [children] must have an associated key /// that uniquely identifies this widget. This key is what will be returned @@ -120,8 +120,6 @@ class CupertinoSegmentedControl extends StatefulWidget { /// The callback that is called when a new option is tapped. /// - /// This attribute must not be null. - /// /// The segmented control passes the newly selected widget's associated key /// to the callback but does not actually change state until the parent /// widget rebuilds the segmented control with the new [groupValue]. diff --git a/packages/flutter/lib/src/cupertino/slider.dart b/packages/flutter/lib/src/cupertino/slider.dart index 006a962514504..50a6d29e4bc8a 100644 --- a/packages/flutter/lib/src/cupertino/slider.dart +++ b/packages/flutter/lib/src/cupertino/slider.dart @@ -202,8 +202,6 @@ class CupertinoSlider extends StatefulWidget { /// The color to use for the thumb of the slider. /// - /// Thumb color must not be null. - /// /// Defaults to [CupertinoColors.white]. final Color thumbColor; diff --git a/packages/flutter/lib/src/cupertino/sliding_segmented_control.dart b/packages/flutter/lib/src/cupertino/sliding_segmented_control.dart index 2a9d12d3a0266..884e86d5ab967 100644 --- a/packages/flutter/lib/src/cupertino/sliding_segmented_control.dart +++ b/packages/flutter/lib/src/cupertino/sliding_segmented_control.dart @@ -299,9 +299,9 @@ class _SegmentSeparatorState extends State<_SegmentSeparator> with TickerProvide class CupertinoSlidingSegmentedControl extends StatefulWidget { /// Creates an iOS-style segmented control bar. /// - /// The [children] and [onValueChanged] arguments must not be null. The - /// [children] argument must be an ordered [Map] such as a [LinkedHashMap]. - /// Further, the length of the [children] list must be greater than one. + /// The [children] argument must be an ordered [Map] such as a + /// [LinkedHashMap]. Further, the length of the [children] list must be + /// greater than one. /// /// Each widget value in the map of [children] must have an associated key /// that uniquely identifies this widget. This key is what will be returned @@ -343,8 +343,6 @@ class CupertinoSlidingSegmentedControl extends StatefulWidget { /// The callback that is called when a new option is tapped. /// - /// This attribute must not be null. - /// /// The segmented control passes the newly selected widget's associated key /// to the callback but does not actually change state until the parent /// widget rebuilds the segmented control with the new [groupValue]. @@ -403,7 +401,7 @@ class CupertinoSlidingSegmentedControl extends StatefulWidget { /// The amount of space by which to inset the [children]. /// - /// Must not be null. Defaults to EdgeInsets.symmetric(vertical: 2, horizontal: 3). + /// Defaults to `EdgeInsets.symmetric(vertical: 2, horizontal: 3)`. final EdgeInsetsGeometry padding; @override diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart index 2057e115a214f..0e76ace69325b 100644 --- a/packages/flutter/lib/src/cupertino/switch.dart +++ b/packages/flutter/lib/src/cupertino/switch.dart @@ -64,8 +64,7 @@ import 'thumb_painter.dart'; class CupertinoSwitch extends StatefulWidget { /// Creates an iOS-style switch. /// - /// The [value] parameter must not be null. - /// The [dragStartBehavior] parameter defaults to [DragStartBehavior.start] and must not be null. + /// The [dragStartBehavior] parameter defaults to [DragStartBehavior.start]. const CupertinoSwitch({ super.key, required this.value, @@ -82,8 +81,6 @@ class CupertinoSwitch extends StatefulWidget { }); /// Whether this switch is on or off. - /// - /// Must not be null. final bool value; /// Called when the user toggles with switch on or off. diff --git a/packages/flutter/lib/src/cupertino/tab_scaffold.dart b/packages/flutter/lib/src/cupertino/tab_scaffold.dart index 056cf18ef967a..8995abfe5d4d2 100644 --- a/packages/flutter/lib/src/cupertino/tab_scaffold.dart +++ b/packages/flutter/lib/src/cupertino/tab_scaffold.dart @@ -32,8 +32,8 @@ class CupertinoTabController extends ChangeNotifier { /// Creates a [CupertinoTabController] to control the tab index of [CupertinoTabScaffold] /// and [CupertinoTabBar]. /// - /// The [initialIndex] must not be null and defaults to 0. The value must be - /// greater than or equal to 0, and less than the total number of tabs. + /// The [initialIndex] defaults to 0. The value must be greater than or equal + /// to 0, and less than the total number of tabs. CupertinoTabController({ int initialIndex = 0 }) : _index = initialIndex, assert(initialIndex >= 0); @@ -123,8 +123,6 @@ class CupertinoTabController extends ChangeNotifier { /// * [iOS human interface guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/bars/tab-bars/). class CupertinoTabScaffold extends StatefulWidget { /// Creates a layout for applications with a tab bar at the bottom. - /// - /// The [tabBar] and [tabBuilder] arguments must not be null. CupertinoTabScaffold({ super.key, required this.tabBar, @@ -158,15 +156,9 @@ class CupertinoTabScaffold extends StatefulWidget { /// If translucent, the main content may slide behind it. /// Otherwise, the main content's bottom margin will be offset by its height. /// - /// By default [tabBar] has its text scale factor set to 1.0 and does not - /// respond to text scale factor changes from the operating system, to match - /// the native iOS behavior. To override this behavior, wrap each of the [tabBar]'s - /// items inside a [MediaQuery] with the desired [MediaQueryData.textScaleFactor] - /// value. The text scale factor value from the operating system can be retrieved - /// int many ways, such as querying [MediaQuery.textScaleFactorOf] against - /// [CupertinoApp]'s [BuildContext]. - /// - /// Must not be null. + /// By default [tabBar] disables text scaling to match the native iOS behavior. + /// To override this behavior, wrap each of the [tabBar]'s items inside a + /// [MediaQuery] with the desired [TextScaler]. final CupertinoTabBar tabBar; /// Controls the currently selected tab index of the [tabBar], as well as the @@ -190,8 +182,6 @@ class CupertinoTabScaffold extends StatefulWidget { /// In that case, the child's [BuildContext]'s [MediaQuery] will have a /// bottom padding indicating the area of obstructing overlap from the /// [tabBar]. - /// - /// Must not be null. final IndexedWidgetBuilder tabBuilder; /// The color of the widget that underlies the entire scaffold. @@ -205,7 +195,7 @@ class CupertinoTabScaffold extends StatefulWidget { /// scaffold, the body can be resized to avoid overlapping the keyboard, which /// prevents widgets inside the body from being obscured by the keyboard. /// - /// Defaults to true and cannot be null. + /// Defaults to true. final bool resizeToAvoidBottomInset; /// Restoration ID to save and restore the state of the [CupertinoTabScaffold]. @@ -361,8 +351,7 @@ class _CupertinoTabScaffoldState extends State with Restor children: [ // The main content being at the bottom is added to the stack first. content, - MediaQuery( - data: existingMediaQuery.copyWith(textScaleFactor: 1), + MediaQuery.withNoTextScaling( child: Align( alignment: Alignment.bottomCenter, // Override the tab bar's currentIndex to the current tab and hook in @@ -519,8 +508,8 @@ class RestorableCupertinoTabController extends RestorableChangeNotifier= 0), _initialIndex = initialIndex; diff --git a/packages/flutter/lib/src/cupertino/tab_view.dart b/packages/flutter/lib/src/cupertino/tab_view.dart index 8728196eee9b8..f41d0a4a3175d 100644 --- a/packages/flutter/lib/src/cupertino/tab_view.dart +++ b/packages/flutter/lib/src/cupertino/tab_view.dart @@ -162,15 +162,39 @@ class _CupertinoTabViewState extends State { ..add(_heroController); } + GlobalKey? _ownedNavigatorKey; + GlobalKey get _navigatorKey { + if (widget.navigatorKey != null) { + return widget.navigatorKey!; + } + _ownedNavigatorKey ??= GlobalKey(); + return _ownedNavigatorKey!; + } + + // Whether this tab is currently the active tab. + bool get _isActive => TickerMode.of(context); + @override Widget build(BuildContext context) { - return Navigator( - key: widget.navigatorKey, + final Widget child = Navigator( + key: _navigatorKey, onGenerateRoute: _onGenerateRoute, onUnknownRoute: _onUnknownRoute, observers: _navigatorObservers, restorationScopeId: widget.restorationScopeId, ); + + // Handle system back gestures only if the tab is currently active. + return NavigatorPopHandler( + enabled: _isActive, + onPop: () { + if (!_isActive) { + return; + } + _navigatorKey.currentState!.pop(); + }, + child: child, + ); } Route? _onGenerateRoute(RouteSettings settings) { diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 4c4b940fcef6d..824f3cb026219 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -194,8 +194,7 @@ class CupertinoTextField extends StatefulWidget { /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default - /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and - /// must not be null. + /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively. /// /// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior], /// [expands], [obscureText], [prefixMode], [readOnly], [scrollPadding], @@ -261,7 +260,7 @@ class CupertinoTextField extends StatefulWidget { this.onSubmitted, this.onTapOutside, this.inputFormatters, - this.enabled, + this.enabled = true, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), @@ -332,13 +331,7 @@ class CupertinoTextField extends StatefulWidget { /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default - /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and - /// must not be null. - /// - /// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior], - /// [expands], [obscureText], [prefixMode], [readOnly], [scrollPadding], - /// [suffixMode], [textAlign], [selectionHeightStyle], [selectionWidthStyle], - /// and [enableSuggestions] properties must not be null. + /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively. /// /// See also: /// @@ -393,7 +386,7 @@ class CupertinoTextField extends StatefulWidget { this.onSubmitted, this.onTapOutside, this.inputFormatters, - this.enabled, + this.enabled = true, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), @@ -489,7 +482,7 @@ class CupertinoTextField extends StatefulWidget { /// Controls the visibility of the [prefix] widget based on the state of /// text entry when the [prefix] argument is not null. /// - /// Defaults to [OverlayVisibilityMode.always] and cannot be null. + /// Defaults to [OverlayVisibilityMode.always]. /// /// Has no effect when [prefix] is null. final OverlayVisibilityMode prefixMode; @@ -500,7 +493,7 @@ class CupertinoTextField extends StatefulWidget { /// Controls the visibility of the [suffix] widget based on the state of /// text entry when the [suffix] argument is not null. /// - /// Defaults to [OverlayVisibilityMode.always] and cannot be null. + /// Defaults to [OverlayVisibilityMode.always]. /// /// Has no effect when [suffix] is null. final OverlayVisibilityMode suffixMode; @@ -512,7 +505,7 @@ class CupertinoTextField extends StatefulWidget { /// /// Will only appear if no [suffix] widget is appearing. /// - /// Defaults to never appearing and cannot be null. + /// Defaults to [OverlayVisibilityMode.never]. final OverlayVisibilityMode clearButtonMode; /// {@macro flutter.widgets.editableText.keyboardType} @@ -653,7 +646,9 @@ class CupertinoTextField extends StatefulWidget { /// Text fields in disabled states have a light grey background and don't /// respond to touch events including the [prefix], [suffix] and the clear /// button. - final bool? enabled; + /// + /// Defaults to true. + final bool enabled; /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; @@ -946,7 +941,7 @@ class _CupertinoTextFieldState extends State with Restoratio if (widget.controller == null) { _createLocalController(); } - _effectiveFocusNode.canRequestFocus = widget.enabled ?? true; + _effectiveFocusNode.canRequestFocus = widget.enabled; _effectiveFocusNode.addListener(_handleFocusChanged); } @@ -965,7 +960,7 @@ class _CupertinoTextFieldState extends State with Restoratio (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); } - _effectiveFocusNode.canRequestFocus = widget.enabled ?? true; + _effectiveFocusNode.canRequestFocus = widget.enabled; } @override @@ -1079,41 +1074,16 @@ class _CupertinoTextFieldState extends State with Restoratio @override bool get wantKeepAlive => _controller?.value.text.isNotEmpty ?? false; - bool _shouldShowAttachment({ + static bool _shouldShowAttachment({ required OverlayVisibilityMode attachment, required bool hasText, }) { - switch (attachment) { - case OverlayVisibilityMode.never: - return false; - case OverlayVisibilityMode.always: - return true; - case OverlayVisibilityMode.editing: - return hasText; - case OverlayVisibilityMode.notEditing: - return !hasText; - } - } - - bool _showPrefixWidget(TextEditingValue text) { - return widget.prefix != null && _shouldShowAttachment( - attachment: widget.prefixMode, - hasText: text.text.isNotEmpty, - ); - } - - bool _showSuffixWidget(TextEditingValue text) { - return widget.suffix != null && _shouldShowAttachment( - attachment: widget.suffixMode, - hasText: text.text.isNotEmpty, - ); - } - - bool _showClearButton(TextEditingValue text) { - return _shouldShowAttachment( - attachment: widget.clearButtonMode, - hasText: text.text.isNotEmpty, - ); + return switch (attachment) { + OverlayVisibilityMode.never => false, + OverlayVisibilityMode.always => true, + OverlayVisibilityMode.editing => hasText, + OverlayVisibilityMode.notEditing => !hasText, + }; } // True if any surrounding decoration widgets will be shown. @@ -1134,6 +1104,32 @@ class _CupertinoTextFieldState extends State with Restoratio return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top; } + void _onClearButtonTapped() { + final bool hadText = _effectiveController.text.isNotEmpty; + _effectiveController.clear(); + if (hadText) { + // Tapping the clear button is also considered a "user initiated" change + // (instead of a programmatical one), so call `onChanged` if the text + // changed as a result. + widget.onChanged?.call(_effectiveController.text); + } + } + + Widget _buildClearButton() { + return GestureDetector( + key: _clearGlobalKey, + onTap: widget.enabled ? _onClearButtonTapped : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Icon( + CupertinoIcons.clear_thick_circled, + size: 18.0, + color: CupertinoDynamicColor.resolve(_kClearButtonColor, context), + ), + ), + ); + } + Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) { // If there are no surrounding widgets, just return the core editable text // part. @@ -1145,59 +1141,69 @@ class _CupertinoTextFieldState extends State with Restoratio return ValueListenableBuilder( valueListenable: _effectiveController, child: editableText, - builder: (BuildContext context, TextEditingValue? text, Widget? child) { + builder: (BuildContext context, TextEditingValue text, Widget? child) { + final bool hasText = text.text.isNotEmpty; + final String? placeholderText = widget.placeholder; + final Widget? placeholder = placeholderText == null + ? null + // Make the placeholder invisible when hasText is true. + : Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: !hasText, + child: SizedBox( + width: double.infinity, + child: Padding( + padding: widget.padding, + child: Text( + placeholderText, + // This is to make sure the text field is always tall enough + // to accommodate the first line of the placeholder, so the + // text does not shrink vertically as you type (however in + // rare circumstances, the height may still change when + // there's no placeholder text). + maxLines: hasText ? 1 : widget.maxLines, + overflow: placeholderStyle.overflow, + style: placeholderStyle, + textAlign: widget.textAlign, + ), + ), + ), + ); + + final Widget? prefixWidget = _shouldShowAttachment(attachment: widget.prefixMode, hasText: hasText) ? widget.prefix : null; + + // Show user specified suffix if applicable and fall back to clear button. + final bool showUserSuffix = _shouldShowAttachment(attachment: widget.suffixMode, hasText: hasText); + final bool showClearButton = _shouldShowAttachment(attachment: widget.clearButtonMode, hasText: hasText); + final Widget? suffixWidget = switch ((showUserSuffix, showClearButton)) { + (false, false) => null, + (true, false) => widget.suffix, + (true, true) => widget.suffix ?? _buildClearButton(), + (false, true) => _buildClearButton(), + }; return Row(children: [ // Insert a prefix at the front if the prefix visibility mode matches // the current text state. - if (_showPrefixWidget(text!)) widget.prefix!, + if (prefixWidget != null) prefixWidget, // In the middle part, stack the placeholder on top of the main EditableText // if needed. Expanded( child: Stack( + // Ideally this should be baseline aligned. However that comes at + // the cost of the ability to compute the intrinsic dimensions of + // this widget. + // See also https://github.com/flutter/flutter/issues/13715. + alignment: AlignmentDirectional.center, + textDirection: widget.textDirection, children: [ - if (widget.placeholder != null && text.text.isEmpty) - SizedBox( - width: double.infinity, - child: Padding( - padding: widget.padding, - child: Text( - widget.placeholder!, - maxLines: widget.maxLines, - overflow: placeholderStyle.overflow ?? TextOverflow.ellipsis, - style: placeholderStyle, - textAlign: widget.textAlign, - ), - ), - ), - child!, + if (placeholder != null) placeholder, + editableText, ], ), ), - // First add the explicit suffix if the suffix visibility mode matches. - if (_showSuffixWidget(text)) - widget.suffix! - // Otherwise, try to show a clear button if its visibility mode matches. - else if (_showClearButton(text)) - GestureDetector( - key: _clearGlobalKey, - onTap: widget.enabled ?? true ? () { - // Special handle onChanged for ClearButton - // Also call onChanged when the clear button is tapped. - final bool textChanged = _effectiveController.text.isNotEmpty; - _effectiveController.clear(); - if (widget.onChanged != null && textChanged) { - widget.onChanged!(_effectiveController.text); - } - } : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Icon( - CupertinoIcons.clear_thick_circled, - size: 18.0, - color: CupertinoDynamicColor.resolve(_kClearButtonColor, context), - ), - ), - ), + if (suffixWidget != null) suffixWidget ]); }, ); @@ -1251,7 +1257,7 @@ class _CupertinoTextFieldState extends State with Restoratio }; } - final bool enabled = widget.enabled ?? true; + final bool enabled = widget.enabled; final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0); final List formatters = [ ...?widget.inputFormatters, diff --git a/packages/flutter/lib/src/cupertino/text_form_field_row.dart b/packages/flutter/lib/src/cupertino/text_form_field_row.dart index 268d8c06125f2..5f9c951ebd4dc 100644 --- a/packages/flutter/lib/src/cupertino/text_form_field_row.dart +++ b/packages/flutter/lib/src/cupertino/text_form_field_row.dart @@ -133,7 +133,7 @@ class CupertinoTextFormFieldRow extends FormField { int? minLines, bool expands = false, int? maxLength, - ValueChanged? onChanged, + this.onChanged, GestureTapCallback? onTap, VoidCallback? onEditingComplete, ValueChanged? onFieldSubmitted, @@ -179,9 +179,7 @@ class CupertinoTextFormFieldRow extends FormField { void onChangedHandler(String value) { field.didChange(value); - if (onChanged != null) { - onChanged(value); - } + onChanged?.call(value); } return CupertinoFormRow( @@ -219,7 +217,7 @@ class CupertinoTextFormFieldRow extends FormField { onEditingComplete: onEditingComplete, onSubmitted: onFieldSubmitted, inputFormatters: inputFormatters, - enabled: enabled, + enabled: enabled ?? true, cursorWidth: cursorWidth, cursorHeight: cursorHeight, cursorColor: cursorColor, @@ -260,6 +258,9 @@ class CupertinoTextFormFieldRow extends FormField { /// initialize its [TextEditingController.text] with [initialValue]. final TextEditingController? controller; + /// {@macro flutter.material.TextFormField.onChanged} + final ValueChanged? onChanged; + static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { return CupertinoAdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, @@ -313,6 +314,7 @@ class _CupertinoTextFormFieldRowState extends FormFieldState { @override void dispose() { _cupertinoTextFormFieldRow.controller?.removeListener(_handleControllerChanged); + _controller?.dispose(); super.dispose(); } @@ -327,13 +329,11 @@ class _CupertinoTextFormFieldRowState extends FormFieldState { @override void reset() { + // Set the controller value before calling super.reset() to let + // _handleControllerChanged suppress the change. + _effectiveController!.text = widget.initialValue!; super.reset(); - - if (widget.initialValue != null) { - setState(() { - _effectiveController!.text = widget.initialValue!; - }); - } + _cupertinoTextFormFieldRow.onChanged?.call(_effectiveController!.text); } void _handleControllerChanged() { diff --git a/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart b/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart index e4703a60f1d94..6e89fc6dd5d5e 100644 --- a/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart +++ b/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:collection'; +import 'dart:math' as math show pi; import 'dart:ui' as ui; import 'package:flutter/foundation.dart' show Brightness, clampDouble; @@ -13,20 +14,25 @@ import 'colors.dart'; import 'text_selection_toolbar_button.dart'; import 'theme.dart'; -// Values extracted from https://developer.apple.com/design/resources/. -// The height of the toolbar, including the arrow. -const double _kToolbarHeight = 45.0; +// The radius of the toolbar RRect shape. +// Value extracted from https://developer.apple.com/design/resources/. +const Radius _kToolbarBorderRadius = Radius.circular(8.0); + // Vertical distance between the tip of the arrow and the line of text the arrow // is pointing to. The value used here is eyeballed. const double _kToolbarContentDistance = 8.0; + +// The size of the arrow pointing to the anchor. Eyeballed value. const Size _kToolbarArrowSize = Size(14.0, 7.0); // Minimal padding from tip of the selection toolbar arrow to horizontal edges of the // screen. Eyeballed value. const double _kArrowScreenPadding = 26.0; -// Values extracted from https://developer.apple.com/design/resources/. -const Radius _kToolbarBorderRadius = Radius.circular(8); +// The size and thickness of the chevron icon used for navigating between toolbar pages. +// Eyeballed values. +const double _kToolbarChevronSize = 10.0; +const double _kToolbarChevronThickness = 2.0; // Color was measured from a screenshot of iOS 16.0.2 // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507. @@ -35,9 +41,6 @@ const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.wit darkColor: Color(0xFF222222), ); -const double _kToolbarChevronSize = 10; -const double _kToolbarChevronThickness = 2; - // Color was measured from a screenshot of iOS 16.0.2. const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFD6D6D6), @@ -63,8 +66,8 @@ const Duration _kToolbarTransitionDuration = Duration(milliseconds: 125); /// Material-style toolbar. typedef CupertinoToolbarBuilder = Widget Function( BuildContext context, - Offset anchor, - bool isAbove, + Offset anchorAbove, + Offset anchorBelow, Widget child, ); @@ -126,37 +129,23 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { // Builds a toolbar just like the default iOS toolbar, with the right color // background and a rounded cutout with an arrow. - static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) { - final Widget outputChild = _CupertinoTextSelectionToolbarShape( - anchor: anchor, - isAbove: isAbove, + static Widget _defaultToolbarBuilder( + BuildContext context, + Offset anchorAbove, + Offset anchorBelow, + Widget child, + ) { + return _CupertinoTextSelectionToolbarShape( + anchorAbove: anchorAbove, + anchorBelow: anchorBelow, + shadowColor: CupertinoTheme.brightnessOf(context) == Brightness.light + ? CupertinoColors.black.withOpacity(0.2) + : null, child: ColoredBox( color: _kToolbarBackgroundColor.resolveFrom(context), child: child, ), ); - if (CupertinoTheme.brightnessOf(context) == Brightness.dark) { - return outputChild; - } - return DecoratedBox( - // These shadow values were eyeballed from a screenshot of iOS 16.3.1, as - // light mode didn't appear in the Apple design resources assets linked at - // the top of this file. - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(_kToolbarBorderRadius), - boxShadow: [ - BoxShadow( - color: CupertinoColors.black.withOpacity(0.2), - blurRadius: 15.0, - offset: Offset( - 0.0, - isAbove ? 0.0 : _kToolbarArrowSize.height, - ), - ), - ], - ), - child: outputChild, - ); } @override @@ -165,10 +154,6 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context); final double paddingAbove = mediaQueryPadding.top + kToolbarScreenPadding; - final double toolbarHeightNeeded = paddingAbove - + _kToolbarContentDistance - + _kToolbarHeight; - final bool fitsAbove = anchorAbove.dy >= toolbarHeightNeeded; // The arrow, which points to the anchor, has some margin so it can't get // too close to the horizontal edges of the screen. @@ -195,11 +180,10 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { delegate: TextSelectionToolbarLayoutDelegate( anchorAbove: anchorAboveAdjusted, anchorBelow: anchorBelowAdjusted, - fitsAbove: fitsAbove, ), child: _CupertinoTextSelectionToolbarContent( - anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted, - isAbove: fitsAbove, + anchorAbove: anchorAboveAdjusted, + anchorBelow: anchorBelowAdjusted, toolbarBuilder: toolbarBuilder, children: children, ), @@ -214,30 +198,32 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { // The anchor should be in global coordinates. class _CupertinoTextSelectionToolbarShape extends SingleChildRenderObjectWidget { const _CupertinoTextSelectionToolbarShape({ - required Offset anchor, - required bool isAbove, + required Offset anchorAbove, + required Offset anchorBelow, + Color? shadowColor, super.child, - }) : _anchor = anchor, - _isAbove = isAbove; + }) : _anchorAbove = anchorAbove, + _anchorBelow = anchorBelow, + _shadowColor = shadowColor; - final Offset _anchor; - - // Whether the arrow should point down and be attached to the bottom - // of the toolbar, or point up and be attached to the top of the toolbar. - final bool _isAbove; + final Offset _anchorAbove; + final Offset _anchorBelow; + final Color? _shadowColor; @override _RenderCupertinoTextSelectionToolbarShape createRenderObject(BuildContext context) => _RenderCupertinoTextSelectionToolbarShape( - _anchor, - _isAbove, + _anchorAbove, + _anchorBelow, + _shadowColor, null, ); @override void updateRenderObject(BuildContext context, _RenderCupertinoTextSelectionToolbarShape renderObject) { renderObject - ..anchor = _anchor - ..isAbove = _isAbove; + ..anchorAbove = _anchorAbove + ..anchorBelow = _anchorBelow + ..shadowColor = _shadowColor; } } @@ -251,115 +237,192 @@ class _CupertinoTextSelectionToolbarShape extends SingleChildRenderObjectWidget // on the necessary side. class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox { _RenderCupertinoTextSelectionToolbarShape( - this._anchor, - this._isAbove, + this._anchorAbove, + this._anchorBelow, + this._shadowColor, super.child, ); @override bool get isRepaintBoundary => true; - Offset get anchor => _anchor; - Offset _anchor; - set anchor(Offset value) { - if (value == _anchor) { + Offset get anchorAbove => _anchorAbove; + Offset _anchorAbove; + set anchorAbove(Offset value) { + if (value == _anchorAbove) { return; } - _anchor = value; + _anchorAbove = value; markNeedsLayout(); } - bool get isAbove => _isAbove; - bool _isAbove; - set isAbove(bool value) { - if (_isAbove == value) { + Offset get anchorBelow => _anchorBelow; + Offset _anchorBelow; + set anchorBelow(Offset value) { + if (value == _anchorBelow) { return; } - _isAbove = value; + _anchorBelow = value; markNeedsLayout(); } - // The child is tall enough to have the arrow clipped out of it on both sides - // top and bottom. Since _kToolbarHeight includes the height of one arrow, the - // total height that the child is given is that plus one more arrow height. - // The extra height on the opposite side of the arrow will be clipped out. By - // using this approach, the buttons don't need any special padding that - // depends on isAbove. - final BoxConstraints _heightConstraint = BoxConstraints.tightFor( - height: _kToolbarHeight + _kToolbarArrowSize.height, - ); + Color? get shadowColor => _shadowColor; + Color? _shadowColor; + set shadowColor(Color? value) { + if (value == _shadowColor) { + return; + } + _shadowColor = value; + markNeedsPaint(); + } + + bool get isAbove => anchorAbove.dy >= (child?.size.height ?? 0.0) - _kToolbarArrowSize.height * 2; @override void performLayout() { + final RenderBox? child = this.child; if (child == null) { return; } - final BoxConstraints enforcedConstraint = constraints.loosen(); - - child!.layout(_heightConstraint.enforce(enforcedConstraint), parentUsesSize: true); + final BoxConstraints enforcedConstraint = BoxConstraints( + minWidth: _kToolbarArrowSize.width + _kToolbarBorderRadius.x * 2, + ).enforce(constraints.loosen()); + child.layout(enforcedConstraint, parentUsesSize: true); + // The buttons are padded on both top and bottom sufficiently to have + // the arrow clipped out of it on either side. By + // using this approach, the buttons don't need any special padding that + // depends on isAbove. // The height of one arrow will be clipped off of the child, so adjust the // size and position to remove that piece from the layout. - final BoxParentData childParentData = child!.parentData! as BoxParentData; + final BoxParentData childParentData = child.parentData! as BoxParentData; childParentData.offset = Offset( 0.0, - _isAbove ? -_kToolbarArrowSize.height : 0.0, + isAbove ? -_kToolbarArrowSize.height : 0.0, ); size = Size( - child!.size.width, - child!.size.height - _kToolbarArrowSize.height, + child.size.width, + child.size.height - _kToolbarArrowSize.height, ); } - // The path is described in the toolbar's coordinate system. - Path _clipPath() { - final BoxParentData childParentData = child!.parentData! as BoxParentData; - final Path rrect = Path() - ..addRRect( - RRect.fromRectAndRadius( - Offset(0.0, _kToolbarArrowSize.height) - & Size( - child!.size.width, - child!.size.height - _kToolbarArrowSize.height * 2, - ), - _kToolbarBorderRadius, - ), - ); - - final Offset localAnchor = globalToLocal(_anchor); - final double centerX = childParentData.offset.dx + child!.size.width / 2; - final double arrowXOffsetFromCenter = localAnchor.dx - centerX; - final double arrowTipX = child!.size.width / 2 + arrowXOffsetFromCenter; + // Returns the RRect inside which the child is painted. + RRect _shapeRRect(RenderBox child) { + final Rect rect = Offset(0.0, _kToolbarArrowSize.height) + & Size(child.size.width, child.size.height - _kToolbarArrowSize.height * 2); + return RRect.fromRectAndRadius(rect, _kToolbarBorderRadius).scaleRadii(); + } - final double arrowBaseY = _isAbove - ? child!.size.height - _kToolbarArrowSize.height - : _kToolbarArrowSize.height; + // Adds the given `rrect` to the current `path`, starting from the last point + // in `path` and ends after the last corner of the rrect (closest corner to + // `startAngle` in the counterclockwise direction), without closing the path. + // + // The `startAngle` argument must be a multiple of pi / 2, with 0 being the + // positive half of the x-axis, and pi / 2 being the negative half of the + // y-axis. + // + // For instance, if `startAngle` equals pi/2 then this method draws a line + // segment to the bottom-left corner of `rrect` from the last point in `path`, + // and follows the `rrect` path clockwise until the bottom-right corner is + // added, then this method returns the mutated path without closing it. + static Path _addRRectToPath(Path path, RRect rrect, { required double startAngle }) { + const double halfPI = math.pi / 2; + assert(startAngle % halfPI == 0.0); + final Rect rect = rrect.outerRect; + + final List<(Offset, Radius)> rrectCorners = <(Offset, Radius)>[ + (rect.bottomRight, -rrect.brRadius), + (rect.bottomLeft, Radius.elliptical(rrect.blRadiusX, -rrect.blRadiusY)), + (rect.topLeft, rrect.tlRadius), + (rect.topRight, Radius.elliptical(-rrect.trRadiusX, rrect.trRadiusY)), + ]; + + // Add the 4 corners to the path clockwise. Convert radians to quadrants + // to avoid fp arithmetics. The order is br -> bl -> tl -> tr if the starting + // angle is 0. + final int startQuadrantIndex = startAngle ~/ halfPI; + for (int i = startQuadrantIndex; i < rrectCorners.length + startQuadrantIndex; i += 1) { + final (Offset vertex, Radius rectCenterOffset) = rrectCorners[i % rrectCorners.length]; + final Offset otherVertex = Offset(vertex.dx + 2 * rectCenterOffset.x, vertex.dy + 2 * rectCenterOffset.y); + final Rect rect = Rect.fromPoints(vertex, otherVertex); + path.arcTo(rect, halfPI * i, halfPI, false); + } + return path; + } - final double arrowTipY = _isAbove ? child!.size.height : 0; + // The path is described in the toolbar child's coordinate system. + Path _clipPath(RenderBox child, RRect rrect) { + final Path path = Path(); + // If there isn't enough width for the arrow + radii, ignore the arrow. + // Because of the constraints we gave children in performLayout, this should + // only happen if the parent isn't wide enough which should be very rare, and + // when that happens the arrow won't be too useful anyways. + if (_kToolbarBorderRadius.x * 2 + _kToolbarArrowSize.width > size.width) { + return path..addRRect(rrect); + } - final Path arrow = Path() - ..moveTo(arrowTipX, arrowTipY) - ..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBaseY) - ..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBaseY) - ..close(); + final Offset localAnchor = globalToLocal(isAbove ? _anchorAbove : _anchorBelow); + final double arrowTipX = clampDouble( + localAnchor.dx, + _kToolbarBorderRadius.x + _kToolbarArrowSize.width / 2, + size.width - _kToolbarArrowSize.width / 2 - _kToolbarBorderRadius.x, + ); - return Path.combine(PathOperation.union, rrect, arrow); + // Draw the path clockwise, starting from the beginning side of the arrow. + if (isAbove) { + final double arrowBaseY = child.size.height - _kToolbarArrowSize.height; + final double arrowTipY = child.size.height; + path + ..moveTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBaseY) // right side of the arrow triangle + ..lineTo(arrowTipX, arrowTipY) // The tip of the arrow + ..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBaseY); // left side of the arrow triangle + } else { + final double arrowBaseY = _kToolbarArrowSize.height; + const double arrowTipY = 0.0; + path + ..moveTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBaseY) // right side of the arrow triangle + ..lineTo(arrowTipX, arrowTipY) // The tip of the arrow + ..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBaseY); // left side of the arrow triangle + } + final double startAngle = isAbove ? math.pi / 2 : -math.pi / 2; + return _addRRectToPath(path, rrect, startAngle: startAngle)..close(); } @override void paint(PaintingContext context, Offset offset) { + final RenderBox? child = this.child; if (child == null) { return; } - final BoxParentData childParentData = child!.parentData! as BoxParentData; + final BoxParentData childParentData = child.parentData! as BoxParentData; + + final RRect rrect = _shapeRRect(child); + final Path clipPath = _clipPath(child, rrect); + + // If configured, paint the shadow beneath the shape. + if (_shadowColor != null) { + final BoxShadow boxShadow = BoxShadow( + color: _shadowColor!, + blurRadius: 15.0, + ); + final RRect shadowRRect = RRect.fromLTRBR( + rrect.left, + rrect.top, + rrect.right, + rrect.bottom + _kToolbarArrowSize.height, + _kToolbarBorderRadius, + ).shift(offset + childParentData.offset + boxShadow.offset); + context.canvas.drawRRect(shadowRRect, boxShadow.toPaint()); + } + _clipPathLayer.layer = context.pushClipPath( needsCompositing, offset + childParentData.offset, - Offset.zero & child!.size, - _clipPath(), - (PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child!, innerOffset), + Offset.zero & child.size, + clipPath, + (PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child, innerOffset), oldLayer: _clipPathLayer.layer, ); } @@ -376,11 +439,12 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox { @override void debugPaintSize(PaintingContext context, Offset offset) { assert(() { + final RenderBox? child = this.child; if (child == null) { return true; } - _debugPaint ??= Paint() + final ui.Paint debugPaint = _debugPaint ??= Paint() ..shader = ui.Gradient.linear( Offset.zero, const Offset(10.0, 10.0), @@ -391,22 +455,28 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox { ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; - final BoxParentData childParentData = child!.parentData! as BoxParentData; - context.canvas.drawPath(_clipPath().shift(offset + childParentData.offset), _debugPaint!); + final BoxParentData childParentData = child.parentData! as BoxParentData; + final Path clipPath = _clipPath(child, _shapeRRect(child)); + context.canvas.drawPath(clipPath.shift(offset + childParentData.offset), debugPaint); return true; }()); } @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + final RenderBox? child = this.child; + if (child == null) { + return false; + } + // Positions outside of the clipped area of the child are not counted as // hits. - final BoxParentData childParentData = child!.parentData! as BoxParentData; + final BoxParentData childParentData = child.parentData! as BoxParentData; final Rect hitBox = Rect.fromLTWH( childParentData.offset.dx, childParentData.offset.dy + _kToolbarArrowSize.height, - child!.size.width, - child!.size.height - _kToolbarArrowSize.height * 2, + child.size.width, + child.size.height - _kToolbarArrowSize.height * 2, ); if (!hitBox.contains(position)) { return false; @@ -423,15 +493,15 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox { // The anchor should be in global coordinates. class _CupertinoTextSelectionToolbarContent extends StatefulWidget { const _CupertinoTextSelectionToolbarContent({ - required this.anchor, - required this.isAbove, + required this.anchorAbove, + required this.anchorBelow, required this.toolbarBuilder, required this.children, }) : assert(children.length > 0); - final Offset anchor; + final Offset anchorAbove; + final Offset anchorBelow; final List children; - final bool isAbove; final CupertinoToolbarBuilder toolbarBuilder; @override @@ -522,26 +592,48 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel super.dispose(); } - Widget _createChevron({required bool isLeft}) { - final Color color = _kToolbarTextColor.resolveFrom(context); - - return IgnorePointer( - child: Center( - // If widthFactor is not set to 0, the button is given unbounded width. - widthFactor: 0, - child: CustomPaint( - painter: isLeft - ? _LeftCupertinoChevronPainter(color: color) - : _RightCupertinoChevronPainter(color: color), - size: const Size.square(_kToolbarChevronSize), + @override + Widget build(BuildContext context) { + final Color chevronColor = _kToolbarTextColor.resolveFrom(context); + + // Wrap the children and the chevron painters in Center with widthFactor + // and heightFactor of 1.0 so _CupertinoTextSelectionToolbarItems can get + // the natural size of the buttons and then expand vertically as needed. + final Widget backButton = Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: CupertinoTextSelectionToolbarButton( + onPressed: _handlePreviousPage, + child: IgnorePointer( + child: CustomPaint( + painter: _LeftCupertinoChevronPainter(color: chevronColor), + size: const Size.square(_kToolbarChevronSize), + ), ), ), ); - } + final Widget nextButton = Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: CupertinoTextSelectionToolbarButton( + onPressed: _handleNextPage, + child: IgnorePointer( + child: CustomPaint( + painter: _RightCupertinoChevronPainter(color: chevronColor), + size: const Size.square(_kToolbarChevronSize), + ), + ), + ), + ); + final List children = widget.children.map((Widget child) { + return Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: child, + ); + }).toList(); - @override - Widget build(BuildContext context) { - return widget.toolbarBuilder(context, widget.anchor, widget.isAbove, FadeTransition( + return widget.toolbarBuilder(context, widget.anchorAbove, widget.anchorBelow, FadeTransition( opacity: _controller, child: AnimatedSize( duration: _kToolbarTransitionDuration, @@ -551,17 +643,11 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel child: _CupertinoTextSelectionToolbarItems( key: _toolbarItemsKey, page: _page, - backButton: CupertinoTextSelectionToolbarButton( - onPressed: _handlePreviousPage, - child: _createChevron(isLeft: true), - ), + backButton: backButton, dividerColor: _kToolbarDividerColor.resolveFrom(context), dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context), - nextButton: CupertinoTextSelectionToolbarButton( - onPressed: _handleNextPage, - child: _createChevron(isLeft: false), - ), - children: widget.children, + nextButton: nextButton, + children: children, ), ), ), @@ -591,7 +677,7 @@ abstract class _CupertinoChevronPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - assert(size.height == size.width, 'size must have the same height and width'); + assert(size.height == size.width, 'size must have the same height and width: $size'); final double iconSize = size.height; @@ -851,10 +937,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container return newChild; } - bool _isSlottedChild(RenderBox child) { - return child == _backButton || child == _nextButton; - } - int _page; int get page => _page; set page(int value) { @@ -904,66 +986,71 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container return; } + // First pass: determine the height of the tallest child. + double greatestHeight = 0.0; + visitChildren((RenderObject renderObjectChild) { + final RenderBox child = renderObjectChild as RenderBox; + final double childHeight = child.getMaxIntrinsicHeight(constraints.maxWidth); + if (childHeight > greatestHeight) { + greatestHeight = childHeight; + } + }); + // Layout slotted children. - _backButton!.layout(constraints.loosen(), parentUsesSize: true); - _nextButton!.layout(constraints.loosen(), parentUsesSize: true); + final BoxConstraints slottedConstraints = BoxConstraints( + maxWidth: constraints.maxWidth, + minHeight: greatestHeight, + maxHeight: greatestHeight, + ); + _backButton!.layout(slottedConstraints, parentUsesSize: true); + _nextButton!.layout(slottedConstraints, parentUsesSize: true); - final double subsequentPageButtonsWidth = - _backButton!.size.width + _nextButton!.size.width; + final double subsequentPageButtonsWidth = _backButton!.size.width + _nextButton!.size.width; double currentButtonPosition = 0.0; late double toolbarWidth; // The width of the whole widget. - late double greatestHeight = 0.0; late double firstPageWidth; int currentPage = 0; int i = -1; visitChildren((RenderObject renderObjectChild) { i++; final RenderBox child = renderObjectChild as RenderBox; - final ToolbarItemsParentData childParentData = - child.parentData! as ToolbarItemsParentData; + final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData; childParentData.shouldPaint = false; // Skip slotted children and children on pages after the visible page. - if (_isSlottedChild(child) || currentPage > _page) { + if (child == _backButton || child == _nextButton || currentPage > _page) { return; } - double paginationButtonsWidth = 0.0; - if (currentPage == 0) { - // If this is the last child, it's ok to fit without a forward button. - // Note childCount doesn't include slotted children which come before the list ones. - paginationButtonsWidth = - i == childCount + 1 ? 0.0 : _nextButton!.size.width; - } else { - paginationButtonsWidth = subsequentPageButtonsWidth; - } + // If this is the last child on the first page, it's ok to fit without a forward button. + // Note childCount doesn't include slotted children which come before the list ones. + double paginationButtonsWidth = currentPage == 0 + ? i == childCount + 1 ? 0.0 : _nextButton!.size.width + : subsequentPageButtonsWidth; // The width of the menu is set by the first page. child.layout( - BoxConstraints.loose(Size( - (currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth, - constraints.maxHeight, - )), + BoxConstraints( + maxWidth: (currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth, + minHeight: greatestHeight, + maxHeight: greatestHeight, + ), parentUsesSize: true, ); - greatestHeight = child.size.height > greatestHeight - ? child.size.height - : greatestHeight; - // If this child causes the current page to overflow, move to the next // page and relayout the child. - final double currentWidth = - currentButtonPosition + paginationButtonsWidth + child.size.width; + final double currentWidth = currentButtonPosition + paginationButtonsWidth + child.size.width; if (currentWidth > constraints.maxWidth) { currentPage++; currentButtonPosition = _backButton!.size.width + dividerWidth; paginationButtonsWidth = _backButton!.size.width + _nextButton!.size.width; child.layout( - BoxConstraints.loose(Size( - firstPageWidth - paginationButtonsWidth, - constraints.maxHeight, - )), + BoxConstraints( + maxWidth: firstPageWidth - paginationButtonsWidth, + minHeight: greatestHeight, + maxHeight: greatestHeight, + ), parentUsesSize: true, ); } @@ -984,10 +1071,8 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container // Position page nav buttons. if (currentPage > 0) { - final ToolbarItemsParentData nextButtonParentData = - _nextButton!.parentData! as ToolbarItemsParentData; - final ToolbarItemsParentData backButtonParentData = - _backButton!.parentData! as ToolbarItemsParentData; + final ToolbarItemsParentData nextButtonParentData = _nextButton!.parentData! as ToolbarItemsParentData; + final ToolbarItemsParentData backButtonParentData = _backButton!.parentData! as ToolbarItemsParentData; // The forward button only shows when there's a page after this one. if (page != currentPage) { nextButtonParentData.offset = Offset(toolbarWidth, 0.0); @@ -1001,16 +1086,16 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container // already been taken care of when laying out the children to // accommodate the back button. } - - // Update previous/next page values so that we can check in the horizontal - // drag gesture callback if it's possible to navigate. - hasNextPage = page != currentPage; - hasPreviousPage = page > 0; } else { // No divider for the next button when there's only one page. toolbarWidth -= dividerWidth; } + // Update previous/next page values so that we can check in the horizontal + // drag gesture callback if it's possible to navigate. + hasNextPage = page != currentPage; + hasPreviousPage = page > 0; + size = constraints.constrain(Size(toolbarWidth, greatestHeight)); } @@ -1051,8 +1136,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container if (child == null) { return false; } - final ToolbarItemsParentData childParentData = - child.parentData! as ToolbarItemsParentData; + final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData; if (!childParentData.shouldPaint) { return false; } diff --git a/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart b/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart index f07d88e4b5ec6..cc991db20d651 100644 --- a/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart +++ b/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart @@ -34,8 +34,6 @@ const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 18.0, h /// A button in the style of the iOS text selection toolbar buttons. class CupertinoTextSelectionToolbarButton extends StatefulWidget { /// Create an instance of [CupertinoTextSelectionToolbarButton]. - /// - /// [child] cannot be null. const CupertinoTextSelectionToolbarButton({ super.key, this.onPressed, @@ -54,8 +52,6 @@ class CupertinoTextSelectionToolbarButton extends StatefulWidget { /// Create an instance of [CupertinoTextSelectionToolbarButton] from the given /// [ContextMenuButtonItem]. - /// - /// [buttonItem] cannot be null. CupertinoTextSelectionToolbarButton.buttonItem({ super.key, required ContextMenuButtonItem this.buttonItem, @@ -105,6 +101,12 @@ class CupertinoTextSelectionToolbarButton extends StatefulWidget { return localizations.pasteButtonLabel; case ContextMenuButtonType.selectAll: return localizations.selectAllButtonLabel; + case ContextMenuButtonType.lookUp: + return localizations.lookUpButtonLabel; + case ContextMenuButtonType.searchWeb: + return localizations.searchWebButtonLabel; + case ContextMenuButtonType.share: + return localizations.shareButtonLabel; case ContextMenuButtonType.liveTextInput: case ContextMenuButtonType.delete: case ContextMenuButtonType.custom: @@ -189,6 +191,9 @@ class _CupertinoTextSelectionToolbarButtonState extends State shadows; /// Half the default diameter of the thumb. diff --git a/packages/flutter/lib/src/cupertino/toggleable.dart b/packages/flutter/lib/src/cupertino/toggleable.dart index dacdab762b50c..a5fb38d9a6d84 100644 --- a/packages/flutter/lib/src/cupertino/toggleable.dart +++ b/packages/flutter/lib/src/cupertino/toggleable.dart @@ -111,7 +111,7 @@ mixin ToggleableStateMixin on TickerProviderStateMixin /// build method - potentially after wrapping it in other widgets. Widget buildToggleable({ FocusNode? focusNode, - Function(bool)? onFocusChange, + ValueChanged? onFocusChange, bool autofocus = false, required Size size, required CustomPainter painter, diff --git a/packages/flutter/lib/src/foundation/README.md b/packages/flutter/lib/src/foundation/README.md index 5e4de0755a288..d32effc8c2381 100644 --- a/packages/flutter/lib/src/foundation/README.md +++ b/packages/flutter/lib/src/foundation/README.md @@ -3,9 +3,9 @@ nothing but core Dart packages. They can't depend on `dart:ui`, they can't depend on any `package:`, and they can't depend on anything outside this directory. -Currently they do depend on dart:ui, but only for `VoidCallback` (and -maybe one day `lerpDouble`), which are all intended to be moved out -of `dart:ui` and into `dart:core`. +Currently they do depend on dart:ui, but only for `VoidCallback` and +`clampDouble` (and maybe one day `lerpDouble`), which are all intended +to be moved out of `dart:ui` and into `dart:core`. There is currently also an unfortunate dependency on the platform dispatcher logic (SingletonFlutterWindow, Brightness, @@ -14,5 +14,4 @@ PlatformDispatcher, window), though that should probably move to the See also: - * https://github.com/dart-lang/sdk/issues/27791 (`VoidCallback`) - * https://github.com/dart-lang/sdk/issues/25217 (`hashValues`, `hashList`, and `lerpDouble`) + * https://github.com/dart-lang/sdk/issues/25217 diff --git a/packages/flutter/lib/src/foundation/_platform_web.dart b/packages/flutter/lib/src/foundation/_platform_web.dart index babeee421a23f..30f757012e18a 100644 --- a/packages/flutter/lib/src/foundation/_platform_web.dart +++ b/packages/flutter/lib/src/foundation/_platform_web.dart @@ -4,7 +4,7 @@ import 'dart:ui_web' as ui_web; -import '../services/dom.dart'; +import 'package:web/web.dart' as web; import 'platform.dart' as platform; @@ -38,7 +38,7 @@ final platform.TargetPlatform? _testPlatform = () { // 0.20ms. As `defaultTargetPlatform` is routinely called dozens of times per // frame this value should be cached. final platform.TargetPlatform _browserPlatform = () { - final String navigatorPlatform = domWindow.navigator.platform?.toLowerCase() ?? ''; + final String navigatorPlatform = web.window.navigator.platform.toLowerCase(); if (navigatorPlatform.startsWith('mac')) { return platform.TargetPlatform.macOS; } @@ -58,7 +58,7 @@ final platform.TargetPlatform _browserPlatform = () { // indicates that a device has a "fine pointer" (mouse) as the primary // pointing device, then we'll assume desktop linux, and otherwise we'll // assume Android. - if (domWindow.matchMedia('only screen and (pointer: fine)').matches) { + if (web.window.matchMedia('only screen and (pointer: fine)').matches) { return platform.TargetPlatform.linux; } return platform.TargetPlatform.android; diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart index 3cc1c3ba9480e..a914609f89e05 100644 --- a/packages/flutter/lib/src/foundation/assertions.dart +++ b/packages/flutter/lib/src/foundation/assertions.dart @@ -48,8 +48,7 @@ typedef StackTraceDemangler = StackTrace Function(StackTrace details); /// * [RepetitiveStackFrameFilter], which uses this class to compare against [StackFrame]s. @immutable class PartialStackFrame { - /// Creates a new [PartialStackFrame] instance. All arguments are required and - /// must not be null. + /// Creates a new [PartialStackFrame] instance. const PartialStackFrame({ required this.package, required this.className, @@ -397,9 +396,6 @@ class FlutterErrorDetails with Diagnosticable { /// /// The framework calls this constructor when catching an exception that will /// subsequently be reported using [FlutterError.onError]. - /// - /// The [exception] must not be null; other arguments can be left to - /// their default values. const FlutterErrorDetails({ required this.exception, this.stack, diff --git a/packages/flutter/lib/src/foundation/basic_types.dart b/packages/flutter/lib/src/foundation/basic_types.dart index 9bc8c6aa6e77b..27c9bc9222923 100644 --- a/packages/flutter/lib/src/foundation/basic_types.dart +++ b/packages/flutter/lib/src/foundation/basic_types.dart @@ -102,9 +102,8 @@ typedef AsyncValueGetter = Future Function(); /// also applies to any iterables derived from this one, e.g. as /// returned by `where`. class CachingIterable extends IterableBase { - /// Creates a CachingIterable using the given [Iterator] as the - /// source of data. The iterator must be non-null and must not throw - /// exceptions. + /// Creates a [CachingIterable] using the given [Iterator] as the source of + /// data. The iterator must not throw exceptions. /// /// Since the argument is an [Iterator], not an [Iterable], it is /// guaranteed that the underlying data set will only be walked @@ -229,8 +228,6 @@ class _LazyListIterator implements Iterator { /// A factory interface that also reports the type of the created objects. class Factory { /// Creates a new factory. - /// - /// The `constructor` parameter must not be null. const Factory(this.constructor); /// Creates a new object of type T. diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart index bc451ac718c54..a663cf9716813 100644 --- a/packages/flutter/lib/src/foundation/binding.dart +++ b/packages/flutter/lib/src/foundation/binding.dart @@ -22,7 +22,7 @@ import 'print.dart'; import 'service_extensions.dart'; import 'timeline.dart'; -export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use +export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow, clampDouble; // ignore: deprecated_member_use export 'basic_types.dart' show AsyncCallback, AsyncValueGetter, AsyncValueSetter; @@ -158,9 +158,8 @@ abstract class BindingBase { initServiceExtensions(); assert(_debugServiceExtensionsRegistered); - developer.postEvent('Flutter.FrameworkInitialization', {}); - if (!kReleaseMode) { + developer.postEvent('Flutter.FrameworkInitialization', {}); FlutterTimeline.finishSync(); } } @@ -169,13 +168,6 @@ abstract class BindingBase { static Type? _debugInitializedType; static bool _debugServiceExtensionsRegistered = false; - /// Additional configuration used by the framework during hot reload. - /// - /// See also: - /// - /// * [DebugReassembleConfig], which describes the configuration. - static DebugReassembleConfig? debugReassembleConfig; - /// Deprecated. Will be removed in a future version of Flutter. /// /// This property has been deprecated to prepare for Flutter's upcoming @@ -569,33 +561,22 @@ abstract class BindingBase { name: FoundationServiceExtensions.platformOverride.name, callback: (Map parameters) async { if (parameters.containsKey('value')) { - switch (parameters['value']) { - case 'android': - debugDefaultTargetPlatformOverride = TargetPlatform.android; - case 'fuchsia': - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - case 'iOS': - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - case 'linux': - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - case 'macOS': - debugDefaultTargetPlatformOverride = TargetPlatform.macOS; - case 'windows': - debugDefaultTargetPlatformOverride = TargetPlatform.windows; - case 'default': - default: - debugDefaultTargetPlatformOverride = null; + final String value = parameters['value']!; + debugDefaultTargetPlatformOverride = null; + for (final TargetPlatform candidate in TargetPlatform.values) { + if (candidate.name == value) { + debugDefaultTargetPlatformOverride = candidate; + break; + } } _postExtensionStateChangedEvent( FoundationServiceExtensions.platformOverride.name, - defaultTargetPlatform.toString().substring('$TargetPlatform.'.length), + defaultTargetPlatform.name, ); await reassembleApplication(); } return { - 'value': defaultTargetPlatform - .toString() - .substring('$TargetPlatform.'.length), + 'value': defaultTargetPlatform.name, }; }, ); @@ -657,14 +638,19 @@ abstract class BindingBase { /// [locked]. @protected Future lockEvents(Future Function() callback) { - final developer.TimelineTask timelineTask = developer.TimelineTask()..start('Lock events'); + developer.TimelineTask? debugTimelineTask; + if (!kReleaseMode) { + debugTimelineTask = developer.TimelineTask()..start('Lock events'); + } _lockCount += 1; final Future future = callback(); future.whenComplete(() { _lockCount -= 1; if (!locked) { - timelineTask.finish(); + if (!kReleaseMode) { + debugTimelineTask!.finish(); + } try { unlocked(); } catch (error, stack) { @@ -985,23 +971,3 @@ abstract class BindingBase { Future _exitApplication() async { exit(0); } - -/// Additional configuration used for hot reload reassemble optimizations. -/// -/// Do not extend, implement, or mixin this class. This may only be instantiated -/// in debug mode. -class DebugReassembleConfig { - /// Create a new [DebugReassembleConfig]. - /// - /// Throws a [FlutterError] if this is called in profile or release mode. - DebugReassembleConfig({ - this.widgetName, - }) { - if (!kDebugMode) { - throw FlutterError('Cannot instantiate DebugReassembleConfig in profile or release mode.'); - } - } - - /// The name of the widget that was modified, or `null` if the change was elsewhere. - final String? widgetName; -} diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index 0b8c6f4ae9d85..7bfc2fb294a26 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -207,6 +207,39 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; + /// Dispatches event of the [object] creation to [MemoryAllocations.instance]. + /// + /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] + /// is false, the method is noop. + /// + /// Tools like leak_tracker use the event of object creation to help + /// developers identify the owner of the object, for troubleshooting purposes, + /// by taking stack trace at the moment of the event. + /// + /// But, as [ChangeNotifier] is mixin, it does not have its own constructor. So, it + /// communicates object creation in first `addListener`, that results + /// in the stack trace pointing to `addListener`, not to constructor. + /// + /// To make debugging easier, invoke [ChangeNotifier.maybeDispatchObjectCreation] + /// in constructor of the class. It will help + /// to identify the owner. + /// + /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` + /// so that the method is tree-shaken away when the flag is false. + @protected + static void maybeDispatchObjectCreation(ChangeNotifier object) { + // Tree shaker does not include this method and the class MemoryAllocations + // if kFlutterMemoryAllocationsEnabled is false. + if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { + MemoryAllocations.instance.dispatchObjectCreated( + library: _flutterFoundationLibrary, + className: '$ChangeNotifier', + object: object, + ); + object._creationDispatched = true; + } + } + /// Register a closure to be called when the object changes. /// /// If the given closure is already registered, an additional instance is @@ -236,14 +269,11 @@ mixin class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ChangeNotifier', - object: this, - ); - _creationDispatched = true; + + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(this); } + if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -505,13 +535,8 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ValueNotifier', - object: this, - ); + ChangeNotifier.maybeDispatchObjectCreation(this); } - _creationDispatched = true; } /// The current value stored in this notifier. diff --git a/packages/flutter/lib/src/foundation/collections.dart b/packages/flutter/lib/src/foundation/collections.dart index 7a0be1b07167c..e1e1499d856ba 100644 --- a/packages/flutter/lib/src/foundation/collections.dart +++ b/packages/flutter/lib/src/foundation/collections.dart @@ -10,10 +10,10 @@ /// the same length, and contain the same members. Returns false otherwise. /// Order is not compared. /// -/// If the elements are maps, lists, sets, or other collections/composite objects, -/// then the contents of those elements are not compared element by element unless their -/// equality operators ([Object.==]) do so. -/// For checking deep equality, consider using [DeepCollectionEquality] class. +/// If the elements are maps, lists, sets, or other collections/composite +/// objects, then the contents of those elements are not compared element by +/// element unless their equality operators ([Object.==]) do so. For checking +/// deep equality, consider using the [DeepCollectionEquality] class. /// /// See also: /// @@ -43,10 +43,10 @@ bool setEquals(Set? a, Set? b) { /// the same length, and contain the same members in the same order. Returns /// false otherwise. /// -/// If the elements are maps, lists, sets, or other collections/composite objects, -/// then the contents of those elements are not compared element by element unless their -/// equality operators ([Object.==]) do so. -/// For checking deep equality, consider using [DeepCollectionEquality] class. +/// If the elements are maps, lists, sets, or other collections/composite +/// objects, then the contents of those elements are not compared element by +/// element unless their equality operators ([Object.==]) do so. For checking +/// deep equality, consider using the [DeepCollectionEquality] class. /// /// See also: /// @@ -76,10 +76,10 @@ bool listEquals(List? a, List? b) { /// the same length, and contain the same keys associated with the same values. /// Returns false otherwise. /// -/// If the elements are maps, lists, sets, or other collections/composite objects, -/// then the contents of those elements are not compared element by element unless their -/// equality operators ([Object.==]) do so. -/// For checking deep equality, consider using [DeepCollectionEquality] class. +/// If the elements are maps, lists, sets, or other collections/composite +/// objects, then the contents of those elements are not compared element by +/// element unless their equality operators ([Object.==]) do so. For checking +/// deep equality, consider using the [DeepCollectionEquality] class. /// /// See also: /// @@ -103,7 +103,6 @@ bool mapEquals(Map? a, Map? b) { return true; } - /// Returns the position of `value` in the `sortedList`, if it exists. /// /// Returns `-1` if the `value` is not in the list. Requires the list items diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index f4f9d5c4c0bc9..4f14a3b4da04c 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -3,13 +3,13 @@ // found in the LICENSE file. import 'dart:math' as math; +import 'dart:ui' show clampDouble; import 'package:meta/meta.dart'; import 'assertions.dart'; import 'constants.dart'; import 'debug.dart'; -import 'math.dart' show clampDouble; import 'object.dart'; // Examples can assume: @@ -226,8 +226,6 @@ enum DiagnosticsTreeStyle { /// to render text art for arbitrary trees of [DiagnosticsNode] objects. class TextTreeConfiguration { /// Create a configuration object describing how to render a tree as text. - /// - /// All of the arguments must not be null. TextTreeConfiguration({ required this.prefixLineOne, required this.prefixOtherLines, @@ -1451,8 +1449,6 @@ abstract class DiagnosticsNode { /// Diagnostics containing just a string `message` and not a concrete name or /// value. /// - /// The [style] and [level] arguments must not be null. - /// /// See also: /// /// * [MessageProperty], which is better suited to messages that are to be @@ -1829,8 +1825,6 @@ class MessageProperty extends DiagnosticsProperty { /// /// Messages have no concrete [value] (so [value] will return null). The /// message is stored as the description. - /// - /// The [name], `message`, and [level] arguments must not be null. MessageProperty( String name, String message, { @@ -1847,8 +1841,6 @@ class MessageProperty extends DiagnosticsProperty { /// instead of describing a property with a string value. class StringProperty extends DiagnosticsProperty { /// Create a diagnostics property for strings. - /// - /// The [showName], [quoted], [style], and [level] arguments must not be null. StringProperty( String super.name, super.value, { @@ -1956,8 +1948,6 @@ abstract class _NumProperty extends DiagnosticsProperty { /// Numeric formatting is optimized for debug message readability. class DoubleProperty extends _NumProperty { /// If specified, [unit] describes the unit for the [value] (e.g. px). - /// - /// The [showName], [style], and [level] arguments must not be null. DoubleProperty( super.name, super.value, { @@ -1974,8 +1964,6 @@ class DoubleProperty extends _NumProperty { /// /// Use if computing the property [value] may throw an exception or is /// expensive. - /// - /// The [showName] and [level] arguments must not be null. DoubleProperty.lazy( super.name, super.computeValue, { @@ -1996,8 +1984,6 @@ class DoubleProperty extends _NumProperty { /// Examples of units include 'px' and 'ms'. class IntProperty extends _NumProperty { /// Create a diagnostics property for integers. - /// - /// The [showName], [style], and [level] arguments must not be null. IntProperty( super.name, super.value, { @@ -2022,8 +2008,6 @@ class PercentProperty extends DoubleProperty { /// Setting [showName] to false is often reasonable for [PercentProperty] /// objects, as the fact that the property is shown as a percentage tends to /// be sufficient to disambiguate its meaning. - /// - /// The [showName] and [level] arguments must not be null. PercentProperty( super.name, super.fraction, { @@ -2096,8 +2080,6 @@ class FlagProperty extends DiagnosticsProperty { /// /// [showName] defaults to false as typically [ifTrue] and [ifFalse] should /// be descriptions that make the property name redundant. - /// - /// The [showName] and [level] arguments must not be null. FlagProperty( String name, { required bool? value, @@ -2196,8 +2178,6 @@ class IterableProperty extends DiagnosticsProperty> { /// empty iterable [value] is not interesting to display similar to how /// [defaultValue] is used to indicate that a specific concrete value is not /// interesting to display. - /// - /// The [style], [showName], [showSeparator], and [level] arguments must not be null. IterableProperty( String super.name, super.value, { @@ -2262,14 +2242,12 @@ class IterableProperty extends DiagnosticsProperty> { } } -/// An property than displays enum values tersely. +/// [DiagnosticsProperty] that has an [Enum] as value. /// -/// The enum value is displayed with the class name stripped. For example: +/// The enum value is displayed with the enum name stripped. For example: /// [HitTestBehavior.deferToChild] is shown as `deferToChild`. /// -/// This class can be used with classes that appear like enums but are not -/// "real" enums, so long as their `toString` implementation, in debug mode, -/// returns a string consisting of the class name followed by the value name. It +/// This class can be used with enums and returns the enum's name getter. It /// can also be used with nullable properties; the null value is represented as /// `null`. /// @@ -2277,7 +2255,7 @@ class IterableProperty extends DiagnosticsProperty> { /// /// * [DiagnosticsProperty] which documents named parameters common to all /// [DiagnosticsProperty]. -class EnumProperty extends DiagnosticsProperty { +class EnumProperty extends DiagnosticsProperty { /// Create a diagnostics property that displays an enum. /// /// The [level] argument must also not be null. @@ -2293,7 +2271,7 @@ class EnumProperty extends DiagnosticsProperty { if (value == null) { return value.toString(); } - return describeEnum(value!); + return value!.name; } } @@ -2324,8 +2302,7 @@ class ObjectFlagProperty extends DiagnosticsProperty { /// absent (null), but for which the exact value's [Object.toString] /// representation is not very transparent (e.g. a callback). /// - /// The [showName] and [level] arguments must not be null. Additionally, at - /// least one of [ifPresent] and [ifNull] must not be null. + /// At least one of [ifPresent] or [ifNull] must be non-null. ObjectFlagProperty( String super.name, super.value, { @@ -2339,8 +2316,6 @@ class ObjectFlagProperty extends DiagnosticsProperty { /// /// Only use if prefixing the property name with the word 'has' is a good /// flag name. - /// - /// The [name] and [level] arguments must not be null. ObjectFlagProperty.has( String super.name, super.value, { @@ -2517,9 +2492,6 @@ typedef ComputePropertyValueCallback = T? Function(); class DiagnosticsProperty extends DiagnosticsNode { /// Create a diagnostics property. /// - /// The [showName], [showSeparator], [style], [missingIfNull], and [level] - /// arguments must not be null. - /// /// The [level] argument is just a suggestion and can be overridden if /// something else about the property causes it to have a lower or higher /// level. For example, if the property value is null and [missingIfNull] is @@ -2556,9 +2528,6 @@ class DiagnosticsProperty extends DiagnosticsNode { /// Use if computing the property [value] may throw an exception or is /// expensive. /// - /// The [showName], [showSeparator], [style], [missingIfNull], and [level] - /// arguments must not be null. - /// /// The [level] argument is just a suggestion and can be overridden /// if something else about the property causes it to have a lower or higher /// level. For example, if calling `computeValue` throws an exception, [level] @@ -2675,7 +2644,7 @@ class DiagnosticsProperty extends DiagnosticsNode { @override String toDescription({ TextTreeConfiguration? parentConfiguration }) { if (_description != null) { - return _addTooltip(_description!); + return _addTooltip(_description); } if (exception != null) { @@ -2696,8 +2665,6 @@ class DiagnosticsProperty extends DiagnosticsNode { /// If a [tooltip] is specified, add the tooltip it to the end of `text` /// enclosing it parenthesis to disambiguate the tooltip from the rest of /// the text. - /// - /// `text` must not be null. String _addTooltip(String text) { return tooltip == null ? text : '$text ($tooltip)'; } @@ -2865,8 +2832,6 @@ class DiagnosticsProperty extends DiagnosticsNode { /// to implement [getChildren] and [getProperties]. class DiagnosticableNode extends DiagnosticsNode { /// Create a diagnostics describing a [Diagnosticable] value. - /// - /// The [value] argument must not be null. DiagnosticableNode({ super.name, required this.value, @@ -2975,11 +2940,16 @@ String describeIdentity(Object? object) => '${objectRuntimeType(object, ' max) { - return max; - } - if (x.isNaN) { - return max; - } - return x; -} diff --git a/packages/flutter/lib/src/foundation/memory_allocations.dart b/packages/flutter/lib/src/foundation/memory_allocations.dart index dd918dd625464..abc6481858c4e 100644 --- a/packages/flutter/lib/src/foundation/memory_allocations.dart +++ b/packages/flutter/lib/src/foundation/memory_allocations.dart @@ -64,6 +64,9 @@ class ObjectCreated extends ObjectEvent { }); /// Name of the instrumented library. + /// + /// The format of this parameter should be a library Uri. + /// For example: `'package:flutter/rendering.dart'`. final String library; /// Name of the instrumented class. diff --git a/packages/flutter/lib/src/foundation/node.dart b/packages/flutter/lib/src/foundation/node.dart index ca383fb1524c1..a32f30a4818c5 100644 --- a/packages/flutter/lib/src/foundation/node.dart +++ b/packages/flutter/lib/src/foundation/node.dart @@ -92,12 +92,9 @@ class AbstractNode { /// Typically called only from the [parent]'s [attach] method, and by the /// [owner] to mark the root of a tree as attached. /// - /// Subclasses with children should override this method to first call their - /// inherited [attach] method, and then [attach] all their children to the - /// same [owner]. - /// - /// Implementations of this method should start with a call to the inherited - /// method, as in `super.attach(owner)`. + /// Subclasses with children should override this method to + /// [attach] all their children to the same [owner] + /// after calling the inherited method, as in `super.attach(owner)`. @mustCallSuper void attach(covariant Object owner) { assert(_owner == null); @@ -109,11 +106,9 @@ class AbstractNode { /// Typically called only from the [parent]'s [detach], and by the [owner] to /// mark the root of a tree as detached. /// - /// Subclasses with children should override this method to first call their - /// inherited [detach] method, and then [detach] all their children. - /// - /// Implementations of this method should end with a call to the inherited - /// method, as in `super.detach()`. + /// Subclasses with children should override this method to + /// [detach] all their children after calling the inherited method, + /// as in `super.detach()`. @mustCallSuper void detach() { assert(_owner != null); diff --git a/packages/flutter/lib/src/foundation/persistent_hash_map.dart b/packages/flutter/lib/src/foundation/persistent_hash_map.dart index 411dca02b2b60..59bc692beb069 100644 --- a/packages/flutter/lib/src/foundation/persistent_hash_map.dart +++ b/packages/flutter/lib/src/foundation/persistent_hash_map.dart @@ -53,7 +53,7 @@ class PersistentHashMap { // Unfortunately can not use unsafeCast(...) here because it leads // to worse code generation on VM. - return _root!.get(0, key, key.hashCode) as V?; + return _root.get(0, key, key.hashCode) as V?; } } diff --git a/packages/flutter/lib/src/foundation/platform.dart b/packages/flutter/lib/src/foundation/platform.dart index d01d0305c9925..60d90b22d7730 100644 --- a/packages/flutter/lib/src/foundation/platform.dart +++ b/packages/flutter/lib/src/foundation/platform.dart @@ -35,7 +35,7 @@ import '_platform_io.dart' // // When adding support for a new platform (e.g. Windows Phone, Raspberry Pi), // first create a new value on the [TargetPlatform] enum, then add a rule for -// selecting that platform here. +// selecting that platform in `_platform_io.dart` and `_platform_web.dart`. // // It would be incorrect to make a platform that isn't supported by // [TargetPlatform] default to the behavior of another platform, because doing @@ -47,6 +47,15 @@ TargetPlatform get defaultTargetPlatform => platform.defaultTargetPlatform; /// The platform that user interaction should adapt to target. /// /// The [defaultTargetPlatform] getter returns the current platform. +/// +/// When using the "flutter run" command, the "o" key will toggle between +/// values of this enum when updating [debugDefaultTargetPlatformOverride]. +/// This lets one test how the application will work on various platforms +/// without having to switch emulators or physical devices. +// +// When you add values here, make sure to also add them to +// nextPlatform() in flutter_tools/lib/src/resident_runner.dart so that +// the tool can support the new platform for its "o" option. enum TargetPlatform { /// Android: android, diff --git a/packages/flutter/lib/src/foundation/print.dart b/packages/flutter/lib/src/foundation/print.dart index 7f60da7bb6cb9..123e591649a66 100644 --- a/packages/flutter/lib/src/foundation/print.dart +++ b/packages/flutter/lib/src/foundation/print.dart @@ -34,6 +34,7 @@ typedef DebugPrintCallback = void Function(String? message, { int? wrapWidth }); /// See also: /// /// * [DebugPrintCallback], for function parameters and usage details. +/// * [debugPrintThrottled], the default implementation. DebugPrintCallback debugPrint = debugPrintThrottled; /// Alternative implementation of [debugPrint] that does not throttle. @@ -48,6 +49,8 @@ void debugPrintSynchronously(String? message, { int? wrapWidth }) { /// Implementation of [debugPrint] that throttles messages. This avoids dropping /// messages on platforms that rate-limit their logging (for example, Android). +/// +/// If `wrapWidth` is not null, the message is wrapped using [debugWordWrap]. void debugPrintThrottled(String? message, { int? wrapWidth }) { final List messageLines = message?.split('\n') ?? ['null']; if (wrapWidth != null) { @@ -100,6 +103,9 @@ enum _WordWrapParseMode { inSpace, inWord, atBreak } /// Wraps the given string at the given width. /// +/// The `message` should not contain newlines (`\n`, U+000A). Strings that may +/// contain newlines should be [String.split] before being wrapped. +/// /// Wrapping occurs at space characters (U+0020). Lines that start with an /// octothorpe ("#", U+0023) are not wrapped (so for example, Dart stack traces /// won't be wrapped). diff --git a/packages/flutter/lib/src/foundation/stack_frame.dart b/packages/flutter/lib/src/foundation/stack_frame.dart index 5e22e1e82ac0f..b45c82fb731c3 100644 --- a/packages/flutter/lib/src/foundation/stack_frame.dart +++ b/packages/flutter/lib/src/foundation/stack_frame.dart @@ -22,8 +22,8 @@ import 'object.dart'; class StackFrame { /// Creates a new StackFrame instance. /// - /// All parameters must not be null. The [className] may be the empty string - /// if there is no class (e.g. for a top level library method). + /// The [className] may be the empty string if there is no class (e.g. for a + /// top level library method). const StackFrame({ required this.number, required this.column, @@ -78,32 +78,45 @@ class StackFrame { // On the Web in non-debug builds the stack trace includes the exception // message that precedes the stack trace itself. fromStackTraceLine will // return null in that case. We will skip it here. + // TODO(polina-c): if one of lines was parsed to null, the entire stack trace + // is in unexpected format and should be returned as is, without partial parsing. + // https://github.com/flutter/flutter/issues/131877 .whereType() .toList(); } - static StackFrame? _parseWebFrame(String line) { + /// Parses a single [StackFrame] from a line of a [StackTrace]. + /// + /// Returns null if format is not as expected. + static StackFrame? _tryParseWebFrame(String line) { if (kDebugMode) { - return _parseWebDebugFrame(line); + return _tryParseWebDebugFrame(line); } else { - return _parseWebNonDebugFrame(line); + return _tryParseWebNonDebugFrame(line); } } - static StackFrame _parseWebDebugFrame(String line) { + /// Parses a single [StackFrame] from a line of a [StackTrace]. + /// + /// Returns null if format is not as expected. + static StackFrame? _tryParseWebDebugFrame(String line) { // This RegExp is only partially correct for flutter run/test differences. // https://github.com/flutter/flutter/issues/52685 final bool hasPackage = line.startsWith('package'); final RegExp parser = hasPackage ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$') : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$'); - Match? match = parser.firstMatch(line); - assert(match != null, 'Expected $line to match $parser.'); - match = match!; + + final Match? match = parser.firstMatch(line); + + if (match == null) { + return null; + } String package = ''; String packageScheme = ''; String packagePath = ''; + if (hasPackage) { packageScheme = 'package'; final Uri packageUri = Uri.parse(match.group(1)!); @@ -132,7 +145,7 @@ class StackFrame { // Parses `line` as a stack frame in profile and release Web builds. If not // recognized as a stack frame, returns null. - static StackFrame? _parseWebNonDebugFrame(String line) { + static StackFrame? _tryParseWebNonDebugFrame(String line) { final Match? match = _webNonDebugFramePattern.firstMatch(line); if (match == null) { // On the Web in non-debug builds the stack trace includes the exception @@ -169,6 +182,8 @@ class StackFrame { } /// Parses a single [StackFrame] from a single line of a [StackTrace]. + /// + /// Returns null if format is not as expected. static StackFrame? fromStackTraceLine(String line) { if (line == '') { return asynchronousSuspension; @@ -185,7 +200,7 @@ class StackFrame { // Web frames. if (!line.startsWith('#')) { - return _parseWebFrame(line); + return _tryParseWebFrame(line); } final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$'); diff --git a/packages/flutter/lib/src/foundation/synchronous_future.dart b/packages/flutter/lib/src/foundation/synchronous_future.dart index ccdd1073dbdee..2b08456c69597 100644 --- a/packages/flutter/lib/src/foundation/synchronous_future.dart +++ b/packages/flutter/lib/src/foundation/synchronous_future.dart @@ -14,6 +14,8 @@ import 'dart:async'; /// rare occasions you want the ability to switch to an asynchronous model. **In /// general use of this class should be avoided as it is very difficult to debug /// such bimodal behavior.** +/// +/// A [SynchronousFuture] will never complete with an error. class SynchronousFuture implements Future { /// Creates a synchronous future. /// diff --git a/packages/flutter/lib/src/gestures/drag_details.dart b/packages/flutter/lib/src/gestures/drag_details.dart index f519c09097155..bcf91c3e884b7 100644 --- a/packages/flutter/lib/src/gestures/drag_details.dart +++ b/packages/flutter/lib/src/gestures/drag_details.dart @@ -20,8 +20,6 @@ export 'velocity_tracker.dart' show Velocity; /// * [DragEndDetails], the details for [GestureDragEndCallback]. class DragDownDetails { /// Creates details for a [GestureDragDownCallback]. - /// - /// The [globalPosition] argument must not be null. DragDownDetails({ this.globalPosition = Offset.zero, Offset? localPosition, @@ -65,8 +63,6 @@ typedef GestureDragDownCallback = void Function(DragDownDetails details); /// * [DragEndDetails], the details for [GestureDragEndCallback]. class DragStartDetails { /// Creates details for a [GestureDragStartCallback]. - /// - /// The [globalPosition] argument must not be null. DragStartDetails({ this.sourceTimeStamp, this.globalPosition = Offset.zero, @@ -128,12 +124,8 @@ typedef GestureDragStartCallback = void Function(DragStartDetails details); class DragUpdateDetails { /// Creates details for a [GestureDragUpdateCallback]. /// - /// The [delta] argument must not be null. - /// /// If [primaryDelta] is non-null, then its value must match one of the /// coordinates of [delta] and the other coordinate must be zero. - /// - /// The [globalPosition] argument must be provided and must not be null. DragUpdateDetails({ this.sourceTimeStamp, this.delta = Offset.zero, @@ -219,8 +211,6 @@ class DragEndDetails { /// If [primaryVelocity] is non-null, its value must match one of the /// coordinates of `velocity.pixelsPerSecond` and the other coordinate /// must be zero. - /// - /// The [velocity] argument must not be null. DragEndDetails({ this.velocity = Velocity.zero, this.primaryVelocity, diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index be147d6d154cc..c78f324e723c2 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -819,8 +819,6 @@ mixin _CopyPointerAddedEvent on PointerEvent { /// made contact with the surface of the device. class PointerAddedEvent extends PointerEvent with _PointerEventDescription, _CopyPointerAddedEvent { /// Creates a pointer added event. - /// - /// All of the arguments must be non-null. const PointerAddedEvent({ super.viewId, super.timeStamp, @@ -914,8 +912,6 @@ mixin _CopyPointerRemovedEvent on PointerEvent { /// detection range or might have been disconnected from the system entirely. class PointerRemovedEvent extends PointerEvent with _PointerEventDescription, _CopyPointerRemovedEvent { /// Creates a pointer removed event. - /// - /// All of the arguments must be non-null. const PointerRemovedEvent({ super.viewId, super.timeStamp, @@ -1024,8 +1020,6 @@ mixin _CopyPointerHoverEvent on PointerEvent { /// events in a widget tree. class PointerHoverEvent extends PointerEvent with _PointerEventDescription, _CopyPointerHoverEvent { /// Creates a pointer hover event. - /// - /// All of the arguments must be non-null. const PointerHoverEvent({ super.viewId, super.timeStamp, @@ -1143,8 +1137,6 @@ mixin _CopyPointerEnterEvent on PointerEvent { /// events in a widget tree. class PointerEnterEvent extends PointerEvent with _PointerEventDescription, _CopyPointerEnterEvent { /// Creates a pointer enter event. - /// - /// All of the arguments must be non-null. const PointerEnterEvent({ super.viewId, super.timeStamp, @@ -1293,8 +1285,6 @@ mixin _CopyPointerExitEvent on PointerEvent { /// events in a widget tree. class PointerExitEvent extends PointerEvent with _PointerEventDescription, _CopyPointerExitEvent { /// Creates a pointer exit event. - /// - /// All of the arguments must be non-null. const PointerExitEvent({ super.viewId, super.timeStamp, @@ -1435,8 +1425,6 @@ mixin _CopyPointerDownEvent on PointerEvent { /// events in a widget tree. class PointerDownEvent extends PointerEvent with _PointerEventDescription, _CopyPointerDownEvent { /// Creates a pointer down event. - /// - /// All of the arguments must be non-null. const PointerDownEvent({ super.viewId, super.timeStamp, @@ -1551,8 +1539,6 @@ mixin _CopyPointerMoveEvent on PointerEvent { /// events in a widget tree. class PointerMoveEvent extends PointerEvent with _PointerEventDescription, _CopyPointerMoveEvent { /// Creates a pointer move event. - /// - /// All of the arguments must be non-null. const PointerMoveEvent({ super.viewId, super.timeStamp, @@ -1668,8 +1654,6 @@ mixin _CopyPointerUpEvent on PointerEvent { /// events in a widget tree. class PointerUpEvent extends PointerEvent with _PointerEventDescription, _CopyPointerUpEvent { /// Creates a pointer up event. - /// - /// All of the arguments must be non-null. const PointerUpEvent({ super.viewId, super.timeStamp, @@ -1802,8 +1786,6 @@ mixin _CopyPointerScrollEvent on PointerEvent { /// participating agents may disambiguate an event's target. class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScrollEvent { /// Creates a pointer scroll event. - /// - /// All of the arguments must be non-null. const PointerScrollEvent({ super.viewId, super.timeStamp, @@ -1905,8 +1887,6 @@ mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent { /// participating agents may disambiguate an event's target. class PointerScrollInertiaCancelEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScrollInertiaCancelEvent { /// Creates a pointer scroll-inertia cancel event. - /// - /// All of the arguments must be non-null. const PointerScrollInertiaCancelEvent({ super.viewId, super.timeStamp, @@ -1994,8 +1974,6 @@ mixin _CopyPointerScaleEvent on PointerEvent { /// participating agents may disambiguate an event's target. class PointerScaleEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScaleEvent { /// Creates a pointer scale event. - /// - /// All of the arguments must be non-null. const PointerScaleEvent({ super.viewId, super.timeStamp, @@ -2080,8 +2058,6 @@ mixin _CopyPointerPanZoomStartEvent on PointerEvent { /// events in a widget tree. class PointerPanZoomStartEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomStartEvent { /// Creates a pointer pan/zoom start event. - /// - /// All of the arguments must be non-null. const PointerPanZoomStartEvent({ super.viewId, super.timeStamp, @@ -2183,8 +2159,6 @@ mixin _CopyPointerPanZoomUpdateEvent on PointerEvent { /// events in a widget tree. class PointerPanZoomUpdateEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomUpdateEvent { /// Creates a pointer pan/zoom update event. - /// - /// All of the arguments must be non-null. const PointerPanZoomUpdateEvent({ super.viewId, super.timeStamp, @@ -2303,8 +2277,6 @@ mixin _CopyPointerPanZoomEndEvent on PointerEvent { /// events in a widget tree. class PointerPanZoomEndEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomEndEvent { /// Creates a pointer pan/zoom end event. - /// - /// All of the arguments must be non-null. const PointerPanZoomEndEvent({ super.viewId, super.timeStamp, @@ -2397,8 +2369,6 @@ mixin _CopyPointerCancelEvent on PointerEvent { /// events in a widget tree. class PointerCancelEvent extends PointerEvent with _PointerEventDescription, _CopyPointerCancelEvent { /// Creates a pointer cancel event. - /// - /// All of the arguments must be non-null. const PointerCancelEvent({ super.viewId, super.timeStamp, diff --git a/packages/flutter/lib/src/gestures/force_press.dart b/packages/flutter/lib/src/gestures/force_press.dart index dc0d19ff93331..d17255a8f1988 100644 --- a/packages/flutter/lib/src/gestures/force_press.dart +++ b/packages/flutter/lib/src/gestures/force_press.dart @@ -46,8 +46,6 @@ enum _ForceState { class ForcePressDetails { /// Creates details for a [GestureForcePressStartCallback], /// [GestureForcePressPeakCallback] or [GestureForcePressEndCallback]. - /// - /// The [globalPosition] argument must not be null. ForcePressDetails({ required this.globalPosition, Offset? localPosition, diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart index 1c759c2161227..f5fef32b42947 100644 --- a/packages/flutter/lib/src/gestures/long_press.dart +++ b/packages/flutter/lib/src/gestures/long_press.dart @@ -107,8 +107,6 @@ typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details) class LongPressDownDetails { /// Creates the details for a [GestureLongPressDownCallback]. /// - /// The `globalPosition` argument must not be null. - /// /// If the `localPosition` argument is not specified, it will default to the /// global position. const LongPressDownDetails({ @@ -136,8 +134,6 @@ class LongPressDownDetails { /// * [LongPressEndDetails], the details for [GestureLongPressEndCallback]. class LongPressStartDetails { /// Creates the details for a [GestureLongPressStartCallback]. - /// - /// The [globalPosition] argument must not be null. const LongPressStartDetails({ this.globalPosition = Offset.zero, Offset? localPosition, @@ -159,8 +155,6 @@ class LongPressStartDetails { /// * [LongPressStartDetails], the details for [GestureLongPressStartCallback]. class LongPressMoveUpdateDetails { /// Creates the details for a [GestureLongPressMoveUpdateCallback]. - /// - /// The [globalPosition] and [offsetFromOrigin] arguments must not be null. const LongPressMoveUpdateDetails({ this.globalPosition = Offset.zero, Offset? localPosition, @@ -195,8 +189,6 @@ class LongPressMoveUpdateDetails { /// * [LongPressStartDetails], the details for [GestureLongPressStartCallback]. class LongPressEndDetails { /// Creates the details for a [GestureLongPressEndCallback]. - /// - /// The [globalPosition] argument must not be null. const LongPressEndDetails({ this.globalPosition = Offset.zero, Offset? localPosition, diff --git a/packages/flutter/lib/src/gestures/lsq_solver.dart b/packages/flutter/lib/src/gestures/lsq_solver.dart index 46a05d7036635..edd2548e9d813 100644 --- a/packages/flutter/lib/src/gestures/lsq_solver.dart +++ b/packages/flutter/lib/src/gestures/lsq_solver.dart @@ -95,8 +95,6 @@ class PolynomialFit { /// Uses the least-squares algorithm to fit a polynomial to a set of data. class LeastSquaresSolver { /// Creates a least-squares solver. - /// - /// The [x], [y], and [w] arguments must not be null. LeastSquaresSolver(this.x, this.y, this.w) : assert(x.length == y.length), assert(y.length == w.length); diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 31a2a028486b8..7904ab2be69a2 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -70,8 +70,6 @@ typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent ev abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// Initialize the object. /// - /// [dragStartBehavior] must not be null. - /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} DragGestureRecognizer({ super.debugOwner, diff --git a/packages/flutter/lib/src/gestures/multidrag.dart b/packages/flutter/lib/src/gestures/multidrag.dart index cab4e7948f518..3d850573b5272 100644 --- a/packages/flutter/lib/src/gestures/multidrag.dart +++ b/packages/flutter/lib/src/gestures/multidrag.dart @@ -32,8 +32,6 @@ typedef GestureMultiDragStartCallback = Drag? Function(Offset position); /// each pointer is a subclass of [MultiDragPointerState]. abstract class MultiDragPointerState { /// Creates per-pointer state for a [MultiDragGestureRecognizer]. - /// - /// The [initialPosition] argument must not be null. MultiDragPointerState(this.initialPosition, this.kind, this.gestureSettings) : _velocityTracker = VelocityTracker.withKind(kind); diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart index a67283c7e5df2..b1dbe36708e6a 100644 --- a/packages/flutter/lib/src/gestures/scale.dart +++ b/packages/flutter/lib/src/gestures/scale.dart @@ -96,8 +96,6 @@ class _PointerPanZoomData { /// Details for [GestureScaleStartCallback]. class ScaleStartDetails { /// Creates details for [GestureScaleStartCallback]. - /// - /// The [focalPoint] argument must not be null. ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, @@ -139,9 +137,8 @@ class ScaleStartDetails { class ScaleUpdateDetails { /// Creates details for [GestureScaleUpdateCallback]. /// - /// The [focalPoint], [scale], [horizontalScale], [verticalScale], [rotation] - /// arguments must not be null. The [scale], [horizontalScale], and [verticalScale] - /// argument must be greater than or equal to zero. + /// The [scale], [horizontalScale], and [verticalScale] arguments must be + /// greater than or equal to zero. ScaleUpdateDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, @@ -243,8 +240,6 @@ class ScaleUpdateDetails { /// Details for [GestureScaleEndCallback]. class ScaleEndDetails { /// Creates details for [GestureScaleEndCallback]. - /// - /// The [velocity] argument must not be null. ScaleEndDetails({ this.velocity = Velocity.zero, this.scaleVelocity = 0, this.pointerCount = 0 }); /// The velocity of the last pointer to be lifted off of the screen. @@ -393,6 +388,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { /// {@endtemplate} Offset trackpadScrollToScaleFactor; + /// The number of pointers being tracked by the gesture recognizer. + /// + /// Typically this is the number of fingers being used to pan the widget using the gesture + /// recognizer. + int get pointerCount { + return _pointerPanZooms.length + _pointerQueue.length; + } + late Offset _initialFocalPoint; Offset? _currentFocalPoint; late double _initialSpan; @@ -443,10 +446,6 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { return scale; } - int get _pointerCount { - return _pointerPanZooms.length + _pointerQueue.length; - } - double _computeRotationFactor() { double factor = 0.0; if (_initialLine != null && _currentLine != null) { @@ -566,7 +565,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { for (final _PointerPanZoomData p in _pointerPanZooms.values) { focalPoint += p.focalPoint; } - _currentFocalPoint = _pointerCount > 0 ? focalPoint / _pointerCount.toDouble() : Offset.zero; + _currentFocalPoint = pointerCount > 0 ? focalPoint / pointerCount.toDouble() : Offset.zero; if (previousFocalPoint == null) { _localFocalPoint = PointerEvent.transformPosition( @@ -662,9 +661,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) { velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); } - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount))); + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: pointerCount))); } else { - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount))); + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: pointerCount))); } } _state = _ScaleState.accepted; @@ -706,7 +705,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { focalPoint: _currentFocalPoint!, localFocalPoint: _localFocalPoint, rotation: _computeRotationFactor(), - pointerCount: _pointerCount, + pointerCount: pointerCount, focalPointDelta: _delta, )); }); @@ -721,7 +720,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { onStart!(ScaleStartDetails( focalPoint: _currentFocalPoint!, localFocalPoint: _localFocalPoint, - pointerCount: _pointerCount, + pointerCount: pointerCount, )); }); } diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 88ea7808fe545..d522c79911028 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -26,8 +26,6 @@ export 'events.dart' show PointerCancelEvent, PointerDownEvent, PointerEvent, Po /// * [TapGestureRecognizer], which passes this information to one of its callbacks. class TapDownDetails { /// Creates details for a [GestureTapDownCallback]. - /// - /// The [globalPosition] argument must not be null. TapDownDetails({ this.globalPosition = Offset.zero, Offset? localPosition, @@ -65,7 +63,7 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details); /// * [GestureDetector.onTapUp], which receives this information. /// * [TapGestureRecognizer], which passes this information to one of its callbacks. class TapUpDetails { - /// The [globalPosition] argument must not be null. + /// Creates a [TapUpDetails] data object. TapUpDetails({ required this.kind, this.globalPosition = Offset.zero, diff --git a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart b/packages/flutter/lib/src/gestures/tap_and_drag.dart similarity index 94% rename from packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart rename to packages/flutter/lib/src/gestures/tap_and_drag.dart index 51a8435b2f759..108beaf024ee6 100644 --- a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart +++ b/packages/flutter/lib/src/gestures/tap_and_drag.dart @@ -5,11 +5,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart' show HardwareKeyboard, LogicalKeyboardKey; -import 'framework.dart'; -import 'gesture_detector.dart'; +import 'constants.dart'; +import 'events.dart'; +import 'monodrag.dart'; +import 'recognizer.dart'; +import 'scale.dart'; +import 'tap.dart'; // Examples can assume: // void setState(VoidCallback fn) { } @@ -74,15 +76,11 @@ typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details); /// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. class TapDragDownDetails with Diagnosticable { /// Creates details for a [GestureTapDragDownCallback]. - /// - /// The [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. TapDragDownDetails({ required this.globalPosition, required this.localPosition, this.kind, required this.consecutiveTapCount, - required this.keysPressedOnDown, }); /// The global position at which the pointer contacted the screen. @@ -98,9 +96,6 @@ class TapDragDownDetails with Diagnosticable { /// the number in the series this tap is. final int consecutiveTapCount; - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -108,7 +103,6 @@ class TapDragDownDetails with Diagnosticable { properties.add(DiagnosticsProperty('localPosition', localPosition)); properties.add(DiagnosticsProperty('kind', kind)); properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); } } @@ -133,15 +127,11 @@ typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details); /// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. class TapDragUpDetails with Diagnosticable { /// Creates details for a [GestureTapDragUpCallback]. - /// - /// The [kind], [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. TapDragUpDetails({ required this.kind, required this.globalPosition, required this.localPosition, required this.consecutiveTapCount, - required this.keysPressedOnDown, }); /// The global position at which the pointer contacted the screen. @@ -157,9 +147,6 @@ class TapDragUpDetails with Diagnosticable { /// the number in the series this tap is. final int consecutiveTapCount; - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -167,7 +154,6 @@ class TapDragUpDetails with Diagnosticable { properties.add(DiagnosticsProperty('localPosition', localPosition)); properties.add(DiagnosticsProperty('kind', kind)); properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); } } @@ -192,16 +178,12 @@ typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details) /// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. class TapDragStartDetails with Diagnosticable { /// Creates details for a [GestureTapDragStartCallback]. - /// - /// The [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. TapDragStartDetails({ this.sourceTimeStamp, required this.globalPosition, required this.localPosition, this.kind, required this.consecutiveTapCount, - required this.keysPressedOnDown, }); /// Recorded timestamp of the source pointer event that triggered the drag @@ -229,9 +211,6 @@ class TapDragStartDetails with Diagnosticable { /// the number in the series this tap is. final int consecutiveTapCount; - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -240,7 +219,6 @@ class TapDragStartDetails with Diagnosticable { properties.add(DiagnosticsProperty('localPosition', localPosition)); properties.add(DiagnosticsProperty('kind', kind)); properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); } } @@ -266,14 +244,8 @@ typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails detail class TapDragUpdateDetails with Diagnosticable { /// Creates details for a [GestureTapDragUpdateCallback]. /// - /// The [delta] argument must not be null. - /// /// If [primaryDelta] is non-null, then its value must match one of the /// coordinates of [delta] and the other coordinate must be zero. - /// - /// The [globalPosition], [localPosition], [offsetFromOrigin], [localOffsetFromOrigin], - /// [consecutiveTapCount], and [keysPressedOnDown] arguments must be provided and must - /// not be null. TapDragUpdateDetails({ this.sourceTimeStamp, this.delta = Offset.zero, @@ -284,7 +256,6 @@ class TapDragUpdateDetails with Diagnosticable { required this.offsetFromOrigin, required this.localOffsetFromOrigin, required this.consecutiveTapCount, - required this.keysPressedOnDown, }) : assert( primaryDelta == null || (primaryDelta == delta.dx && delta.dy == 0.0) @@ -357,9 +328,6 @@ class TapDragUpdateDetails with Diagnosticable { /// the number in the series this tap is. final int consecutiveTapCount; - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -372,7 +340,6 @@ class TapDragUpdateDetails with Diagnosticable { properties.add(DiagnosticsProperty('offsetFromOrigin', offsetFromOrigin)); properties.add(DiagnosticsProperty('localOffsetFromOrigin', localOffsetFromOrigin)); properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); } } @@ -397,16 +364,10 @@ typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails); /// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. class TapDragEndDetails with Diagnosticable { /// Creates details for a [GestureTapDragEndCallback]. - /// - /// The [velocity] argument must not be null. - /// - /// The [consecutiveTapCount], and [keysPressedOnDown] arguments must - /// be provided and must not be null. TapDragEndDetails({ this.velocity = Velocity.zero, this.primaryVelocity, required this.consecutiveTapCount, - required this.keysPressedOnDown, }) : assert( primaryVelocity == null || primaryVelocity == velocity.pixelsPerSecond.dx @@ -434,16 +395,12 @@ class TapDragEndDetails with Diagnosticable { /// the number in the series this tap is. final int consecutiveTapCount; - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('velocity', velocity)); properties.add(DiagnosticsProperty('primaryVelocity', primaryVelocity)); properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); } } @@ -506,15 +463,6 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { // this value will be set to `0`, and a new series will begin. int get consecutiveTapCount => _consecutiveTapCount; - // The set of [LogicalKeyboardKey]s pressed when the most recent [PointerDownEvent] - // was tracked in [addAllowedPointer]. - // - // This value defaults to an empty set. - // - // When the timer between two taps elapses, the recognizer loses the arena, the gesture is cancelled - // or the recognizer is disposed of then this value is reset. - Set get keysPressedOnDown => _keysPressedOnDown ?? {}; - // The upper limit for the [consecutiveTapCount]. When this limit is reached // all tap related state is reset and a new tap series is tracked. // @@ -525,7 +473,6 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { PointerDownEvent? _down; PointerUpEvent? _up; int _consecutiveTapCount = 0; - Set? _keysPressedOnDown; OffsetPair? _originPosition; int? _previousButtons; @@ -534,6 +481,21 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { Timer? _consecutiveTapTimer; Offset? _lastTapOffset; + /// {@template flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackStart} + /// Callback used to indicate that a tap tracking has started upon + /// a [PointerDownEvent]. + /// {@endtemplate} + VoidCallback? onTapTrackStart; + + /// {@template flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackReset} + /// Callback used to indicate that a tap tracking has been reset which + /// happens on the next [PointerDownEvent] after the timer between two taps + /// elapses, the recognizer loses the arena, the gesture is cancelled or + /// the recognizer is disposed of. + /// {@endtemplate} + + VoidCallback? onTapTrackReset; + // When tracking a tap, the [consecutiveTapCount] is incremented if the given tap // falls under the tolerance specifications and reset to 1 if not. @override @@ -595,10 +557,10 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { void _trackTap(PointerDownEvent event) { _down = event; - _keysPressedOnDown = HardwareKeyboard.instance.logicalKeysPressed; _previousButtons = event.buttons; _lastTapOffset = event.position; _originPosition = OffsetPair(local: event.localPosition, global: event.position); + onTapTrackStart?.call(); } bool _hasSameButton(int buttons) { @@ -653,9 +615,9 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { _originPosition = null; _lastTapOffset = null; _consecutiveTapCount = 0; - _keysPressedOnDown = null; _down = null; _up = null; + onTapTrackReset?.call(); } } @@ -704,6 +666,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { /// pointer does travel enough distance then the recognizer that entered the arena /// first will win. The gesture detected in this case is a drag. /// +/// {@tool dartpad} +/// This example shows how to use the [TapAndPanGestureRecognizer] along with a +/// [RawGestureDetector] to scale a Widget. +/// +/// ** See code in examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart ** +/// {@end-tool} +/// /// {@tool snippet} /// /// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested @@ -1204,7 +1173,6 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize localPosition: event.localPosition, kind: getKindForPointer(event.pointer), consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, ); if (onTapDown != null) { @@ -1224,7 +1192,6 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize globalPosition: event.position, localPosition: event.localPosition, consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, ); if (onTapUp != null) { @@ -1245,7 +1212,6 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize localPosition: _initialPosition.local, kind: getKindForPointer(event.pointer), consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, ); invokeCallback('onDragStart', () => onDragStart!(details)); @@ -1267,7 +1233,6 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize offsetFromOrigin: globalPosition - _initialPosition.global, localOffsetFromOrigin: localPosition - _initialPosition.local, consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, ); if (dragUpdateThrottleFrequency != null) { @@ -1293,7 +1258,6 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize TapDragEndDetails( primaryVelocity: 0.0, consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, ); if (onDragEnd != null) { diff --git a/packages/flutter/lib/src/gestures/velocity_tracker.dart b/packages/flutter/lib/src/gestures/velocity_tracker.dart index 414eb6cc17e66..f761a1be23f8d 100644 --- a/packages/flutter/lib/src/gestures/velocity_tracker.dart +++ b/packages/flutter/lib/src/gestures/velocity_tracker.dart @@ -13,9 +13,7 @@ export 'dart:ui' show Offset, PointerDeviceKind; /// A velocity in two dimensions. @immutable class Velocity { - /// Creates a velocity. - /// - /// The [pixelsPerSecond] argument must not be null. + /// Creates a [Velocity]. const Velocity({ required this.pixelsPerSecond, }); @@ -90,8 +88,6 @@ class Velocity { /// useful velocity operations. class VelocityEstimate { /// Creates a dimensional velocity estimate. - /// - /// [pixelsPerSecond], [confidence], [duration], and [offset] must not be null. const VelocityEstimate({ required this.pixelsPerSecond, required this.confidence, @@ -153,12 +149,17 @@ class VelocityTracker { /// The kind of pointer this tracker is for. final PointerDeviceKind kind; + // Time difference since the last sample was added + final Stopwatch _sinceLastSample = Stopwatch(); + // Circular buffer; current sample at _index. final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null); int _index = 0; /// Adds a position as the given time to the tracker. void addPosition(Duration time, Offset position) { + _sinceLastSample.start(); + _sinceLastSample.reset(); _index += 1; if (_index == _historySize) { _index = 0; @@ -173,6 +174,16 @@ class VelocityTracker { /// /// Returns null if there is no data on which to base an estimate. VelocityEstimate? getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + final List x = []; final List y = []; final List w = []; @@ -199,7 +210,7 @@ class VelocityTracker { final double age = (newestSample.time - sample.time).inMicroseconds.toDouble() / 1000; final double delta = (sample.time - previousSample.time).inMicroseconds.abs().toDouble() / 1000; previousSample = sample; - if (age > _horizonMilliseconds || delta > _assumePointerMoveStoppedMilliseconds) { + if (age > _horizonMilliseconds || delta > VelocityTracker._assumePointerMoveStoppedMilliseconds) { break; } @@ -292,6 +303,8 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker { @override void addPosition(Duration time, Offset position) { + _sinceLastSample.start(); + _sinceLastSample.reset(); assert(() { final _PointAtTime? previousPoint = _touchSamples[_index]; if (previousPoint == null || previousPoint.time <= time) { @@ -330,6 +343,16 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker { @override VelocityEstimate getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + // The velocity estimated using this expression is an approximation of the // scroll velocity of an iOS scroll view at the moment the user touch was // released, not the final velocity of the iOS pan gesture recognizer @@ -391,6 +414,16 @@ class MacOSScrollViewFlingVelocityTracker extends IOSScrollViewFlingVelocityTrac @override VelocityEstimate getVelocityEstimate() { + // no recent user movement? + if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) { + return const VelocityEstimate( + pixelsPerSecond: Offset.zero, + confidence: 1.0, + duration: Duration.zero, + offset: Offset.zero, + ); + } + // The velocity estimated using this expression is an approximation of the // scroll velocity of a macOS scroll view at the moment the user touch was // released. diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index 280ef14b252cb..76462c8c6033f 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -170,9 +170,9 @@ class AboutListTile extends StatelessWidget { /// The licenses shown on the [LicensePage] are those returned by the /// [LicenseRegistry] API, which can be used to add more licenses to the list. /// -/// The [context], [useRootNavigator], [routeSettings] and [anchorPoint] -/// arguments are passed to [showDialog], the documentation for which discusses -/// how it is used. +/// The [context], [barrierDismissible], [barrierColor], [barrierLabel], +/// [useRootNavigator], [routeSettings] and [anchorPoint] arguments are +/// passed to [showDialog], the documentation for which discusses how it is used. void showAboutDialog({ required BuildContext context, String? applicationName, @@ -180,12 +180,18 @@ void showAboutDialog({ Widget? applicationIcon, String? applicationLegalese, List? children, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, bool useRootNavigator = true, RouteSettings? routeSettings, Offset? anchorPoint, }) { showDialog( context: context, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, useRootNavigator: useRootNavigator, builder: (BuildContext context) { return AboutDialog( @@ -1173,9 +1179,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp _builtLayout = _LayoutMode.nested; final MaterialPageRoute masterPageRoute = _masterPageRoute(context); - return WillPopScope( - // Push pop check into nested navigator. - onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()), + return NavigatorPopHandler( + onPop: () { + _navigatorKey.currentState!.maybePop(); + }, child: Navigator( key: _navigatorKey, initialRoute: 'initial', @@ -1228,12 +1235,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp MaterialPageRoute _detailPageRoute(Object? arguments) { return MaterialPageRoute(builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async { + return PopScope( + onPopInvoked: (bool didPop) { // No need for setState() as rebuild happens on navigation pop. focus = _Focus.master; - Navigator.of(context).pop(); - return false; }, child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)), ); diff --git a/packages/flutter/lib/src/material/action_chip.dart b/packages/flutter/lib/src/material/action_chip.dart index 725570fd56dfb..142fc4058b6fb 100644 --- a/packages/flutter/lib/src/material/action_chip.dart +++ b/packages/flutter/lib/src/material/action_chip.dart @@ -311,7 +311,7 @@ class _ActionChipDefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } diff --git a/packages/flutter/lib/src/material/action_icons_theme.dart b/packages/flutter/lib/src/material/action_icons_theme.dart index 7dd698793dfb5..6c291e6b1ebd2 100644 --- a/packages/flutter/lib/src/material/action_icons_theme.dart +++ b/packages/flutter/lib/src/material/action_icons_theme.dart @@ -114,6 +114,13 @@ class ActionIconThemeData with Diagnosticable { /// An inherited widget that overrides the default icon of [BackButtonIcon], /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this /// widget's subtree. +/// +/// {@tool dartpad} +/// This example shows how to define custom builders for drawer and back +/// buttons. +/// +/// ** See code in examples/api/lib/material/action_buttons/action_icon_theme.0.dart ** +/// {@end-tool} class ActionIconTheme extends InheritedTheme { /// Creates a theme that overrides the default icon of [BackButtonIcon], /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this diff --git a/packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart b/packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart index 0d32cfef1f2d1..87af240fbb1e9 100644 --- a/packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart +++ b/packages/flutter/lib/src/material/adaptive_text_selection_toolbar.dart @@ -103,6 +103,9 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget { required VoidCallback? onCut, required VoidCallback? onPaste, required VoidCallback? onSelectAll, + required VoidCallback? onLookUp, + required VoidCallback? onSearchWeb, + required VoidCallback? onShare, required VoidCallback? onLiveTextInput, required this.anchors, }) : children = null, @@ -112,6 +115,9 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget { onCut: onCut, onPaste: onPaste, onSelectAll: onSelectAll, + onLookUp: onLookUp, + onSearchWeb: onSearchWeb, + onShare: onShare, onLiveTextInput: onLiveTextInput ); @@ -215,6 +221,12 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget { return localizations.selectAllButtonLabel; case ContextMenuButtonType.delete: return localizations.deleteButtonTooltip.toUpperCase(); + case ContextMenuButtonType.lookUp: + return localizations.lookUpButtonLabel; + case ContextMenuButtonType.searchWeb: + return localizations.searchWebButtonLabel; + case ContextMenuButtonType.share: + return localizations.searchWebButtonLabel; case ContextMenuButtonType.liveTextInput: return localizations.scanTextButtonLabel; case ContextMenuButtonType.custom: diff --git a/packages/flutter/lib/src/material/animated_icons.dart b/packages/flutter/lib/src/material/animated_icons.dart index 659c66ba2c6ff..12ae800e750b2 100644 --- a/packages/flutter/lib/src/material/animated_icons.dart +++ b/packages/flutter/lib/src/material/animated_icons.dart @@ -6,8 +6,7 @@ library material_animated_icons; import 'dart:math' as math show pi; -import 'dart:ui' as ui show Canvas, Paint, Path; -import 'dart:ui' show lerpDouble; +import 'dart:ui' as ui show Canvas, Paint, Path, lerpDouble; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/widgets.dart'; diff --git a/packages/flutter/lib/src/material/animated_icons/animated_icons.dart b/packages/flutter/lib/src/material/animated_icons/animated_icons.dart index bc8ea27ac4790..09feb5eced18d 100644 --- a/packages/flutter/lib/src/material/animated_icons/animated_icons.dart +++ b/packages/flutter/lib/src/material/animated_icons/animated_icons.dart @@ -29,8 +29,8 @@ part of material_animated_icons; // ignore: use_string_in_part_of_directives class AnimatedIcon extends StatelessWidget { /// Creates an AnimatedIcon. /// - /// The [progress] and [icon] arguments must not be null. - /// The [size] and [color] default to the value given by the current [IconTheme]. + /// The [size] and [color] default to the value given by the current + /// [IconTheme]. const AnimatedIcon({ super.key, required this.icon, @@ -196,7 +196,7 @@ class _PathFrames { final List opacities; void paint(ui.Canvas canvas, Color color, _UiPathFactory uiPathFactory, double progress) { - final double opacity = _interpolate(opacities, progress, lerpDouble)!; + final double opacity = _interpolate(opacities, progress, ui.lerpDouble)!; final ui.Paint paint = ui.Paint() ..style = PaintingStyle.fill ..color = color.withOpacity(color.opacity * opacity); @@ -293,7 +293,7 @@ T _interpolate(List values, double progress, _Interpolator interpolator if (values.length == 1) { return values[0]; } - final double targetIdx = lerpDouble(0, values.length -1, progress)!; + final double targetIdx = ui.lerpDouble(0, values.length -1, progress)!; final int lowIdx = targetIdx.floor(); final int highIdx = targetIdx.ceil(); final double t = targetIdx - lowIdx; diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 2438c45828989..7d7d42687e5ec 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -202,8 +202,6 @@ class MaterialApp extends StatefulWidget { /// unsupported route. /// /// This class creates an instance of [WidgetsApp]. - /// - /// The boolean arguments, [routes], and [navigatorObservers], must not be null. const MaterialApp({ super.key, this.navigatorKey, @@ -214,6 +212,7 @@ class MaterialApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, + this.onNavigationNotification, List this.navigatorObservers = const [], this.builder, this.title = '', @@ -267,6 +266,7 @@ class MaterialApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, + this.onNavigationNotification, this.color, this.theme, this.darkTheme, @@ -343,6 +343,9 @@ class MaterialApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} final RouteFactory? onUnknownRoute; + /// {@macro flutter.widgets.widgetsApp.onNavigationNotification} + final NotificationListenerCallback? onNavigationNotification; + /// {@macro flutter.widgets.widgetsApp.navigatorObservers} final List? navigatorObservers; @@ -780,35 +783,26 @@ class MaterialApp extends StatefulWidget { /// discoverable, so consider adding a Scrollbar in these cases, either directly /// or through the [buildScrollbar] method. /// -/// [MaterialScrollBehavior.androidOverscrollIndicator] specifies the -/// overscroll indicator that is used on [TargetPlatform.android]. When null, -/// [ThemeData.androidOverscrollIndicator] is used. If also null, the default -/// overscroll indicator is the [GlowingOverscrollIndicator]. These properties -/// are deprecated. In order to use the [StretchingOverscrollIndicator], use -/// the [ThemeData.useMaterial3] flag, or override -/// [ScrollBehavior.buildOverscrollIndicator]. +/// [ThemeData.useMaterial3] specifies the +/// overscroll indicator that is used on [TargetPlatform.android], which +/// defaults to true, resulting in a [StretchingOverscrollIndicator]. Setting +/// [ThemeData.useMaterial3] to false will instead use a +/// [GlowingOverscrollIndicator]. /// /// See also: /// /// * [ScrollBehavior], the default scrolling behavior extended by this class. class MaterialScrollBehavior extends ScrollBehavior { /// Creates a MaterialScrollBehavior that decorates [Scrollable]s with - /// [GlowingOverscrollIndicator]s and [Scrollbar]s based on the current + /// [StretchingOverscrollIndicator]s and [Scrollbar]s based on the current /// platform and provided [ScrollableDetails]. /// - /// [MaterialScrollBehavior.androidOverscrollIndicator] specifies the - /// overscroll indicator that is used on [TargetPlatform.android]. When null, - /// [ThemeData.androidOverscrollIndicator] is used. If also null, the default - /// overscroll indicator is the [GlowingOverscrollIndicator]. - const MaterialScrollBehavior({ - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - super.androidOverscrollIndicator, - }) : _androidOverscrollIndicator = androidOverscrollIndicator; - - final AndroidOverscrollIndicator? _androidOverscrollIndicator; + /// [ThemeData.useMaterial3] specifies the + /// overscroll indicator that is used on [TargetPlatform.android], which + /// defaults to true, resulting in a [StretchingOverscrollIndicator]. Setting + /// [ThemeData.useMaterial3] to false will instead use a + /// [GlowingOverscrollIndicator]. + const MaterialScrollBehavior(); @override TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform; @@ -842,14 +836,9 @@ class MaterialScrollBehavior extends ScrollBehavior { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { // When modifying this function, consider modifying the implementation in // the base class ScrollBehavior as well. - late final AndroidOverscrollIndicator indicator; - if (Theme.of(context).useMaterial3) { - indicator = AndroidOverscrollIndicator.stretch; - } else { - indicator = _androidOverscrollIndicator - ?? Theme.of(context).androidOverscrollIndicator - ?? androidOverscrollIndicator; - } + final AndroidOverscrollIndicator indicator = Theme.of(context).useMaterial3 + ? AndroidOverscrollIndicator.stretch + : AndroidOverscrollIndicator.glow; switch (getPlatform(context)) { case TargetPlatform.iOS: case TargetPlatform.linux: @@ -889,6 +878,12 @@ class _MaterialAppState extends State { _heroController = MaterialApp.createMaterialHeroController(); } + @override + void dispose() { + _heroController.dispose(); + super.dispose(); + } + // Combine the Localizations for Material with the ones contributed // by the localizationsDelegates parameter, if any. Only the first delegate // of a particular LocalizationsDelegate.type is loaded so the @@ -1019,6 +1014,7 @@ class _MaterialAppState extends State { onGenerateRoute: widget.onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes, onUnknownRoute: widget.onUnknownRoute, + onNavigationNotification: widget.onNavigationNotification, builder: _materialBuilder, title: widget.title, onGenerateTitle: widget.onGenerateTitle, diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index d0aabc3ca0d19..621acad1463ab 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -951,22 +951,13 @@ class _AppBarState extends State { Widget? title = widget.title; if (title != null) { - bool? namesRoute; - switch (theme.platform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - namesRoute = true; - case TargetPlatform.iOS: - case TargetPlatform.macOS: - break; - } - title = _AppBarTitleBox(child: title); if (!widget.excludeHeaderSemantics) { title = Semantics( - namesRoute: namesRoute, + namesRoute: switch (theme.platform) { + TargetPlatform.android || TargetPlatform.fuchsia || TargetPlatform.linux || TargetPlatform.windows => true, + TargetPlatform.iOS || TargetPlatform.macOS => null, + }, header: true, child: title, ); @@ -982,16 +973,9 @@ class _AppBarState extends State { // Set maximum text scale factor to [_kMaxTitleTextScaleFactor] for the // title to keep the visual hierarchy the same even with larger font // sizes. To opt out, wrap the [title] widget in a [MediaQuery] widget - // with [MediaQueryData.textScaleFactor] set to - // `MediaQuery.textScaleFactorOf(context)`. - final MediaQueryData mediaQueryData = MediaQuery.of(context); - title = MediaQuery( - data: mediaQueryData.copyWith( - textScaleFactor: math.min( - mediaQueryData.textScaleFactor, - _kMaxTitleTextScaleFactor, - ), - ), + // with a different `TextScaler`. + title = MediaQuery.withClampedTextScaling( + maxScaleFactor: _kMaxTitleTextScaleFactor, child: title, ); } @@ -1277,6 +1261,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { currentExtent: math.max(minExtent, maxExtent - shrinkOffset), toolbarOpacity: toolbarOpacity, isScrolledUnder: isScrolledUnder, + hasLeading: leading != null || automaticallyImplyLeading, child: AppBar( clipBehavior: clipBehavior, leading: leading, @@ -1451,9 +1436,6 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { /// * class SliverAppBar extends StatefulWidget { /// Creates a Material Design app bar that can be placed in a [CustomScrollView]. - /// - /// The arguments [forceElevated], [primary], [floating], [pinned], [snap] - /// and [automaticallyImplyLeading] must not be null. const SliverAppBar({ super.key, this.leading, @@ -2098,7 +2080,6 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget { Widget build(BuildContext context) { late final AppBarTheme appBarTheme = AppBarTheme.of(context); late final AppBarTheme defaults = Theme.of(context).useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context); - final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), _kMaxTitleTextScaleFactor); // TODO(tahatesser): Add link to Material spec when available, https://github.com/flutter/flutter/issues/58769. final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType()!; final _ScrollUnderFlexibleConfig config = configBuilder(context); assert( @@ -2125,10 +2106,10 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget { // Set maximum text scale factor to [_kMaxTitleTextScaleFactor] for the // title to keep the visual hierarchy the same even with larger font // sizes. To opt out, wrap the [title] widget in a [MediaQuery] widget - // with [MediaQueryData.textScaleFactor] set to - // `MediaQuery.textScaleFactorOf(context)`. - return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor), + // with a different TextScaler. + // TODO(tahatesser): Add link to Material spec when available, https://github.com/flutter/flutter/issues/58769. + return MediaQuery.withClampedTextScaling( + maxScaleFactor: _kMaxTitleTextScaleFactor, // This column will assume the full height of the parent Stack. child: Column( children: [ diff --git a/packages/flutter/lib/src/material/app_bar_theme.dart b/packages/flutter/lib/src/material/app_bar_theme.dart index adab8d2addf5a..c8528b5d7d99a 100644 --- a/packages/flutter/lib/src/material/app_bar_theme.dart +++ b/packages/flutter/lib/src/material/app_bar_theme.dart @@ -200,8 +200,6 @@ class AppBarTheme with Diagnosticable { /// Linearly interpolate between two AppBar themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static AppBarTheme lerp(AppBarTheme? a, AppBarTheme? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index 9f00633c9cc1e..c078018a6a0d5 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -66,6 +66,7 @@ class Autocomplete extends StatelessWidget { this.onSelected, this.optionsMaxHeight = 200.0, this.optionsViewBuilder, + this.optionsViewOpenDirection = OptionsViewOpenDirection.down, this.initialValue, }); @@ -90,6 +91,9 @@ class Autocomplete extends StatelessWidget { /// default. final AutocompleteOptionsViewBuilder? optionsViewBuilder; + /// {@macro flutter.widgets.RawAutocomplete.optionsViewOpenDirection} + final OptionsViewOpenDirection optionsViewOpenDirection; + /// The maximum height used for the default Material options list widget. /// /// When [optionsViewBuilder] is `null`, this property sets the maximum height @@ -116,6 +120,7 @@ class Autocomplete extends StatelessWidget { fieldViewBuilder: fieldViewBuilder, initialValue: initialValue, optionsBuilder: optionsBuilder, + optionsViewOpenDirection: optionsViewOpenDirection, optionsViewBuilder: optionsViewBuilder ?? (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { return _AutocompleteOptions( displayStringForOption: displayStringForOption, diff --git a/packages/flutter/lib/src/material/banner.dart b/packages/flutter/lib/src/material/banner.dart index b3a47a56a793e..de2ef0d08ed1a 100644 --- a/packages/flutter/lib/src/material/banner.dart +++ b/packages/flutter/lib/src/material/banner.dart @@ -93,9 +93,8 @@ enum MaterialBannerClosedReason { class MaterialBanner extends StatefulWidget { /// Creates a [MaterialBanner]. /// - /// The [actions], [content], and [forceActionsBelow] must be non-null. - /// The [actions].length must be greater than 0. The [elevation] must be null or - /// non-negative. + /// The length of the [actions] list must not be empty. The [elevation] must + /// be null or non-negative. const MaterialBanner({ super.key, required this.content, diff --git a/packages/flutter/lib/src/material/banner_theme.dart b/packages/flutter/lib/src/material/banner_theme.dart index e2d973fd5ffef..8792204ef8380 100644 --- a/packages/flutter/lib/src/material/banner_theme.dart +++ b/packages/flutter/lib/src/material/banner_theme.dart @@ -97,8 +97,6 @@ class MaterialBannerThemeData with Diagnosticable { /// Linearly interpolate between two Banner themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static MaterialBannerThemeData lerp(MaterialBannerThemeData? a, MaterialBannerThemeData? b, double t) { return MaterialBannerThemeData( diff --git a/packages/flutter/lib/src/material/bottom_app_bar_theme.dart b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart index c056894899b64..d2516e647a413 100644 --- a/packages/flutter/lib/src/material/bottom_app_bar_theme.dart +++ b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart @@ -94,8 +94,6 @@ class BottomAppBarTheme with Diagnosticable { /// Linearly interpolate between two BAB themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static BottomAppBarTheme lerp(BottomAppBarTheme? a, BottomAppBarTheme? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index f85a58e8e9c32..072fccce82d60 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -80,8 +80,8 @@ enum BottomNavigationBarLandscapeLayout { /// [BottomNavigationBarType.fixed] when there are less than four items, and /// [BottomNavigationBarType.shifting] otherwise. /// -/// The length of [items] must be at least two and each item's icon and title/label -/// must not be null. +/// The length of [items] must be at least two and each item's icon and +/// label must not be null. /// /// * [BottomNavigationBarType.fixed], the default when there are less than /// four [items]. The selected item is rendered with the @@ -188,7 +188,7 @@ class BottomNavigationBar extends StatefulWidget { /// are two or three [items], [BottomNavigationBarType.shifting] otherwise. /// /// The [iconSize], [selectedFontSize], [unselectedFontSize], and [elevation] - /// arguments must be non-null and non-negative. + /// arguments must be non-negative. /// /// If [selectedLabelStyle].color and [unselectedLabelStyle].color values /// are non-null, they will be used instead of [selectedItemColor] and @@ -784,11 +784,8 @@ class _Label extends StatelessWidget { if (item.label != null) { // Do not grow text in bottom navigation bar when we can show a tooltip // instead. - final MediaQueryData mediaQueryData = MediaQuery.of(context); - text = MediaQuery( - data: mediaQueryData.copyWith( - textScaleFactor: math.min(1.0, mediaQueryData.textScaleFactor), - ), + text = MediaQuery.withClampedTextScaling( + maxScaleFactor: 1.0, child: text, ); } diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart b/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart index b6d3ad6bff4cc..d60fa6b016d8d 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart @@ -170,8 +170,6 @@ class BottomNavigationBarThemeData with Diagnosticable { /// Linearly interpolate between two [BottomNavigationBarThemeData]. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static BottomNavigationBarThemeData lerp(BottomNavigationBarThemeData? a, BottomNavigationBarThemeData? b, double t) { if (identical(a, b) && a != null) { @@ -276,8 +274,6 @@ class BottomNavigationBarThemeData with Diagnosticable { class BottomNavigationBarTheme extends InheritedWidget { /// Constructs a bottom navigation bar theme that configures all descendant /// [BottomNavigationBar] widgets. - /// - /// The [data] must not be null. const BottomNavigationBarTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index c59afd7889124..75f8b3e7796e2 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -26,6 +26,7 @@ const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); const Curve _modalBottomSheetCurve = decelerateEasing; const double _minFlingVelocity = 700.0; const double _closeProgressThreshold = 0.5; +const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0; /// A callback for when the user begins dragging the bottom sheet. /// @@ -122,6 +123,10 @@ class BottomSheet extends StatefulWidget { /// because the drag handle is always draggable. /// /// Default is true. + /// + /// If this is true, the [animationController] must not be null. + /// Use [BottomSheet.createAnimationController] to create one, or provide + /// another AnimationController. final bool enableDrag; /// Specifies whether a drag handle is shown. @@ -133,6 +138,10 @@ class BottomSheet extends StatefulWidget { /// /// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If /// that is also null, defaults to false. + /// + /// If this is true, the [animationController] must not be null. + /// Use [BottomSheet.createAnimationController] to create one, or provide + /// another AnimationController. final bool? showDragHandle; /// The bottom sheet drag handle's color. @@ -431,7 +440,7 @@ class _DragHandle extends StatelessWidget { }); final VoidCallback? onSemanticsTap; - final Function(bool) handleHover; + final ValueChanged handleHover; final Set materialState; final Color? dragHandleColor; final Size? dragHandleSize; @@ -471,24 +480,26 @@ class _DragHandle extends StatelessWidget { } class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget { - const _BottomSheetLayoutWithSizeListener({ + required this.onChildSizeChanged, required this.animationValue, required this.isScrollControlled, - required this.onChildSizeChanged, + required this.scrollControlDisabledMaxHeightRatio, super.child, }); + final _SizeChangeCallback onChildSizeChanged; final double animationValue; final bool isScrollControlled; - final _SizeChangeCallback onChildSizeChanged; + final double scrollControlDisabledMaxHeightRatio; @override _RenderBottomSheetLayoutWithSizeListener createRenderObject(BuildContext context) { return _RenderBottomSheetLayoutWithSizeListener( + onChildSizeChanged: onChildSizeChanged, animationValue: animationValue, isScrollControlled: isScrollControlled, - onChildSizeChanged: onChildSizeChanged, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, ); } @@ -497,6 +508,7 @@ class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget { renderObject.onChildSizeChanged = onChildSizeChanged; renderObject.animationValue = animationValue; renderObject.isScrollControlled = isScrollControlled; + renderObject.scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio; } } @@ -506,9 +518,11 @@ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { required _SizeChangeCallback onChildSizeChanged, required double animationValue, required bool isScrollControlled, - }) : _animationValue = animationValue, + required double scrollControlDisabledMaxHeightRatio, + }) : _onChildSizeChanged = onChildSizeChanged, + _animationValue = animationValue, _isScrollControlled = isScrollControlled, - _onChildSizeChanged = onChildSizeChanged, + _scrollControlDisabledMaxHeightRatio = scrollControlDisabledMaxHeightRatio, super(child); Size _lastSize = Size.zero; @@ -546,6 +560,17 @@ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { markNeedsLayout(); } + double get scrollControlDisabledMaxHeightRatio => _scrollControlDisabledMaxHeightRatio; + double _scrollControlDisabledMaxHeightRatio; + set scrollControlDisabledMaxHeightRatio(double newValue) { + if (_scrollControlDisabledMaxHeightRatio == newValue) { + return; + } + + _scrollControlDisabledMaxHeightRatio = newValue; + markNeedsLayout(); + } + Size _getSize(BoxConstraints constraints) { return constraints.constrain(constraints.biggest); } @@ -591,13 +616,13 @@ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox { return _getSize(constraints); } - BoxConstraints _getConstraintsForChild(BoxConstraints constraints) { + BoxConstraints _getConstraintsForChild(BoxConstraints constraints) { return BoxConstraints( minWidth: constraints.maxWidth, maxWidth: constraints.maxWidth, maxHeight: isScrollControlled - ? constraints.maxHeight - : constraints.maxHeight * 9.0 / 16.0, + ? constraints.maxHeight + : constraints.maxHeight * scrollControlDisabledMaxHeightRatio, ); } @@ -634,12 +659,14 @@ class _ModalBottomSheet extends StatefulWidget { this.clipBehavior, this.constraints, this.isScrollControlled = false, + this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio, this.enableDrag = true, this.showDragHandle = false, }); final ModalBottomSheetRoute route; final bool isScrollControlled; + final double scrollControlDisabledMaxHeightRatio; final Color? backgroundColor; final double? elevation; final ShapeBorder? shape; @@ -730,6 +757,7 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { }, animationValue: animationValue, isScrollControlled: widget.isScrollControlled, + scrollControlDisabledMaxHeightRatio: widget.scrollControlDisabledMaxHeightRatio, child: child, ), ), @@ -815,6 +843,7 @@ class ModalBottomSheetRoute extends PopupRoute { this.enableDrag = true, this.showDragHandle, required this.isScrollControlled, + this.scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio, super.settings, this.transitionAnimationController, this.anchorPoint, @@ -842,6 +871,13 @@ class ModalBottomSheetRoute extends PopupRoute { /// to have the bottom sheet be draggable. final bool isScrollControlled; + /// The max height constraint ratio for the bottom sheet + /// when [isScrollControlled] set to false, + /// no ratio will be applied when [isScrollControlled] set to true. + /// + /// Defaults to 9 / 16. + final double scrollControlDisabledMaxHeightRatio; + /// The bottom sheet's background color. /// /// Defines the bottom sheet's [Material.color]. @@ -969,6 +1005,12 @@ class ModalBottomSheetRoute extends PopupRoute { final ValueNotifier _clipDetailsNotifier = ValueNotifier(EdgeInsets.zero); + @override + void dispose() { + _clipDetailsNotifier.dispose(); + super.dispose(); + } + /// Updates the details regarding how the [SemanticsNode.rect] (focus) of /// the barrier for this [ModalBottomSheetRoute] should be clipped. /// @@ -1026,6 +1068,7 @@ class ModalBottomSheetRoute extends PopupRoute { clipBehavior: clipBehavior, constraints: constraints, isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, enableDrag: enableDrag, showDragHandle: showDragHandle ?? (enableDrag && (sheetTheme.showDragHandle ?? false)), ); @@ -1090,8 +1133,6 @@ class ModalBottomSheetRoute extends PopupRoute { /// curve specified with the [curve] argument, after the finger is released. In /// such a case, the value of [startingPoint] would be the progress of the /// animation at the time when the finger was released. -/// -/// The [startingPoint] and [curve] arguments must not be null. class _BottomSheetSuspendedCurve extends ParametricCurve { /// Creates a suspended curve. const _BottomSheetSuspendedCurve( @@ -1192,6 +1233,7 @@ Future showModalBottomSheet({ BoxConstraints? constraints, Color? barrierColor, bool isScrollControlled = false, + double scrollControlDisabledMaxHeightRatio = _defaultScrollControlDisabledMaxHeightRatio, bool useRootNavigator = false, bool isDismissible = true, bool enableDrag = true, @@ -1210,6 +1252,7 @@ Future showModalBottomSheet({ builder: builder, capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, barrierLabel: barrierLabel ?? localizations.scrimLabel, barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel), backgroundColor: backgroundColor, diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index 917123558b1cc..bda8513ebffa6 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -40,11 +40,8 @@ import 'theme_data.dart'; class RawMaterialButton extends StatefulWidget { /// Create a button based on [Semantics], [Material], and [InkWell] widgets. /// - /// The [shape], [elevation], [focusElevation], [hoverElevation], - /// [highlightElevation], [disabledElevation], [padding], [constraints], - /// [autofocus], and [clipBehavior] arguments must not be null. Additionally, - /// [elevation], [focusElevation], [hoverElevation], [highlightElevation], and - /// [disabledElevation] must be non-negative. + /// The [elevation], [focusElevation], [hoverElevation], [highlightElevation], + /// and [disabledElevation] parameters must be non-negative. const RawMaterialButton({ super.key, required this.onPressed, @@ -288,7 +285,7 @@ class RawMaterialButton extends StatefulWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// Whether detected gestures should provide acoustic and/or haptic feedback. diff --git a/packages/flutter/lib/src/material/button_bar_theme.dart b/packages/flutter/lib/src/material/button_bar_theme.dart index 9f3aac0ca8858..2251b940a0c11 100644 --- a/packages/flutter/lib/src/material/button_bar_theme.dart +++ b/packages/flutter/lib/src/material/button_bar_theme.dart @@ -233,8 +233,6 @@ class ButtonBarThemeData with Diagnosticable { class ButtonBarTheme extends InheritedWidget { /// Constructs a button bar theme that configures all descendent [ButtonBar] /// widgets. - /// - /// The [data] must not be null. const ButtonBarTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/button_style_button.dart b/packages/flutter/lib/src/material/button_style_button.dart index 9155e49ef69a4..1568ce0353d17 100644 --- a/packages/flutter/lib/src/material/button_style_button.dart +++ b/packages/flutter/lib/src/material/button_style_button.dart @@ -89,7 +89,7 @@ abstract class ButtonStyleButton extends StatefulWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// {@macro flutter.widgets.Focus.focusNode} @@ -184,15 +184,12 @@ abstract class ButtonStyleButton extends StatefulWidget { EdgeInsetsGeometry geometry3x, double textScaleFactor, ) { - - if (textScaleFactor <= 1) { - return geometry1x; - } else if (textScaleFactor >= 3) { - return geometry3x; - } else if (textScaleFactor <= 2) { - return EdgeInsetsGeometry.lerp(geometry1x, geometry2x, textScaleFactor - 1)!; - } - return EdgeInsetsGeometry.lerp(geometry2x, geometry3x, textScaleFactor - 2)!; + return switch (textScaleFactor) { + <= 1 => geometry1x, + < 2 => EdgeInsetsGeometry.lerp(geometry1x, geometry2x, textScaleFactor - 1)!, + < 3 => EdgeInsetsGeometry.lerp(geometry2x, geometry3x, textScaleFactor - 2)!, + _ => geometry3x, + }; } } diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart index 1406f2deb7672..e41ea025e58c0 100644 --- a/packages/flutter/lib/src/material/button_theme.dart +++ b/packages/flutter/lib/src/material/button_theme.dart @@ -66,9 +66,6 @@ enum ButtonBarLayoutBehavior { /// depend on any inherited themes. class ButtonTheme extends InheritedTheme { /// Creates a button theme. - /// - /// The [textTheme], [minWidth], [height], and [colorScheme] arguments - /// must not be null. ButtonTheme({ super.key, ButtonTextTheme textTheme = ButtonTextTheme.normal, @@ -108,8 +105,6 @@ class ButtonTheme extends InheritedTheme { ); /// Creates a button theme from [data]. - /// - /// The [data] argument must not be null. const ButtonTheme.fromButtonThemeData({ super.key, required this.data, @@ -168,9 +163,7 @@ class ButtonThemeData with Diagnosticable { /// Create a button theme object that can be used with [ButtonTheme] /// or [ThemeData]. /// - /// The [textTheme], [minWidth], [height], [alignedDropdown], and - /// [layoutBehavior] parameters must not be null. The [minWidth] and - /// [height] parameters must greater than or equal to zero. + /// The [minWidth] and [height] parameters must greater than or equal to zero. /// /// The ButtonTheme's methods that have a [MaterialButton] parameter and /// have a name with a `get` prefix are used to configure a @@ -249,7 +242,7 @@ class ButtonThemeData with Diagnosticable { /// child (typically the button's label). EdgeInsetsGeometry get padding { if (_padding != null) { - return _padding!; + return _padding; } switch (textTheme) { case ButtonTextTheme.normal: @@ -277,7 +270,7 @@ class ButtonThemeData with Diagnosticable { /// [Material]. ShapeBorder get shape { if (_shape != null) { - return _shape!; + return _shape; } switch (textTheme) { case ButtonTextTheme.normal: @@ -537,7 +530,7 @@ class ButtonThemeData with Diagnosticable { switch (getTextTheme(button)) { case ButtonTextTheme.normal: case ButtonTextTheme.accent: - return _splashColor!; + return _splashColor; case ButtonTextTheme.primary: break; } @@ -641,12 +634,8 @@ class ButtonThemeData with Diagnosticable { return button.padding!; } - if (button is MaterialButtonWithIconMixin) { - return const EdgeInsetsDirectional.only(start: 12.0, end: 16.0); - } - if (_padding != null) { - return _padding!; + return _padding; } switch (getTextTheme(button)) { diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 641ad3ba5e124..e3cefd4775429 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -60,8 +60,9 @@ const double _monthNavButtonsWidth = 108.0; class CalendarDatePicker extends StatefulWidget { /// Creates a calendar date picker. /// - /// It will display a grid of days for the [initialDate]'s month. The day - /// indicated by [initialDate] will be selected. + /// It will display a grid of days for the [initialDate]'s month, or, if that + /// is null, the [currentDate]'s month. The day indicated by [initialDate] will + /// be selected if it is not null. /// /// The optional [onDisplayedMonthChanged] callback can be used to track /// the currently displayed month. @@ -71,23 +72,20 @@ class CalendarDatePicker extends StatefulWidget { /// to start in the year selection interface with [initialCalendarMode] set /// to [DatePickerMode.year]. /// - /// The [initialDate], [firstDate], [lastDate], [onDateChanged], and - /// [initialCalendarMode] must be non-null. + /// The [lastDate] must be after or equal to [firstDate]. /// - /// [lastDate] must be after or equal to [firstDate]. + /// The [initialDate], if provided, must be between [firstDate] and [lastDate] + /// or equal to one of them. /// - /// [initialDate] must be between [firstDate] and [lastDate] or equal to - /// one of them. - /// - /// [currentDate] represents the current day (i.e. today). This + /// The [currentDate] represents the current day (i.e. today). This /// date will be highlighted in the day grid. If null, the date of /// `DateTime.now()` will be used. /// - /// If [selectableDayPredicate] is non-null, it must return `true` for the - /// [initialDate]. + /// If [selectableDayPredicate] and [initialDate] are both non-null, + /// [selectableDayPredicate] must return `true` for the [initialDate]. CalendarDatePicker({ super.key, - required DateTime initialDate, + required DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -95,7 +93,7 @@ class CalendarDatePicker extends StatefulWidget { this.onDisplayedMonthChanged, this.initialCalendarMode = DatePickerMode.day, this.selectableDayPredicate, - }) : initialDate = DateUtils.dateOnly(initialDate), + }) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate), firstDate = DateUtils.dateOnly(firstDate), lastDate = DateUtils.dateOnly(lastDate), currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { @@ -104,21 +102,26 @@ class CalendarDatePicker extends StatefulWidget { 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isBefore(this.firstDate), + this.initialDate == null || !this.initialDate!.isBefore(this.firstDate), 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isAfter(this.lastDate), + this.initialDate == null || !this.initialDate!.isAfter(this.lastDate), 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.', ); assert( - selectableDayPredicate == null || selectableDayPredicate!(this.initialDate), + selectableDayPredicate == null || this.initialDate == null || selectableDayPredicate!(this.initialDate!), 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.', ); } /// The initially selected [DateTime] that the picker should display. - final DateTime initialDate; + /// + /// Subsequently changing this has no effect. To change the selected date, + /// change the [key] to create a new instance of the [CalendarDatePicker], and + /// provide that widget the new [initialDate]. This will reset the widget's + /// interactive state. + final DateTime? initialDate; /// The earliest allowable [DateTime] that the user can select. final DateTime firstDate; @@ -136,6 +139,11 @@ class CalendarDatePicker extends StatefulWidget { final ValueChanged? onDisplayedMonthChanged; /// The initial display of the calendar picker. + /// + /// Subsequently changing this has no effect. To change the calendar mode, + /// change the [key] to create a new instance of the [CalendarDatePicker], and + /// provide that widget a new [initialCalendarMode]. This will reset the + /// widget's interactive state. final DatePickerMode initialCalendarMode; /// Function to provide full control over which dates in the calendar can be selected. @@ -149,7 +157,7 @@ class _CalendarDatePickerState extends State { bool _announcedInitialDate = false; late DatePickerMode _mode; late DateTime _currentDisplayedMonthDate; - late DateTime _selectedDate; + DateTime? _selectedDate; final GlobalKey _monthPickerKey = GlobalKey(); final GlobalKey _yearPickerKey = GlobalKey(); late MaterialLocalizations _localizations; @@ -159,18 +167,9 @@ class _CalendarDatePickerState extends State { void initState() { super.initState(); _mode = widget.initialCalendarMode; - _currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month); - _selectedDate = widget.initialDate; - } - - @override - void didUpdateWidget(CalendarDatePicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialCalendarMode != oldWidget.initialCalendarMode) { - _mode = widget.initialCalendarMode; - } - if (!DateUtils.isSameDay(widget.initialDate, oldWidget.initialDate)) { - _currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month); + final DateTime currentDisplayedDate = widget.initialDate ?? widget.currentDate; + _currentDisplayedMonthDate = DateTime(currentDisplayedDate.year, currentDisplayedDate.month); + if (widget.initialDate != null) { _selectedDate = widget.initialDate; } } @@ -183,12 +182,13 @@ class _CalendarDatePickerState extends State { assert(debugCheckHasDirectionality(context)); _localizations = MaterialLocalizations.of(context); _textDirection = Directionality.of(context); - if (!_announcedInitialDate) { + if (!_announcedInitialDate && widget.initialDate != null) { + assert(_selectedDate != null); _announcedInitialDate = true; final bool isToday = DateUtils.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.announce( - '${_localizations.formatFullDate(_selectedDate)}$semanticLabelSuffix', + '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, ); } @@ -211,16 +211,18 @@ class _CalendarDatePickerState extends State { _vibrate(); setState(() { _mode = mode; - if (_mode == DatePickerMode.day) { - SemanticsService.announce( - _localizations.formatMonthYear(_selectedDate), - _textDirection, - ); - } else { - SemanticsService.announce( - _localizations.formatYear(_selectedDate), - _textDirection, - ); + if (_selectedDate != null) { + if (_mode == DatePickerMode.day) { + SemanticsService.announce( + _localizations.formatMonthYear(_selectedDate!), + _textDirection, + ); + } else { + SemanticsService.announce( + _localizations.formatYear(_selectedDate!), + _textDirection, + ); + } } }); } @@ -237,6 +239,10 @@ class _CalendarDatePickerState extends State { void _handleYearChanged(DateTime value) { _vibrate(); + final int daysInMonth = DateUtils.getDaysInMonth(value.year, value.month); + final int preferredDay = math.min(_selectedDate?.day ?? 1, daysInMonth); + value = value.copyWith(day: preferredDay); + if (value.isBefore(widget.firstDate)) { value = widget.firstDate; } else if (value.isAfter(widget.lastDate)) { @@ -246,6 +252,11 @@ class _CalendarDatePickerState extends State { setState(() { _mode = DatePickerMode.day; _handleMonthChanged(value); + + if (_isSelectable(value)) { + _selectedDate = value; + widget.onDateChanged(_selectedDate!); + } }); } @@ -253,10 +264,14 @@ class _CalendarDatePickerState extends State { _vibrate(); setState(() { _selectedDate = value; - widget.onDateChanged(_selectedDate); + widget.onDateChanged(_selectedDate!); }); } + bool _isSelectable(DateTime date) { + return widget.selectableDayPredicate == null || widget.selectableDayPredicate!.call(date); + } + Widget _buildPicker() { switch (_mode) { case DatePickerMode.day: @@ -279,7 +294,6 @@ class _CalendarDatePickerState extends State { currentDate: widget.currentDate, firstDate: widget.firstDate, lastDate: widget.lastDate, - initialDate: _currentDisplayedMonthDate, selectedDate: _currentDisplayedMonthDate, onChanged: _handleYearChanged, ), @@ -439,10 +453,15 @@ class _MonthPicker extends StatefulWidget { required this.onDisplayedMonthChanged, this.selectableDayPredicate, }) : assert(!firstDate.isAfter(lastDate)), - assert(!selectedDate.isBefore(firstDate)), - assert(!selectedDate.isAfter(lastDate)); + assert(selectedDate == null || !selectedDate.isBefore(firstDate)), + assert(selectedDate == null || !selectedDate.isAfter(lastDate)); /// The initial month to display. + /// + /// Subsequently changing this has no effect. To change the selected month, + /// change the [key] to create a new instance of the [_MonthPicker], and + /// provide that widget the new [initialMonth]. This will reset the widget's + /// interactive state. final DateTime initialMonth; /// The current date. @@ -463,7 +482,7 @@ class _MonthPicker extends StatefulWidget { /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// Called when the user picks a day. final ValueChanged onChanged; @@ -515,17 +534,6 @@ class _MonthPickerState extends State<_MonthPicker> { _textDirection = Directionality.of(context); } - @override - void didUpdateWidget(_MonthPicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialMonth != oldWidget.initialMonth && widget.initialMonth != _currentMonth) { - // We can't interrupt this widget build with a scroll, so do it next frame - WidgetsBinding.instance.addPostFrameCallback( - (Duration timeStamp) => _showMonth(widget.initialMonth, jump: true), - ); - } - } - @override void dispose() { _pageController.dispose(); @@ -821,13 +829,13 @@ class _DayPicker extends StatefulWidget { required this.onChanged, this.selectableDayPredicate, }) : assert(!firstDate.isAfter(lastDate)), - assert(!selectedDate.isBefore(firstDate)), - assert(!selectedDate.isAfter(lastDate)); + assert(selectedDate == null || !selectedDate.isBefore(firstDate)), + assert(selectedDate == null || !selectedDate.isAfter(lastDate)); /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// The current date at the time the picker is displayed. final DateTime currentDate; @@ -907,14 +915,11 @@ class _DayPickerState extends State<_DayPicker> { /// List _dayHeaders(TextStyle? headerStyle, MaterialLocalizations localizations) { final List result = []; - for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { + for (int i = localizations.firstDayOfWeekIndex; result.length < DateTime.daysPerWeek; i = (i + 1) % DateTime.daysPerWeek) { final String weekday = localizations.narrowWeekdays[i]; result.add(ExcludeSemantics( child: Center(child: Text(weekday, style: headerStyle)), )); - if (i == (localizations.firstDayOfWeekIndex - 1) % 7) { - break; - } } return result; } @@ -925,7 +930,6 @@ class _DayPickerState extends State<_DayPicker> { final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context); final DatePickerThemeData defaults = DatePickerTheme.defaults(context); final TextStyle? weekdayStyle = datePickerTheme.weekdayStyle ?? defaults.weekdayStyle; - final TextStyle? dayStyle = datePickerTheme.dayStyle ?? defaults.dayStyle; final int year = widget.displayedMonth.year; final int month = widget.displayedMonth.month; @@ -933,18 +937,6 @@ class _DayPickerState extends State<_DayPicker> { final int daysInMonth = DateUtils.getDaysInMonth(year, month); final int dayOffset = DateUtils.firstDayOffset(year, month, localizations); - T? effectiveValue(T? Function(DatePickerThemeData? theme) getProperty) { - return getProperty(datePickerTheme) ?? getProperty(defaults); - } - - T? resolve(MaterialStateProperty? Function(DatePickerThemeData? theme) getProperty, Set states) { - return effectiveValue( - (DatePickerThemeData? theme) { - return getProperty(theme)?.resolve(states); - }, - ); - } - final List dayItems = _dayHeaders(weekdayStyle, localizations); // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on // a leap year. @@ -961,68 +953,18 @@ class _DayPickerState extends State<_DayPicker> { (widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild)); final bool isSelectedDay = DateUtils.isSameDay(widget.selectedDate, dayToBuild); final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild); - final String semanticLabelSuffix = isToday ? ', ${localizations.currentDateLabel}' : ''; - - final Set states = { - if (isDisabled) MaterialState.disabled, - if (isSelectedDay) MaterialState.selected, - }; - final Color? dayForegroundColor = resolve((DatePickerThemeData? theme) => isToday ? theme?.todayForegroundColor : theme?.dayForegroundColor, states); - final Color? dayBackgroundColor = resolve((DatePickerThemeData? theme) => isToday ? theme?.todayBackgroundColor : theme?.dayBackgroundColor, states); - final MaterialStateProperty dayOverlayColor = MaterialStateProperty.resolveWith( - (Set states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), - ); - final BoxDecoration decoration = isToday - ? BoxDecoration( - color: dayBackgroundColor, - border: Border.fromBorderSide( - (datePickerTheme.todayBorder ?? defaults.todayBorder!) - .copyWith(color: dayForegroundColor) - ), - shape: BoxShape.circle, - ) - : BoxDecoration( - color: dayBackgroundColor, - shape: BoxShape.circle, - ); - - Widget dayWidget = Container( - decoration: decoration, - child: Center( - child: Text(localizations.formatDecimal(day), style: dayStyle?.apply(color: dayForegroundColor)), + dayItems.add( + _Day( + dayToBuild, + key: ValueKey(dayToBuild), + isDisabled: isDisabled, + isSelectedDay: isSelectedDay, + isToday: isToday, + onChanged: widget.onChanged, + focusNode: _dayFocusNodes[day - 1], ), ); - - if (isDisabled) { - dayWidget = ExcludeSemantics( - child: dayWidget, - ); - } else { - dayWidget = InkResponse( - focusNode: _dayFocusNodes[day - 1], - onTap: () => widget.onChanged(dayToBuild), - radius: _dayPickerRowHeight / 2 + 4, - statesController: MaterialStatesController(states), - overlayColor: dayOverlayColor, - child: Semantics( - // We want the day of month to be spoken first irrespective of the - // locale-specific preferences or TextDirection. This is because - // an accessibility user is more likely to be interested in the - // day of month before the rest of the date, as they are looking - // for the day of month. To do that we prepend day of month to the - // formatted full date. - label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}$semanticLabelSuffix', - // Set button to true to make the date selectable. - button: true, - selected: isSelectedDay, - excludeSemantics: true, - child: dayWidget, - ), - ); - } - - dayItems.add(dayWidget); } } @@ -1042,6 +984,122 @@ class _DayPickerState extends State<_DayPicker> { } } +class _Day extends StatefulWidget { + const _Day( + this.day, { + super.key, + required this.isDisabled, + required this.isSelectedDay, + required this.isToday, + required this.onChanged, + required this.focusNode, + }); + + final DateTime day; + final bool isDisabled; + final bool isSelectedDay; + final bool isToday; + final ValueChanged onChanged; + final FocusNode? focusNode; + + @override + State<_Day> createState() => _DayState(); +} + +class _DayState extends State<_Day> { + final MaterialStatesController _statesController = MaterialStatesController(); + + @override + Widget build(BuildContext context) { + final DatePickerThemeData defaults = DatePickerTheme.defaults(context); + final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context); + final TextStyle? dayStyle = datePickerTheme.dayStyle ?? defaults.dayStyle; + T? effectiveValue(T? Function(DatePickerThemeData? theme) getProperty) { + return getProperty(datePickerTheme) ?? getProperty(defaults); + } + + T? resolve(MaterialStateProperty? Function(DatePickerThemeData? theme) getProperty, Set states) { + return effectiveValue( + (DatePickerThemeData? theme) { + return getProperty(theme)?.resolve(states); + }, + ); + } + + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final String semanticLabelSuffix = widget.isToday ? ', ${localizations.currentDateLabel}' : ''; + + final Set states = { + if (widget.isDisabled) MaterialState.disabled, + if (widget.isSelectedDay) MaterialState.selected, + }; + + _statesController.value = states; + + final Color? dayForegroundColor = resolve((DatePickerThemeData? theme) => widget.isToday ? theme?.todayForegroundColor : theme?.dayForegroundColor, states); + final Color? dayBackgroundColor = resolve((DatePickerThemeData? theme) => widget.isToday ? theme?.todayBackgroundColor : theme?.dayBackgroundColor, states); + final MaterialStateProperty dayOverlayColor = MaterialStateProperty.resolveWith( + (Set states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), + ); + final BoxDecoration decoration = widget.isToday + ? BoxDecoration( + color: dayBackgroundColor, + border: Border.fromBorderSide( + (datePickerTheme.todayBorder ?? defaults.todayBorder!) + .copyWith(color: dayForegroundColor) + ), + shape: BoxShape.circle, + ) + : BoxDecoration( + color: dayBackgroundColor, + shape: BoxShape.circle, + ); + + Widget dayWidget = Container( + decoration: decoration, + child: Center( + child: Text(localizations.formatDecimal(widget.day.day), style: dayStyle?.apply(color: dayForegroundColor)), + ), + ); + + if (widget.isDisabled) { + dayWidget = ExcludeSemantics( + child: dayWidget, + ); + } else { + dayWidget = InkResponse( + focusNode: widget.focusNode, + onTap: () => widget.onChanged(widget.day), + radius: _dayPickerRowHeight / 2 + 4, + statesController: _statesController, + overlayColor: dayOverlayColor, + child: Semantics( + // We want the day of month to be spoken first irrespective of the + // locale-specific preferences or TextDirection. This is because + // an accessibility user is more likely to be interested in the + // day of month before the rest of the date, as they are looking + // for the day of month. To do that we prepend day of month to the + // formatted full date. + label: '${localizations.formatDecimal(widget.day.day)}, ${localizations.formatFullDate(widget.day)}$semanticLabelSuffix', + // Set button to true to make the date selectable. + button: true, + selected: widget.isSelectedDay, + excludeSemantics: true, + child: dayWidget, + ), + ); + } + + return dayWidget; + } + + @override + void dispose() { + _statesController.dispose(); + super.dispose(); + } +} + class _DayPickerGridDelegate extends SliverGridDelegate { const _DayPickerGridDelegate(); @@ -1085,20 +1143,24 @@ const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate(); class YearPicker extends StatefulWidget { /// Creates a year picker. /// - /// The [firstDate], [lastDate], [selectedDate], and [onChanged] - /// arguments must be non-null. The [lastDate] must be after the [firstDate]. + /// The [lastDate] must be after the [firstDate]. YearPicker({ super.key, DateTime? currentDate, required this.firstDate, required this.lastDate, + @Deprecated( + 'This parameter has no effect and can be removed. Previously it controlled ' + 'the month that was used in "onChanged" when a new year was selected, but ' + 'now that role is filled by "selectedDate" instead. ' + 'This feature was deprecated after v3.13.0-0.3.pre.' + ) DateTime? initialDate, required this.selectedDate, required this.onChanged, this.dragStartBehavior = DragStartBehavior.start, }) : assert(!firstDate.isAfter(lastDate)), - currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()), - initialDate = DateUtils.dateOnly(initialDate ?? selectedDate); + currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()); /// The current date. /// @@ -1111,13 +1173,10 @@ class YearPicker extends StatefulWidget { /// The latest date the user is permitted to pick. final DateTime lastDate; - /// The initial date to center the year display around. - final DateTime initialDate; - /// The currently selected date. /// /// This date is highlighted in the picker. - final DateTime selectedDate; + final DateTime? selectedDate; /// Called when the user picks a year. final ValueChanged onChanged; @@ -1130,7 +1189,8 @@ class YearPicker extends StatefulWidget { } class _YearPickerState extends State { - late ScrollController _scrollController; + ScrollController? _scrollController; + final MaterialStatesController _statesController = MaterialStatesController(); // The approximate number of years necessary to fill the available space. static const int minYears = 18; @@ -1138,14 +1198,21 @@ class _YearPickerState extends State { @override void initState() { super.initState(); - _scrollController = ScrollController(initialScrollOffset: _scrollOffsetForYear(widget.selectedDate)); + _scrollController = ScrollController(initialScrollOffset: _scrollOffsetForYear(widget.selectedDate ?? widget.firstDate)); + } + + @override + void dispose() { + _scrollController?.dispose(); + _statesController.dispose(); + super.dispose(); } @override void didUpdateWidget(YearPicker oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.selectedDate != oldWidget.selectedDate) { - _scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate)); + if (widget.selectedDate != oldWidget.selectedDate && widget.selectedDate != null) { + _scrollController!.jumpTo(_scrollOffsetForYear(widget.selectedDate!)); } } @@ -1176,7 +1243,7 @@ class _YearPickerState extends State { // Backfill the _YearPicker with disabled years if necessary. final int offset = _itemCount < minYears ? (minYears - _itemCount) ~/ 2 : 0; final int year = widget.firstDate.year + index - offset; - final bool isSelected = year == widget.selectedDate.year; + final bool isSelected = year == widget.selectedDate?.year; final bool isCurrentYear = year == widget.currentDate.year; final bool isDisabled = year < widget.firstDate.year || year > widget.lastDate.year; const double decorationHeight = 36.0; @@ -1191,7 +1258,7 @@ class _YearPickerState extends State { final Color? background = resolve((DatePickerThemeData? theme) => isCurrentYear ? theme?.todayBackgroundColor : theme?.yearBackgroundColor, states); final MaterialStateProperty overlayColor = MaterialStateProperty.resolveWith((Set states) => - effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), + effectiveValue((DatePickerThemeData? theme) => theme?.yearOverlayColor?.resolve(states)), ); BoxBorder? border; @@ -1228,10 +1295,21 @@ class _YearPickerState extends State { child: yearItem, ); } else { + DateTime date = DateTime(year, widget.selectedDate?.month ?? DateTime.january); + if (date.isBefore(DateTime(widget.firstDate.year, widget.firstDate.month))) { + // Ignore firstDate.day because we're just working in years and months here. + assert(date.year == widget.firstDate.year); + date = DateTime(year, widget.firstDate.month); + } else if (date.isAfter(widget.lastDate)) { + // No need to ignore the day here because it can only be bigger than what we care about. + assert(date.year == widget.lastDate.year); + date = DateTime(year, widget.lastDate.month); + } + _statesController.value = states; yearItem = InkWell( key: ValueKey(year), - onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month)), - statesController: MaterialStatesController(states), + onTap: () => widget.onChanged(date), + statesController: _statesController, overlayColor: overlayColor, child: yearItem, ); diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index a39ff240b08d0..dd9ced05fa960 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -58,8 +58,7 @@ import 'theme.dart'; class Card extends StatelessWidget { /// Creates a Material Design card. /// - /// The [elevation] must be null or non-negative. The [borderOnForeground] - /// must not be null. + /// The [elevation] must be null or non-negative. const Card({ super.key, this.color, @@ -78,6 +77,12 @@ class Card extends StatelessWidget { /// /// Defines the card's [Material.color]. /// + /// In Material 3, [surfaceTintColor] is drawn on top of this color + /// when the card is elevated. This might make the appearance of + /// the card slightly different than in Material 2. To disable this + /// feature, set [surfaceTintColor] to [Colors.transparent]. + /// See [Material.surfaceTintColor] for more details. + /// /// If this property is null then the ambient [CardTheme.color] is used. If that is null, /// and [ThemeData.useMaterial3] is true, then [ColorScheme.surface] of /// [ThemeData.colorScheme] is used. Otherwise, [ThemeData.cardColor] is used. diff --git a/packages/flutter/lib/src/material/card_theme.dart b/packages/flutter/lib/src/material/card_theme.dart index 25855ddbafc64..23391dbe77047 100644 --- a/packages/flutter/lib/src/material/card_theme.dart +++ b/packages/flutter/lib/src/material/card_theme.dart @@ -110,8 +110,6 @@ class CardTheme with Diagnosticable { /// Linearly interpolate between two Card themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static CardTheme lerp(CardTheme? a, CardTheme? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index bfead13fe1d83..c6bd10b8a15d1 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -44,6 +44,12 @@ enum _CheckboxType { material, adaptive } /// ** See code in examples/api/lib/material/checkbox/checkbox.0.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows what the checkbox error state looks like. +/// +/// ** See code in examples/api/lib/material/checkbox/checkbox.1.dart ** +/// {@end-tool} +/// /// See also: /// /// * [CheckboxListTile], which combines this widget with a [ListTile] so that @@ -68,8 +74,6 @@ class Checkbox extends StatefulWidget { /// can only be null if [tristate] is true. /// * [onChanged], which is called when the value of the checkbox should /// change. It can be set to null to disable the checkbox. - /// - /// The values of [tristate] and [autofocus] must not be null. const Checkbox({ super.key, required this.value, @@ -385,7 +389,7 @@ class Checkbox extends StatefulWidget { /// this is true. This is only used when [ThemeData.useMaterial3] is set to true. /// {@endtemplate} /// - /// Must not be null. Defaults to false. + /// Defaults to false. final bool isError; /// {@template flutter.material.checkbox.semanticLabel} diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index 87d91827bd354..76dc4265e4c3b 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -159,8 +159,6 @@ class CheckboxListTile extends StatelessWidget { /// can only be null if [tristate] is true. /// * [onChanged], which is called when the value of the checkbox should /// change. It can be set to null to disable the checkbox. - /// - /// The value of [tristate] must not be null. const CheckboxListTile({ super.key, required this.value, diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index eb457147e068d..757c64269e489 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -76,8 +76,9 @@ abstract interface class ChipAttributes { /// The style to be applied to the chip's label. /// - /// The default label style is [TextTheme.bodyLarge] from the overall - /// theme's [ThemeData.textTheme]. + /// If this is null and [ThemeData.useMaterial3] is true, then + /// [TextTheme.labelLarge] is used. Otherwise, [TextTheme.bodyLarge] + /// is used. // /// This only has an effect on widgets that respect the [DefaultTextStyle], /// such as [Text]. @@ -95,12 +96,17 @@ abstract interface class ChipAttributes { /// The color and weight of the chip's outline. /// /// Defaults to the border side in the ambient [ChipThemeData]. If the theme - /// border side resolves to null, the default is the border side of [shape]. + /// border side resolves to null and [ThemeData.useMaterial3] is true, then + /// [BorderSide] with a [ColorScheme.outline] color is used when the chip is + /// enabled, and [BorderSide] with a [ColorScheme.onSurface] color with an + /// opacity of 0.12 is used when the chip is disabled. Otherwise, it defaults + /// to null. /// /// This value is combined with [shape] to create a shape decorated with an - /// outline. If it is a [MaterialStateBorderSide], - /// [MaterialStateProperty.resolve] is used for the following - /// [MaterialState]s: + /// outline. To omit the outline entirely, pass [BorderSide.none] to [side]. + /// + /// If it is a [MaterialStateBorderSide], [MaterialStateProperty.resolve] is + /// used for the following [MaterialState]s: /// /// * [MaterialState.disabled]. /// * [MaterialState.selected]. @@ -112,12 +118,15 @@ abstract interface class ChipAttributes { /// The [OutlinedBorder] to draw around the chip. /// /// Defaults to the shape in the ambient [ChipThemeData]. If the theme - /// shape resolves to null, the default is [StadiumBorder]. + /// shape resolves to null and [ThemeData.useMaterial3] is true, then + /// [RoundedRectangleBorder] with a circular border radius of 8.0 is used. + /// Otherwise, [StadiumBorder] is used. /// /// This shape is combined with [side] to create a shape decorated with an - /// outline. If it is a [MaterialStateOutlinedBorder], - /// [MaterialStateProperty.resolve] is used for the following - /// [MaterialState]s: + /// outline. To omit the outline entirely, pass [BorderSide.none] to [side]. + /// + /// If it is a [MaterialStateOutlinedBorder], [MaterialStateProperty.resolve] + /// is used for the following [MaterialState]s: /// /// * [MaterialState.disabled]. /// * [MaterialState.selected]. @@ -128,7 +137,7 @@ abstract interface class ChipAttributes { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. Clip get clipBehavior; /// {@macro flutter.widgets.Focus.focusNode} @@ -139,6 +148,8 @@ abstract interface class ChipAttributes { /// The color that fills the chip, in all [MaterialState]s. /// + /// Defaults to null. + /// /// Resolves in the following states: /// * [MaterialState.selected]. /// * [MaterialState.disabled]. @@ -151,7 +162,9 @@ abstract interface class ChipAttributes { /// The padding between the contents of the chip and the outside [shape]. /// - /// Defaults to 4 logical pixels on all sides. + /// If this is null and [ThemeData.useMaterial3] is true, then + /// a padding of 8.0 logical pixels on all sides is used. Otherwise, + /// it defaults to a padding of 4.0 logical pixels on all sides. EdgeInsetsGeometry? get padding; /// Defines how compact the chip's layout will be. @@ -190,18 +203,25 @@ abstract interface class ChipAttributes { /// Color of the chip's shadow when the elevation is greater than 0. /// - /// The default is null. + /// If this is null and [ThemeData.useMaterial3] is true, then + /// [Colors.transparent] color is used. Otherwise, it defaults to null. Color? get shadowColor; /// Color of the chip's surface tint overlay when its elevation is /// greater than 0. /// - /// The default is null. + /// If this is null and [ThemeData.useMaterial3] is true, then + /// [ColorScheme.surfaceTint] color is used. Otherwise, it defaults + /// to null. Color? get surfaceTintColor; /// Theme used for all icons in the chip. /// - /// The default is null. + /// If this is null and [ThemeData.useMaterial3] is true, then [IconThemeData] + /// with a [ColorScheme.primary] color and a size of 18.0 is used when + /// the chip is enabled, and [IconThemeData] with a [ColorScheme.onSurface] + /// color and a size of 18.0 is used when the chip is disabled. Otherwise, + /// it defaults to null. IconThemeData? get iconTheme; } @@ -259,16 +279,6 @@ abstract interface class DeletableChipAttributes { /// If null, the default [MaterialLocalizations.deleteButtonTooltip] will be /// used. String? get deleteButtonTooltipMessage; - - /// Whether to use a tooltip on the chip's delete button showing the - /// [deleteButtonTooltipMessage]. - /// - /// Defaults to true. - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - bool get useDeleteButtonTooltip; } /// An interface for Material Design chips that can have check marks. @@ -323,7 +333,7 @@ abstract interface class SelectableChipAttributes { /// If [onSelected] is not null, this value will be used to determine if the /// select check mark will be shown or not. /// - /// Must not be null. Defaults to false. + /// Defaults to false. bool get selected; /// Called when the chip should change between selected and de-selected @@ -437,7 +447,7 @@ abstract interface class DisabledChipAttributes { /// For classes which don't have this as a constructor argument, [isEnabled] /// returns true if their user action callback is set. /// - /// Defaults to true. Cannot be null. + /// Defaults to true. bool get isEnabled; /// The color used for the chip's background to indicate that it is not @@ -551,7 +561,6 @@ abstract interface class TappableChipAttributes { class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttributes { /// Creates a Material Design chip. /// - /// The [label], [autofocus], and [clipBehavior] arguments must not be null. /// The [elevation] must be null or non-negative. const Chip({ super.key, @@ -577,11 +586,6 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.shadowColor, this.surfaceTintColor, this.iconTheme, - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - this.useDeleteButtonTooltip = true, }) : assert(elevation == null || elevation >= 0.0); @override @@ -628,12 +632,6 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri final Color? surfaceTintColor; @override final IconThemeData? iconTheme; - @override - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - final bool useDeleteButtonTooltip; @override Widget build(BuildContext context) { @@ -646,7 +644,6 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri deleteIcon: deleteIcon, onDeleted: onDeleted, deleteIconColor: deleteIconColor, - useDeleteButtonTooltip: useDeleteButtonTooltip, deleteButtonTooltipMessage: deleteButtonTooltipMessage, tapEnabled: false, side: side, @@ -709,10 +706,8 @@ class RawChip extends StatefulWidget /// The [onPressed] and [onSelected] callbacks must not both be specified at /// the same time. /// - /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior] - /// arguments must not be null. The [pressElevation] and [elevation] must be - /// null or non-negative. Typically, [pressElevation] is greater than - /// [elevation]. + /// The [pressElevation] and [elevation] must be null or non-negative. + /// Typically, [pressElevation] is greater than [elevation]. const RawChip({ super.key, this.defaultProperties, @@ -748,14 +743,9 @@ class RawChip extends StatefulWidget this.surfaceTintColor, this.iconTheme, this.selectedShadowColor, - this.showCheckmark = true, + this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - this.useDeleteButtonTooltip = true, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), deleteIcon = deleteIcon ?? _kDefaultDeleteIcon; @@ -835,12 +825,6 @@ class RawChip extends StatefulWidget final Color? checkmarkColor; @override final ShapeBorder avatarBorder; - @override - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - final bool useDeleteButtonTooltip; /// If set, this indicates that the chip should be disabled if all of the /// tap callbacks ([onSelected], [onPressed]) are null. @@ -992,13 +976,21 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid OutlinedBorder _getShape(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) { final BorderSide? resolvedSide = MaterialStateProperty.resolveAs(widget.side, materialStates) - ?? MaterialStateProperty.resolveAs(chipTheme.side, materialStates) - ?? MaterialStateProperty.resolveAs(chipDefaults.side, materialStates); + ?? MaterialStateProperty.resolveAs(chipTheme.side, materialStates); final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs(widget.shape, materialStates) ?? MaterialStateProperty.resolveAs(chipTheme.shape, materialStates) ?? MaterialStateProperty.resolveAs(chipDefaults.shape, materialStates) + // TODO(tahatesser): Remove this fallback when Material 2 is deprecated. ?? const StadiumBorder(); - return resolvedShape.copyWith(side: resolvedSide); + // If the side is provided, shape uses the provided side. + if (resolvedSide != null) { + return resolvedShape.copyWith(side: resolvedSide); + } + // If the side is not provided and the shape's side is not [BorderSide.none], + // then the shape's side is used. Otherwise, the default side is used. + return resolvedShape.side != BorderSide.none + ? resolvedShape + : resolvedShape.copyWith(side: chipDefaults.side); } Color? resolveColor({ @@ -1131,9 +1123,8 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid container: true, button: true, child: _wrapWithTooltip( - tooltip: widget.useDeleteButtonTooltip - ? widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip - : null, + tooltip: widget.deleteButtonTooltipMessage + ?? MaterialLocalizations.of(context).deleteButtonTooltip, enabled: widget.onDeleted != null, child: InkWell( // Radius should be slightly less than the full size of the chip. @@ -1169,7 +1160,7 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid final EdgeInsetsGeometry defaultLabelPadding = EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; final ThemeData theme = Theme.of(context); @@ -1763,6 +1754,7 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip } final bool hitIsOnDeleteIcon = deleteIcon != null && _hitIsOnDeleteIcon( padding: theme.padding, + labelPadding: theme.labelPadding, tapPosition: position, chipSize: size, deleteButtonSize: deleteIcon!.size, @@ -2042,6 +2034,8 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip } } + final LayerHandle _avatarOpacityLayerHandler = LayerHandle(); + void _paintAvatar(PaintingContext context, Offset offset) { void paintWithOverlay(PaintingContext context, Offset offset) { context.paintChild(avatar!, _boxParentData(avatar!).offset + offset); @@ -2049,13 +2043,15 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip } if (!theme.showAvatar && avatarDrawerAnimation.isDismissed) { + _avatarOpacityLayerHandler.layer = null; return; } final Color disabledColor = _disabledColor; final int disabledColorAlpha = disabledColor.alpha; if (needsCompositing) { - context.pushLayer(OpacityLayer(alpha: disabledColorAlpha), paintWithOverlay, offset); + _avatarOpacityLayerHandler.layer = context.pushOpacity(offset, disabledColorAlpha, paintWithOverlay, oldLayer: _avatarOpacityLayerHandler.layer); } else { + _avatarOpacityLayerHandler.layer = null; if (disabledColorAlpha != 0xff) { context.canvas.saveLayer( _boxRect(avatar).shift(offset).inflate(20.0), @@ -2069,21 +2065,26 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip } } + final LayerHandle _childOpacityLayerHandler = LayerHandle(); + void _paintChild(PaintingContext context, Offset offset, RenderBox? child, bool? isEnabled) { if (child == null) { + _childOpacityLayerHandler.layer = null; return; } final int disabledColorAlpha = _disabledColor.alpha; if (!enableAnimation.isCompleted) { if (needsCompositing) { - context.pushLayer( - OpacityLayer(alpha: disabledColorAlpha), + _childOpacityLayerHandler.layer = context.pushOpacity( + offset, + disabledColorAlpha, (PaintingContext context, Offset offset) { context.paintChild(child, _boxParentData(child).offset + offset); }, - offset, + oldLayer: _childOpacityLayerHandler.layer, ); } else { + _childOpacityLayerHandler.layer = null; final Rect childRect = _boxRect(child).shift(offset); context.canvas.saveLayer(childRect.inflate(20.0), Paint()..color = _disabledColor); context.paintChild(child, _boxParentData(child).offset + offset); @@ -2094,6 +2095,13 @@ class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_Chip } } + @override + void dispose() { + _childOpacityLayerHandler.layer = null; + _avatarOpacityLayerHandler.layer = null; + super.dispose(); + } + @override void paint(PaintingContext context, Offset offset) { _paintAvatar(context, offset); @@ -2186,6 +2194,7 @@ class _UnconstrainedInkSplashFactory extends InteractiveInkFeatureFactory { bool _hitIsOnDeleteIcon({ required EdgeInsetsGeometry padding, + required EdgeInsetsGeometry labelPadding, required Offset tapPosition, required Size chipSize, required Size deleteButtonSize, @@ -2197,10 +2206,10 @@ bool _hitIsOnDeleteIcon({ final Size deflatedSize = resolvedPadding.deflateSize(chipSize); final Offset adjustedPosition = tapPosition - Offset(resolvedPadding.left, resolvedPadding.top); // The delete button hit area should be at least the width of the delete - // button, but, if there's room, up to 24 pixels from the center of the delete - // icon (corresponding to part of a 48x48 square that Material would prefer - // for touch targets), but no more than approximately half of the overall size - // of the chip when the chip is small. + // button and right label padding, but, if there's room, up to 24 pixels + // from the center of the delete icon (corresponding to part of a 48x48 square + // that Material would prefer for touch targets), but no more than approximately + // half of the overall size of the chip when the chip is small. // // This isn't affected by materialTapTargetSize because it only applies to the // width of the tappable region within the chip, not outside of the chip, @@ -2211,7 +2220,7 @@ bool _hitIsOnDeleteIcon({ // chip will still hit the chip, not the delete button. final double accessibleDeleteButtonWidth = math.min( deflatedSize.width * 0.499, - math.max(deleteButtonSize.width, 24.0 + deleteButtonSize.width / 2.0), + math.min(labelPadding.resolve(textDirection).right + deleteButtonSize.width, 24.0 + deleteButtonSize.width / 2.0), ); switch (textDirection) { case TextDirection.ltr: @@ -2283,7 +2292,7 @@ class _ChipDefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } diff --git a/packages/flutter/lib/src/material/chip_theme.dart b/packages/flutter/lib/src/material/chip_theme.dart index 4c9a052d9e039..44aa0b0016bb9 100644 --- a/packages/flutter/lib/src/material/chip_theme.dart +++ b/packages/flutter/lib/src/material/chip_theme.dart @@ -42,8 +42,6 @@ import 'theme.dart'; /// application. class ChipTheme extends InheritedTheme { /// Applies the given theme [data] to [child]. - /// - /// The [data] and [child] arguments must not be null. const ChipTheme({ super.key, required this.data, @@ -490,8 +488,6 @@ class ChipThemeData with Diagnosticable { /// Linearly interpolate between two chip themes. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static ChipThemeData? lerp(ChipThemeData? a, ChipThemeData? b, double t) { if (identical(a, b)) { diff --git a/packages/flutter/lib/src/material/choice_chip.dart b/packages/flutter/lib/src/material/choice_chip.dart index 7b05db36229a9..873d554199b90 100644 --- a/packages/flutter/lib/src/material/choice_chip.dart +++ b/packages/flutter/lib/src/material/choice_chip.dart @@ -22,8 +22,7 @@ enum _ChipVariant { flat, elevated } /// [ChoiceChip]s represent a single choice from a set. Choice chips contain /// related descriptive text or categories. /// -/// Requires one of its ancestors to be a [Material] widget. The [selected] and -/// [label] arguments must not be null. +/// Requires one of its ancestors to be a [Material] widget. /// /// {@tool dartpad} /// This example shows how to create [ChoiceChip]s with [onSelected]. When the @@ -338,7 +337,7 @@ class _ChoiceChipDefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } diff --git a/packages/flutter/lib/src/material/circle_avatar.dart b/packages/flutter/lib/src/material/circle_avatar.dart index 6d537775ab3cc..3f7c9584ff0cb 100644 --- a/packages/flutter/lib/src/material/circle_avatar.dart +++ b/packages/flutter/lib/src/material/circle_avatar.dart @@ -252,10 +252,9 @@ class CircleAvatar extends StatelessWidget { child: child == null ? null : Center( - child: MediaQuery( - // Need to ignore the ambient textScaleFactor here so that the - // text doesn't escape the avatar when the textScaleFactor is large. - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + // Need to disable text scaling here so that the text doesn't + // escape the avatar when the textScaleFactor is large. + child: MediaQuery.withNoTextScaling( child: IconTheme( data: theme.iconTheme.copyWith(color: textStyle.color), child: DefaultTextStyle( diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index 4696cce72d6a5..ce5fb3256bbee 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -4,8 +4,6 @@ import 'package:flutter/painting.dart'; -import 'colors.dart'; - /// The minimum dimension of any interactive region according to Material /// guidelines. /// @@ -51,11 +49,13 @@ const EdgeInsets kTabLabelPadding = EdgeInsets.symmetric(horizontal: 16.0); const EdgeInsets kMaterialListPadding = EdgeInsets.symmetric(vertical: 8.0); /// The default color for [ThemeData.iconTheme] when [ThemeData.brightness] is -/// [Brightness.light]. This color is used in [IconButton] to detect whether +/// [Brightness.dark]. This color is used in [IconButton] to detect whether /// [IconTheme.of(context).color] is the same as the default color of [ThemeData.iconTheme]. -const Color kDefaultIconLightColor = Colors.white; +// ignore: prefer_const_constructors +final Color kDefaultIconLightColor = Color(0xFFFFFFFF); /// The default color for [ThemeData.iconTheme] when [ThemeData.brightness] is -/// [Brightness.dark]. This color is used in [IconButton] to detect whether +/// [Brightness.light]. This color is used in [IconButton] to detect whether /// [IconTheme.of(context).color] is the same as the default color of [ThemeData.iconTheme]. -const Color kDefaultIconDarkColor = Colors.black87; +// ignore: prefer_const_constructors +final Color kDefaultIconDarkColor = Color(0xDD000000); diff --git a/packages/flutter/lib/src/material/curves.dart b/packages/flutter/lib/src/material/curves.dart index eb5762178f10d..c267e3771ee42 100644 --- a/packages/flutter/lib/src/material/curves.dart +++ b/packages/flutter/lib/src/material/curves.dart @@ -6,6 +6,8 @@ import 'package:flutter/animation.dart'; // The easing curves of the Material Library +// TODO(guidezpl): deprecate the three curves below once customers (packages/plugins) are migrated + /// The standard easing curve in the Material specification. /// /// Elements that begin and end at rest use standard easing. diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index b1c01ce207994..e1add455280dc 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -36,8 +36,6 @@ typedef DataColumnSortCallback = void Function(int columnIndex, bool ascending); @immutable class DataColumn { /// Creates the configuration for a column of a [DataTable]. - /// - /// The [label] argument must not be null. const DataColumn({ required this.label, this.tooltip, @@ -112,8 +110,6 @@ class DataColumn { @immutable class DataRow { /// Creates the configuration for a row of a [DataTable]. - /// - /// The [cells] argument must not be null. const DataRow({ this.key, this.selected = false, @@ -126,8 +122,6 @@ class DataRow { /// Creates the configuration for a row of a [DataTable], deriving /// the key from a row index. - /// - /// The [cells] argument must not be null. DataRow.byIndex({ int? index, this.selected = false, @@ -248,8 +242,7 @@ class DataCell { /// Creates an object to hold the data for a cell in a [DataTable]. /// /// The first argument is the widget to show for the cell, typically - /// a [Text] or [DropdownButton] widget; this becomes the [child] - /// property and must not be null. + /// a [Text] or [DropdownButton] widget. /// /// If the cell has no data, then a [Text] widget with placeholder /// text should be provided instead, and then the [placeholder] @@ -357,6 +350,13 @@ class DataCell { /// [PaginatedDataTable] which automatically splits the data into /// multiple pages. /// +/// ## Performance considerations when wrapping [DataTable] with [SingleChildScrollView] +/// +/// Wrapping a [DataTable] with [SingleChildScrollView] is expensive as [SingleChildScrollView] +/// mounts and paints the entire [DataTable] even when only some rows are visible. If scrolling in +/// one direction is necessary, then consider using a [CustomScrollView], otherwise use [PaginatedDataTable] +/// to split the data into smaller pages. +/// /// {@tool dartpad} /// This sample shows how to display a [DataTable] with three columns: name, age, and /// role. The columns are defined by three [DataColumn] objects. The table @@ -395,7 +395,7 @@ class DataTable extends StatelessWidget { /// The [columns] argument must be a list of as many [DataColumn] /// objects as the table is to have columns, ignoring the leading /// checkbox column if any. The [columns] argument must have a - /// length greater than zero and must not be null. + /// length greater than zero. /// /// The [rows] argument must be a list of as many [DataRow] objects /// as the table is to have rows, ignoring the leading heading row @@ -444,7 +444,7 @@ class DataTable extends StatelessWidget { this.clipBehavior = Clip.none, }) : assert(columns.isNotEmpty), assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), - assert(!rows.any((DataRow row) => row.cells.length != columns.length)), + assert(!rows.any((DataRow row) => row.cells.length != columns.length), 'All rows must have the same number of cells as there are header cells (${columns.length})'), assert(dividerThickness == null || dividerThickness >= 0), assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight), assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), @@ -467,6 +467,8 @@ class DataTable extends StatelessWidget { /// /// When this is null, it implies that the table's sort order does /// not correspond to any of the columns. + /// + /// The direction of the sort is specified using [sortAscending]. final int? sortColumnIndex; /// Whether the column mentioned in [sortColumnIndex], if any, is sorted @@ -479,6 +481,8 @@ class DataTable extends StatelessWidget { /// If false, the order is descending (meaning the rows with the /// smallest values for the current sort column are last in the /// table). + /// + /// Ascending order is represented by an upwards-facing arrow. final bool sortAscending; /// Invoked when the user selects or unselects every row, using the @@ -665,7 +669,7 @@ class DataTable extends StatelessWidget { /// The data to show in each row (excluding the row that contains /// the column headings). /// - /// Must be non-null, but may be empty. + /// The list may be empty. final List rows; /// {@template flutter.material.dataTable.dividerThickness} @@ -701,7 +705,7 @@ class DataTable extends StatelessWidget { /// /// This can be used to clip the content within the border of the [DataTable]. /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; // Set by the constructor to the index of the only Column that is @@ -853,7 +857,7 @@ class DataTable extends StatelessWidget { height: effectiveHeadingRowHeight, alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart, child: AnimatedDefaultTextStyle( - style: effectiveHeadingTextStyle, + style: DefaultTextStyle.of(context).style.merge(effectiveHeadingTextStyle), softWrap: false, duration: _sortArrowAnimationDuration, child: label, @@ -922,9 +926,9 @@ class DataTable extends StatelessWidget { constraints: BoxConstraints(minHeight: effectiveDataRowMinHeight, maxHeight: effectiveDataRowMaxHeight), alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart, child: DefaultTextStyle( - style: effectiveDataTextStyle.copyWith( - color: placeholder ? effectiveDataTextStyle.color!.withOpacity(0.6) : null, - ), + style: DefaultTextStyle.of(context).style + .merge(effectiveDataTextStyle) + .copyWith(color: placeholder ? effectiveDataTextStyle.color!.withOpacity(0.6) : null), child: DropdownButtonHideUnderline(child: label), ), ); diff --git a/packages/flutter/lib/src/material/data_table_source.dart b/packages/flutter/lib/src/material/data_table_source.dart index dd7e8b9febc05..7b51441f96f27 100644 --- a/packages/flutter/lib/src/material/data_table_source.dart +++ b/packages/flutter/lib/src/material/data_table_source.dart @@ -18,21 +18,32 @@ import 'data_table.dart'; /// /// DataTableSource objects are expected to be long-lived, not recreated with /// each build. +/// +/// If a [DataTableSource] is used with a [PaginatedDataTable] that supports +/// sortable columns (see [DataColumn.onSort] and +/// [PaginatedDataTable.sortColumnIndex]), the rows reported by the data source +/// must be reported in the sorted order. abstract class DataTableSource extends ChangeNotifier { /// Called to obtain the data about a particular row. /// + /// Rows should be keyed so that state can be maintained when the data source + /// is sorted (e.g. in response to [DataColumn.onSort]). Keys should be + /// consistent for a given [DataRow] regardless of the sort order (i.e. the + /// key represents the data's identity, not the row position). + /// /// The [DataRow.byIndex] constructor provides a convenient way to construct - /// [DataRow] objects for this callback's purposes without having to worry about - /// independently keying each row. + /// [DataRow] objects for this method's purposes without having to worry about + /// independently keying each row. The index passed to that constructor is the + /// index of the underlying data, which is different than the `index` + /// parameter for [getRow], which represents the _sorted_ position. /// /// If the given index does not correspond to a row, or if no data is yet /// available for a row, then return null. The row will be left blank and a /// loading indicator will be displayed over the table. Once data is available /// or once it is firmly established that the row index in question is beyond - /// the end of the table, call [notifyListeners]. + /// the end of the table, call [notifyListeners]. (See [rowCount].) /// - /// Data returned from this method must be consistent for the lifetime of the - /// object. If the row count changes, then a new delegate must be provided. + /// If the underlying data changes, call [notifyListeners]. DataRow? getRow(int index); /// Called to obtain the number of rows to tell the user are available. @@ -58,5 +69,7 @@ abstract class DataTableSource extends ChangeNotifier { /// Called to obtain the number of rows that are currently selected. /// /// If the selected row count changes, call [notifyListeners]. + /// + /// Selected rows are those whose [DataRow.selected] property is set to true. int get selectedRowCount; } diff --git a/packages/flutter/lib/src/material/data_table_theme.dart b/packages/flutter/lib/src/material/data_table_theme.dart index 79a534e69a92c..fb2bb96b00565 100644 --- a/packages/flutter/lib/src/material/data_table_theme.dart +++ b/packages/flutter/lib/src/material/data_table_theme.dart @@ -162,8 +162,6 @@ class DataTableThemeData with Diagnosticable { /// Linearly interpolate between two [DataTableThemeData]s. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static DataTableThemeData lerp(DataTableThemeData a, DataTableThemeData b, double t) { if (identical(a, b)) { @@ -266,8 +264,6 @@ class DataTableThemeData with Diagnosticable { class DataTableTheme extends InheritedWidget { /// Constructs a data table theme that configures all descendant /// [DataTable] widgets. - /// - /// The [data] must not be null. const DataTableTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/date.dart b/packages/flutter/lib/src/material/date.dart index 8d59527e48daa..b6e00827b7272 100644 --- a/packages/flutter/lib/src/material/date.dart +++ b/packages/flutter/lib/src/material/date.dart @@ -67,7 +67,7 @@ abstract final class DateUtils { /// /// `date` would be January 15, 2019. /// `futureDate` would be April 1, 2019 since it adds 3 months. - static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) { + static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) { return DateTime(monthDate.year, monthDate.month + monthsToAdd); } diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index afb72d4b84ba7..154529a84e28c 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -46,29 +46,31 @@ const Size _inputRangeLandscapeDialogSize = Size(496, 164.0); const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200); const double _inputFormPortraitHeight = 98.0; const double _inputFormLandscapeHeight = 108.0; +const double _kMaxTextScaleFactor = 1.3; /// Shows a dialog containing a Material Design date picker. /// /// The returned [Future] resolves to the date selected by the user when the /// user confirms the dialog. If the user cancels the dialog, null is returned. /// -/// When the date picker is first displayed, it will show the month of -/// [initialDate], with [initialDate] selected. +/// When the date picker is first displayed, if [initialDate] is not null, it +/// will show the month of [initialDate], with [initialDate] selected. Otherwise +/// it will show the [currentDate]'s month. /// /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest -/// allowable date. [initialDate] must either fall between these dates, -/// or be equal to one of them. For each of these [DateTime] parameters, only -/// their dates are considered. Their time fields are ignored. They must all -/// be non-null. +/// allowable date. If [initialDate] is not null, it must either fall between +/// these dates, or be equal to one of them. For each of these [DateTime] +/// parameters, only their dates are considered. Their time fields are ignored. +/// They must all be non-null. /// /// The [currentDate] represents the current day (i.e. today). This /// date will be highlighted in the day grid. If null, the date of -/// `DateTime.now()` will be used. +/// [DateTime.now] will be used. /// /// An optional [initialEntryMode] argument can be used to display the date /// picker in the [DatePickerEntryMode.calendar] (a calendar month grid) /// or [DatePickerEntryMode.input] (a text input field) mode. -/// It defaults to [DatePickerEntryMode.calendar] and must be non-null. +/// It defaults to [DatePickerEntryMode.calendar]. /// /// {@template flutter.material.date_picker.switchToInputEntryModeIcon} /// An optional [switchToInputEntryModeIcon] argument can be used to @@ -112,17 +114,16 @@ const double _inputFormLandscapeHeight = 108.0; /// [locale] and [textDirection] are non-null, [textDirection] overrides the /// direction chosen for the [locale]. /// -/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to -/// [showDialog], the documentation for which discusses how it is used. [context] -/// and [useRootNavigator] must be non-null. +/// The [context], [barrierDismissible], [barrierColor], [barrierLabel], +/// [useRootNavigator] and [routeSettings] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Theme]. /// /// An optional [initialDatePickerMode] argument can be used to have the /// calendar date picker initially appear in the [DatePickerMode.year] or -/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and -/// must be non-null. +/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day]. /// /// {@macro flutter.widgets.RawDialogRoute} /// @@ -155,10 +156,9 @@ const double _inputFormLandscapeHeight = 108.0; /// * [DisplayFeatureSubScreen], which documents the specifics of how /// [DisplayFeature]s can split the screen into sub-screens. /// * [showTimePicker], which shows a dialog that contains a Material Design time picker. -/// Future showDatePicker({ required BuildContext context, - required DateTime initialDate, + DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -168,6 +168,9 @@ Future showDatePicker({ String? cancelText, String? confirmText, Locale? locale, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, bool useRootNavigator = true, RouteSettings? routeSettings, TextDirection? textDirection, @@ -183,7 +186,7 @@ Future showDatePicker({ final Icon? switchToInputEntryModeIcon, final Icon? switchToCalendarEntryModeIcon, }) async { - initialDate = DateUtils.dateOnly(initialDate); + initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate); firstDate = DateUtils.dateOnly(firstDate); lastDate = DateUtils.dateOnly(lastDate); assert( @@ -191,15 +194,15 @@ Future showDatePicker({ 'lastDate $lastDate must be on or after firstDate $firstDate.', ); assert( - !initialDate.isBefore(firstDate), + initialDate == null || !initialDate.isBefore(firstDate), 'initialDate $initialDate must be on or after firstDate $firstDate.', ); assert( - !initialDate.isAfter(lastDate), + initialDate == null || !initialDate.isAfter(lastDate), 'initialDate $initialDate must be on or before lastDate $lastDate.', ); assert( - selectableDayPredicate == null || selectableDayPredicate(initialDate), + selectableDayPredicate == null || initialDate == null || selectableDayPredicate(initialDate), 'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.', ); assert(debugCheckHasMaterialLocalizations(context)); @@ -242,6 +245,9 @@ Future showDatePicker({ return showDialog( context: context, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, useRootNavigator: useRootNavigator, routeSettings: routeSettings, builder: (BuildContext context) { @@ -264,7 +270,7 @@ class DatePickerDialog extends StatefulWidget { /// A Material-style date picker dialog. DatePickerDialog({ super.key, - required DateTime initialDate, + DateTime? initialDate, required DateTime firstDate, required DateTime lastDate, DateTime? currentDate, @@ -283,7 +289,7 @@ class DatePickerDialog extends StatefulWidget { this.onDatePickerModeChange, this.switchToInputEntryModeIcon, this.switchToCalendarEntryModeIcon, - }) : initialDate = DateUtils.dateOnly(initialDate), + }) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate), firstDate = DateUtils.dateOnly(firstDate), lastDate = DateUtils.dateOnly(lastDate), currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { @@ -292,21 +298,24 @@ class DatePickerDialog extends StatefulWidget { 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isBefore(this.firstDate), + initialDate == null || !this.initialDate!.isBefore(this.firstDate), 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.', ); assert( - !this.initialDate.isAfter(this.lastDate), + initialDate == null || !this.initialDate!.isAfter(this.lastDate), 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.', ); assert( - selectableDayPredicate == null || selectableDayPredicate!(this.initialDate), + selectableDayPredicate == null || initialDate == null || selectableDayPredicate!(this.initialDate!), 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate', ); } /// The initially selected [DateTime] that the picker should display. - final DateTime initialDate; + /// + /// If this is null, there is no selected date. A date must be selected to + /// submit the dialog. + final DateTime? initialDate; /// The earliest allowable [DateTime] that the user can select. final DateTime firstDate; @@ -402,10 +411,18 @@ class DatePickerDialog extends StatefulWidget { } class _DatePickerDialogState extends State with RestorationMixin { - late final RestorableDateTime _selectedDate = RestorableDateTime(widget.initialDate); + late final RestorableDateTimeN _selectedDate = RestorableDateTimeN(widget.initialDate); late final _RestorableDatePickerEntryMode _entryMode = _RestorableDatePickerEntryMode(widget.initialEntryMode); final _RestorableAutovalidateMode _autovalidateMode = _RestorableAutovalidateMode(AutovalidateMode.disabled); + @override + void dispose() { + _selectedDate.dispose(); + _entryMode.dispose(); + _autovalidateMode.dispose(); + super.dispose(); + } + @override String? get restorationId => widget.restorationId; @@ -436,9 +453,7 @@ class _DatePickerDialogState extends State with RestorationMix } void _handleOnDatePickerModeChange() { - if (widget.onDatePickerModeChange != null) { - widget.onDatePickerModeChange!(_entryMode.value); - } + widget.onDatePickerModeChange?.call(_entryMode.value); } void _handleEntryModeToggle() { @@ -454,7 +469,7 @@ class _DatePickerDialogState extends State with RestorationMix _handleOnDatePickerModeChange(); case DatePickerEntryMode.calendarOnly: case DatePickerEntryMode.inputOnly: - assert(false, 'Can not change entry mode from _entryMode'); + assert(false, 'Can not change entry mode from ${_entryMode.value}'); } }); } @@ -535,6 +550,7 @@ class _DatePickerDialogState extends State with RestorationMix spacing: 8, children: [ TextButton( + style: datePickerTheme.cancelButtonStyle ?? defaults.cancelButtonStyle, onPressed: _handleCancel, child: Text(widget.cancelText ?? ( useMaterial3 @@ -543,6 +559,7 @@ class _DatePickerDialogState extends State with RestorationMix )), ), TextButton( + style: datePickerTheme.confirmButtonStyle ?? defaults.confirmButtonStyle, onPressed: _handleOk, child: Text(widget.confirmText ?? localizations.okButtonLabel), ), @@ -633,7 +650,7 @@ class _DatePickerDialogState extends State with RestorationMix ? localizations.datePickerHelpText : localizations.datePickerHelpText.toUpperCase() ), - titleText: localizations.formatMediumDate(_selectedDate.value), + titleText: _selectedDate.value == null ? '' : localizations.formatMediumDate(_selectedDate.value!), titleStyle: headlineStyle, orientation: orientation, isShort: orientation == Orientation.landscape, @@ -642,7 +659,7 @@ class _DatePickerDialogState extends State with RestorationMix // Constrain the textScaleFactor to the largest supported value to prevent // layout issues. - final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.3); + final double textScaleFactor = MediaQuery.textScalerOf(context).clamp(maxScaleFactor: _kMaxTextScaleFactor).textScaleFactor; final Size dialogSize = _dialogSize(context) * textScaleFactor; final DialogTheme dialogTheme = theme.dialogTheme; return Dialog( @@ -662,11 +679,16 @@ class _DatePickerDialogState extends State with RestorationMix height: dialogSize.height, duration: _dialogSizeAnimationDuration, curve: Curves.easeIn, - child: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: textScaleFactor, - ), - child: Builder(builder: (BuildContext context) { + child: MediaQuery.withClampedTextScaling( + // Constrain the textScaleFactor to the largest supported value to prevent + // layout issues. + maxScaleFactor: _kMaxTextScaleFactor, + child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + final Size portraitDialogSize = useMaterial3 ? _inputPortraitDialogSizeM3 : _inputPortraitDialogSizeM2; + // Make sure the portrait dialog can fit the contents comfortably when + // resized from the landscape dialog. + final bool isFullyPortrait = constraints.maxHeight >= portraitDialogSize.height; + switch (orientation) { case Orientation.portrait: return Column( @@ -675,8 +697,10 @@ class _DatePickerDialogState extends State with RestorationMix children: [ header, if (useMaterial3) Divider(height: 0, color: datePickerTheme.dividerColor), - Expanded(child: picker), - actions, + if (isFullyPortrait) ...[ + Expanded(child: picker), + actions, + ], ], ); case Orientation.landscape: @@ -918,7 +942,7 @@ class _DatePickerHeader extends StatelessWidget { /// before or on `initialDateRange.end`. /// /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest -/// allowable date. Both must be non-null. +/// allowable date. /// /// If an initial date range is provided, `initialDateRange.start` /// and `initialDateRange.end` must both fall between or on [firstDate] and @@ -932,7 +956,7 @@ class _DatePickerHeader extends StatelessWidget { /// An optional [initialEntryMode] argument can be used to display the date /// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month /// grid) or [DatePickerEntryMode.input] (two text input fields) mode. -/// It defaults to [DatePickerEntryMode.calendar] and must be non-null. +/// It defaults to [DatePickerEntryMode.calendar]. /// /// {@macro flutter.material.date_picker.switchToInputEntryModeIcon} /// @@ -968,9 +992,9 @@ class _DatePickerHeader extends StatelessWidget { /// [locale] and [textDirection] are non-null, [textDirection] overrides the /// direction chosen for the [locale]. /// -/// The [context], [useRootNavigator] and [routeSettings] arguments are passed -/// to [showDialog], the documentation for which discusses how it is used. -/// [context] and [useRootNavigator] must be non-null. +/// The [context], [barrierDismissible], [barrierColor], [barrierLabel], +/// [useRootNavigator] and [routeSettings] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Theme]. @@ -1023,6 +1047,9 @@ Future showDateRangePicker({ String? fieldStartLabelText, String? fieldEndLabelText, Locale? locale, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, bool useRootNavigator = true, RouteSettings? routeSettings, TextDirection? textDirection, @@ -1101,6 +1128,9 @@ Future showDateRangePicker({ return showDialog( context: context, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, useRootNavigator: useRootNavigator, routeSettings: routeSettings, useSafeArea: false, @@ -1205,7 +1235,7 @@ class DateRangePickerDialog extends StatefulWidget { /// scrollable calendar month grid) or [DatePickerEntryMode.input] (two text /// input fields) mode. /// - /// It defaults to [DatePickerEntryMode.calendar] and must be non-null. + /// It defaults to [DatePickerEntryMode.calendar]. final DatePickerEntryMode initialEntryMode; /// The label on the cancel button for the text input mode. @@ -1352,7 +1382,7 @@ class _DateRangePickerDialogState extends State with Rest _entryMode.value = DatePickerEntryMode.input; case DatePickerEntryMode.input: - // Validate the range dates + // Validate the range dates if (_selectedStart.value != null && (_selectedStart.value!.isBefore(widget.firstDate) || _selectedStart.value!.isAfter(widget.lastDate))) { _selectedStart.value = null; @@ -1391,7 +1421,6 @@ class _DateRangePickerDialogState extends State with Rest final ThemeData theme = Theme.of(context); final bool useMaterial3 = theme.useMaterial3; final Orientation orientation = MediaQuery.orientationOf(context); - final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.3); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context); final DatePickerThemeData defaults = DatePickerTheme.defaults(context); @@ -1535,10 +1564,8 @@ class _DateRangePickerDialogState extends State with Rest height: size.height, duration: _dialogSizeAnimationDuration, curve: Curves.easeIn, - child: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: textScaleFactor, - ), + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: _kMaxTextScaleFactor, child: Builder(builder: (BuildContext context) { return contents; }), @@ -2081,14 +2108,11 @@ class _DayHeaders extends StatelessWidget { /// List _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) { final List result = []; - for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { + for (int i = localizations.firstDayOfWeekIndex; result.length < DateTime.daysPerWeek; i = (i + 1) % DateTime.daysPerWeek) { final String weekday = localizations.narrowWeekdays[i]; result.add(ExcludeSemantics( child: Center(child: Text(weekday, style: headerStyle)), )); - if (i == (localizations.firstDayOfWeekIndex - 1) % 7) { - break; - } } return result; } @@ -2500,13 +2524,9 @@ class _MonthItemState extends State<_MonthItem> { final double gridHeight = weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows; final List dayItems = []; - for (int i = 0; true; i += 1) { - // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on - // a leap year. - final int day = i - dayOffset + 1; - if (day > daysInMonth) { - break; - } + // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on + // a leap year. + for (int day = 0 - dayOffset + 1; day <= daysInMonth; day += 1) { if (day < 1) { dayItems.add(Container()); } else { @@ -2770,14 +2790,25 @@ class _InputDateRangePickerDialog extends StatelessWidget { switch (orientation) { case Orientation.portrait: - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - header, - Expanded(child: picker), - actions, - ], + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final Size portraitDialogSize = useMaterial3 ? _inputPortraitDialogSizeM3 : _inputPortraitDialogSizeM2; + // Make sure the portrait dialog can fit the contents comfortably when + // resized from the landscape dialog. + final bool isFullyPortrait = constraints.maxHeight >= portraitDialogSize.height; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + if (isFullyPortrait) ...[ + Expanded(child: picker), + actions, + ], + ], + ); + } ); case Orientation.landscape: diff --git a/packages/flutter/lib/src/material/date_picker_theme.dart b/packages/flutter/lib/src/material/date_picker_theme.dart index a970ffbd5dbd2..21ffc60ca57dc 100644 --- a/packages/flutter/lib/src/material/date_picker_theme.dart +++ b/packages/flutter/lib/src/material/date_picker_theme.dart @@ -7,10 +7,12 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'button_style.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'input_decorator.dart'; import 'material_state.dart'; +import 'text_button.dart'; import 'text_theme.dart'; import 'theme.dart'; @@ -70,6 +72,8 @@ class DatePickerThemeData with Diagnosticable { this.rangeSelectionOverlayColor, this.dividerColor, this.inputDecorationTheme, + this.cancelButtonStyle, + this.confirmButtonStyle, }); /// Overrides the default value of [Dialog.backgroundColor]. @@ -294,6 +298,12 @@ class DatePickerThemeData with Diagnosticable { /// If this is null, [ThemeData.inputDecorationTheme] is used instead. final InputDecorationTheme? inputDecorationTheme; + /// Overrides the default style of the cancel button of a [DatePickerDialog]. + final ButtonStyle? cancelButtonStyle; + + /// Overrrides the default style of the confirm (OK) button of a [DatePickerDialog]. + final ButtonStyle? confirmButtonStyle; + /// Creates a copy of this object with the given fields replaced with the /// new values. DatePickerThemeData copyWith({ @@ -331,6 +341,8 @@ class DatePickerThemeData with Diagnosticable { MaterialStateProperty? rangeSelectionOverlayColor, Color? dividerColor, InputDecorationTheme? inputDecorationTheme, + ButtonStyle? cancelButtonStyle, + ButtonStyle? confirmButtonStyle, }) { return DatePickerThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -367,6 +379,8 @@ class DatePickerThemeData with Diagnosticable { rangeSelectionOverlayColor: rangeSelectionOverlayColor ?? this.rangeSelectionOverlayColor, dividerColor: dividerColor ?? this.dividerColor, inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme, + cancelButtonStyle: cancelButtonStyle ?? this.cancelButtonStyle, + confirmButtonStyle: confirmButtonStyle ?? this.confirmButtonStyle, ); } @@ -410,6 +424,8 @@ class DatePickerThemeData with Diagnosticable { rangeSelectionOverlayColor: MaterialStateProperty.lerp(a?.rangeSelectionOverlayColor, b?.rangeSelectionOverlayColor, t, Color.lerp), dividerColor: Color.lerp(a?.dividerColor, b?.dividerColor, t), inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme, + cancelButtonStyle: ButtonStyle.lerp(a?.cancelButtonStyle, b?.cancelButtonStyle, t), + confirmButtonStyle: ButtonStyle.lerp(a?.confirmButtonStyle, b?.confirmButtonStyle, t), ); } @@ -459,6 +475,8 @@ class DatePickerThemeData with Diagnosticable { rangeSelectionOverlayColor, dividerColor, inputDecorationTheme, + cancelButtonStyle, + confirmButtonStyle, ]); @override @@ -500,7 +518,9 @@ class DatePickerThemeData with Diagnosticable { && other.rangeSelectionBackgroundColor == rangeSelectionBackgroundColor && other.rangeSelectionOverlayColor == rangeSelectionOverlayColor && other.dividerColor == dividerColor - && other.inputDecorationTheme == inputDecorationTheme; + && other.inputDecorationTheme == inputDecorationTheme + && other.cancelButtonStyle == cancelButtonStyle + && other.confirmButtonStyle == confirmButtonStyle; } @override @@ -540,6 +560,8 @@ class DatePickerThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('rangeSelectionOverlayColor', rangeSelectionOverlayColor, defaultValue: null)); properties.add(ColorProperty('dividerColor', dividerColor, defaultValue: null)); properties.add(DiagnosticsProperty('inputDecorationTheme', inputDecorationTheme, defaultValue: null)); + properties.add(DiagnosticsProperty('cancelButtonStyle', cancelButtonStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('confirmButtonStyle', confirmButtonStyle, defaultValue: null)); } } @@ -658,6 +680,16 @@ class _DatePickerDefaultsM2 extends DatePickerThemeData { @override Color? get headerBackgroundColor => _isDark ? _colors.surface : _colors.primary; + @override + ButtonStyle get cancelButtonStyle { + return TextButton.styleFrom(); + } + + @override + ButtonStyle get confirmButtonStyle { + return TextButton.styleFrom(); + } + @override Color? get headerForegroundColor => _isDark ? _colors.onSurface : _colors.onPrimary; @@ -818,6 +850,16 @@ class _DatePickerDefaultsM3 extends DatePickerThemeData { @override Color? get backgroundColor => _colors.surface; + @override + ButtonStyle get cancelButtonStyle { + return TextButton.styleFrom(); + } + + @override + ButtonStyle get confirmButtonStyle { + return TextButton.styleFrom(); + } + @override Color? get shadowColor => Colors.transparent; @@ -993,8 +1035,6 @@ class _DatePickerDefaultsM3 extends DatePickerThemeData { @override TextStyle? get rangePickerHeaderHelpStyle => _textTheme.titleSmall; - - } // END GENERATED TOKEN PROPERTIES - DatePicker diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 181aff65f5ca0..a9ca87452c813 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui'; +import 'dart:ui' show clampDouble, lerpDouble; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart' show clampDouble; import 'color_scheme.dart'; import 'colors.dart'; @@ -183,7 +182,7 @@ class Dialog extends StatelessWidget { /// See the enum [Clip] for details of all possible options and their common /// use cases. /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. /// {@endtemplate} final Clip clipBehavior; @@ -719,7 +718,7 @@ class AlertDialog extends StatelessWidget { // The paddingScaleFactor is used to adjust the padding of Dialog's // children. - final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScaleFactorOf(context)); + final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScalerOf(context).textScaleFactor); final TextDirection? textDirection = Directionality.maybeOf(context); Widget? iconWidget; @@ -1097,8 +1096,6 @@ class SimpleDialog extends StatelessWidget { /// Creates a simple dialog. /// /// Typically used in conjunction with [showDialog]. - /// - /// The [titlePadding] and [contentPadding] arguments must not be null. const SimpleDialog({ super.key, this.title, @@ -1216,7 +1213,7 @@ class SimpleDialog extends StatelessWidget { // The paddingScaleFactor is used to adjust the padding of Dialog // children. - final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScaleFactorOf(context)); + final double paddingScaleFactor = _paddingScaleFactor(MediaQuery.textScalerOf(context).textScaleFactor); final TextDirection? textDirection = Directionality.maybeOf(context); Widget? titleWidget; @@ -1404,7 +1401,7 @@ Future showDialog({ required BuildContext context, required WidgetBuilder builder, bool barrierDismissible = true, - Color? barrierColor = Colors.black54, + Color? barrierColor, String? barrierLabel, bool useSafeArea = true, bool useRootNavigator = true, @@ -1426,7 +1423,7 @@ Future showDialog({ return Navigator.of(context, rootNavigator: useRootNavigator).push(DialogRoute( context: context, builder: builder, - barrierColor: barrierColor, + barrierColor: barrierColor ?? Colors.black54, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, useSafeArea: useSafeArea, @@ -1449,7 +1446,7 @@ Future showAdaptiveDialog({ required BuildContext context, required WidgetBuilder builder, bool? barrierDismissible, - Color? barrierColor = Colors.black54, + Color? barrierColor, String? barrierLabel, bool useSafeArea = true, bool useRootNavigator = true, diff --git a/packages/flutter/lib/src/material/dialog_theme.dart b/packages/flutter/lib/src/material/dialog_theme.dart index 8f318cf8d0622..f27728418b64d 100644 --- a/packages/flutter/lib/src/material/dialog_theme.dart +++ b/packages/flutter/lib/src/material/dialog_theme.dart @@ -107,8 +107,6 @@ class DialogTheme with Diagnosticable { /// Linearly interpolate between two dialog themes. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/divider_theme.dart b/packages/flutter/lib/src/material/divider_theme.dart index 93975aa34e27c..b9f0a52fccd65 100644 --- a/packages/flutter/lib/src/material/divider_theme.dart +++ b/packages/flutter/lib/src/material/divider_theme.dart @@ -83,8 +83,6 @@ class DividerThemeData with Diagnosticable { /// Linearly interpolate between two Divider themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static DividerThemeData lerp(DividerThemeData? a, DividerThemeData? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index ecaecb2c0d482..abb67e32858a5 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -79,61 +79,24 @@ const Duration _kBaseSettleDuration = Duration(milliseconds: 246); /// are a little bit different, see the Material 3 spec at /// for /// more details. While the [Drawer] widget can have only one child, the -/// [NavigationDrawer] widget can have list of widgets, which typically contains +/// [NavigationDrawer] widget can have a list of widgets, which typically contains /// [NavigationDrawerDestination] widgets and/or customized widgets like headlines /// and dividers. /// -/// {@tool snippet} +/// {@tool dartpad} /// This example shows how to create a [Scaffold] that contains an [AppBar] and /// a [Drawer]. A user taps the "menu" icon in the [AppBar] to open the /// [Drawer]. The [Drawer] displays four items: A header and three menu items. /// The [Drawer] displays the four items using a [ListView], which allows the /// user to scroll through the items if need be. /// -/// ```dart -/// Scaffold( -/// appBar: AppBar( -/// title: const Text('Drawer Demo'), -/// ), -/// drawer: Drawer( -/// child: ListView( -/// padding: EdgeInsets.zero, -/// children: const [ -/// DrawerHeader( -/// decoration: BoxDecoration( -/// color: Colors.blue, -/// ), -/// child: Text( -/// 'Drawer Header', -/// style: TextStyle( -/// color: Colors.white, -/// fontSize: 24, -/// ), -/// ), -/// ), -/// ListTile( -/// leading: Icon(Icons.message), -/// title: Text('Messages'), -/// ), -/// ListTile( -/// leading: Icon(Icons.account_circle), -/// title: Text('Profile'), -/// ), -/// ListTile( -/// leading: Icon(Icons.settings), -/// title: Text('Settings'), -/// ), -/// ], -/// ), -/// ), -/// ) -/// ``` +/// ** See code in examples/api/lib/material/drawer/drawer.0.dart ** /// {@end-tool} /// -/// {@tool snippet} +/// {@tool dartpad} /// This example shows how to migrate the above [Drawer] to a [NavigationDrawer]. /// -/// See code in examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart ** +/// ** See code in examples/api/lib/material/navigation_drawer/navigation_drawer.0.dart ** /// {@end-tool} /// /// An open drawer may be closed with a swipe to close gesture, pressing the @@ -347,7 +310,7 @@ class DrawerController extends StatefulWidget { /// /// Rarely used directly. /// - /// The [child] argument must not be null and is typically a [Drawer]. + /// The [child] argument is typically a [Drawer]. const DrawerController({ GlobalKey? key, required this.child, @@ -508,6 +471,7 @@ class DrawerControllerState extends State with SingleTickerPro void dispose() { _historyEntry?.remove(); _controller.dispose(); + _focusScopeNode.dispose(); super.dispose(); } diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index a115b0982b134..719488109270d 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -15,6 +15,7 @@ import 'constants.dart'; import 'debug.dart'; import 'icons.dart'; import 'ink_well.dart'; +import 'input_border.dart'; import 'input_decorator.dart'; import 'material.dart'; import 'material_localizations.dart'; @@ -101,9 +102,11 @@ class _DropdownMenuItemButton extends StatefulWidget { required this.constraints, required this.itemIndex, required this.enableFeedback, + required this.scrollController, }); final _DropdownRoute route; + final ScrollController scrollController; final EdgeInsets? padding; final Rect buttonRect; final BoxConstraints constraints; @@ -130,7 +133,7 @@ class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> widget.constraints.maxHeight, widget.itemIndex, ); - widget.route.scrollController!.animateTo( + widget.scrollController.animateTo( menuLimits.scrollOffset, curve: Curves.easeInOut, duration: const Duration(milliseconds: 100), @@ -204,6 +207,7 @@ class _DropdownMenu extends StatefulWidget { this.dropdownColor, required this.enableFeedback, this.borderRadius, + required this.scrollController, }); final _DropdownRoute route; @@ -213,6 +217,7 @@ class _DropdownMenu extends StatefulWidget { final Color? dropdownColor; final bool enableFeedback; final BorderRadius? borderRadius; + final ScrollController scrollController; @override _DropdownMenuState createState() => _DropdownMenuState(); @@ -263,6 +268,7 @@ class _DropdownMenuState extends State<_DropdownMenu> { constraints: widget.constraints, itemIndex: itemIndex, enableFeedback: widget.enableFeedback, + scrollController: widget.scrollController, ), ]; @@ -303,7 +309,7 @@ class _DropdownMenuState extends State<_DropdownMenu> { platform: Theme.of(context).platform, ), child: PrimaryScrollController( - controller: widget.route.scrollController!, + controller: widget.scrollController, child: Scrollbar( thumbVisibility: true, child: ListView( @@ -446,7 +452,6 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { final BorderRadius? borderRadius; final List itemHeights; - ScrollController? scrollController; @override Duration get transitionDuration => _kDropdownMenuDuration; @@ -571,7 +576,7 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { } } -class _DropdownRoutePage extends StatelessWidget { +class _DropdownRoutePage extends StatefulWidget { const _DropdownRoutePage({ super.key, required this.route, @@ -602,8 +607,15 @@ class _DropdownRoutePage extends StatelessWidget { final BorderRadius? borderRadius; @override - Widget build(BuildContext context) { - assert(debugCheckHasDirectionality(context)); + State<_DropdownRoutePage> createState() => _DropdownRoutePageState(); +} + +class _DropdownRoutePageState extends State<_DropdownRoutePage> { + late ScrollController _scrollSontroller; + + @override + void initState(){ + super.initState(); // Computing the initialScrollOffset now, before the items have been laid // out. This only works if the item heights are effectively fixed, i.e. either @@ -611,20 +623,25 @@ class _DropdownRoutePage extends StatelessWidget { // and all of the items' intrinsic heights are less than kMinInteractiveDimension. // Otherwise the initialScrollOffset is just a rough approximation based on // treating the items as if their heights were all equal to kMinInteractiveDimension. - if (route.scrollController == null) { - final _MenuLimits menuLimits = route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex); - route.scrollController = ScrollController(initialScrollOffset: menuLimits.scrollOffset); - } + final _MenuLimits menuLimits = widget.route.getMenuLimits(widget.buttonRect, widget.constraints.maxHeight, widget.selectedIndex); + _scrollSontroller = ScrollController(initialScrollOffset: menuLimits.scrollOffset); + } + + + @override + Widget build(BuildContext context) { + assert(debugCheckHasDirectionality(context)); final TextDirection? textDirection = Directionality.maybeOf(context); final Widget menu = _DropdownMenu( - route: route, - padding: padding.resolve(textDirection), - buttonRect: buttonRect, - constraints: constraints, - dropdownColor: dropdownColor, - enableFeedback: enableFeedback, - borderRadius: borderRadius, + route: widget.route, + padding: widget.padding.resolve(textDirection), + buttonRect: widget.buttonRect, + constraints: widget.constraints, + dropdownColor: widget.dropdownColor, + enableFeedback: widget.enableFeedback, + borderRadius: widget.borderRadius, + scrollController: _scrollSontroller, ); return MediaQuery.removePadding( @@ -637,16 +654,22 @@ class _DropdownRoutePage extends StatelessWidget { builder: (BuildContext context) { return CustomSingleChildLayout( delegate: _DropdownMenuRouteLayout( - buttonRect: buttonRect, - route: route, + buttonRect: widget.buttonRect, + route: widget.route, textDirection: textDirection, ), - child: capturedThemes.wrap(menu), + child: widget.capturedThemes.wrap(menu), ); }, ), ); } + + @override + void dispose() { + _scrollSontroller.dispose(); + super.dispose(); + } } // This widget enables _DropdownRoute to look up the sizes of @@ -707,7 +730,7 @@ class _DropdownMenuItemContainer extends StatelessWidget { /// Defines how the item is positioned within the container. /// - /// This property must not be null. It defaults to [AlignmentDirectional.centerStart]. + /// Defaults to [AlignmentDirectional.centerStart]. /// /// See also: /// @@ -889,12 +912,6 @@ class DropdownButton extends StatefulWidget { /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed /// if it is non-null. /// - /// The [elevation] and [iconSize] arguments must not be null (they both have - /// defaults, so do not need to be specified). The boolean [isDense] and - /// [isExpanded] arguments must not be null. - /// - /// The [autofocus] argument must not be null. - /// /// The [dropdownColor] argument specifies the background color of the /// dropdown when it is open. If it is null, the current theme's /// [ThemeData.canvasColor] will be used instead. @@ -1192,7 +1209,7 @@ class DropdownButton extends StatefulWidget { /// Defines how the hint or the selected item is positioned within the button. /// - /// This property must not be null. It defaults to [AlignmentDirectional.centerStart]. + /// Defaults to [AlignmentDirectional.centerStart]. /// /// See also: /// @@ -1355,9 +1372,8 @@ class _DropdownButtonState extends State> with WidgetsBindi // Similarly, we don't reduce the height of the button so much that its icon // would be clipped. double get _denseButtonHeight { - final double textScaleFactor = MediaQuery.textScaleFactorOf(context); final double fontSize = _textStyle!.fontSize ?? Theme.of(context).textTheme.titleMedium!.fontSize!; - final double scaledFontSize = textScaleFactor * fontSize; + final double scaledFontSize = MediaQuery.textScalerOf(context).scale(fontSize); return math.max(scaledFontSize, math.max(widget.iconSize, _kDenseButtonHeight)); } @@ -1567,9 +1583,6 @@ class DropdownButtonFormField extends FormField { /// For a description of the `onSaved`, `validator`, or `autovalidateMode` /// parameters, see [FormField]. For the rest (other than [decoration]), see /// [DropdownButton]. - /// - /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`, - /// `autofocus`, and `decoration` parameters must not be null. DropdownButtonFormField({ super.key, required List>? items, @@ -1634,6 +1647,7 @@ class DropdownButtonFormField extends FormField { } } final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable(); + final bool hasError = effectiveDecoration.errorText != null; // An unfocusable Focus widget so that this widget can detect if its // descendants have focus or not. @@ -1641,6 +1655,33 @@ class DropdownButtonFormField extends FormField { canRequestFocus: false, skipTraversal: true, child: Builder(builder: (BuildContext context) { + final bool isFocused = Focus.of(context).hasFocus; + InputBorder? resolveInputBorder() { + if (hasError) { + if (isFocused) { + return effectiveDecoration.focusedErrorBorder; + } + return effectiveDecoration.errorBorder; + } + if (isFocused) { + return effectiveDecoration.focusedBorder; + } + if (effectiveDecoration.enabled) { + return effectiveDecoration.enabledBorder; + } + return effectiveDecoration.border; + } + BorderRadius? effectiveBorderRadius() { + final InputBorder? inputBorder = resolveInputBorder(); + if (inputBorder is OutlineInputBorder) { + return inputBorder.borderRadius; + } + if (inputBorder is UnderlineInputBorder) { + return inputBorder.borderRadius; + } + return null; + } + return DropdownButtonHideUnderline( child: DropdownButton._formField( items: items, @@ -1666,10 +1707,10 @@ class DropdownButtonFormField extends FormField { menuMaxHeight: menuMaxHeight, enableFeedback: enableFeedback, alignment: alignment, - borderRadius: borderRadius, + borderRadius: borderRadius ?? effectiveBorderRadius(), inputDecoration: effectiveDecoration.copyWith(errorText: field.errorText), isEmpty: isEmpty, - isFocused: Focus.of(context).hasFocus, + isFocused: isFocused, padding: padding, ), ); @@ -1695,13 +1736,12 @@ class DropdownButtonFormField extends FormField { } class _DropdownButtonFormFieldState extends FormFieldState { + DropdownButtonFormField get _dropdownButtonFormField => widget as DropdownButtonFormField; @override void didChange(T? value) { super.didChange(value); - final DropdownButtonFormField dropdownButtonFormField = widget as DropdownButtonFormField; - assert(dropdownButtonFormField.onChanged != null); - dropdownButtonFormField.onChanged!(value); + _dropdownButtonFormField.onChanged!(value); } @override @@ -1711,4 +1751,10 @@ class _DropdownButtonFormFieldState extends FormFieldState { setValue(widget.initialValue); } } + + @override + void reset() { + super.reset(); + _dropdownButtonFormField.onChanged!(value); + } } diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 0d7125d6be56b..51b17e5b2cb1a 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -39,11 +39,10 @@ const double _kDefaultHorizontalPadding = 12.0; /// * [DropdownMenu] class DropdownMenuEntry { /// Creates an entry that is used with [DropdownMenu.dropdownMenuEntries]. - /// - /// [label] must be non-null. const DropdownMenuEntry({ required this.value, required this.label, + this.labelWidget, this.leadingIcon, this.trailingIcon, this.enabled = true, @@ -58,6 +57,17 @@ class DropdownMenuEntry { /// The label displayed in the center of the menu item. final String label; + /// Overrides the default label widget which is `Text(label)`. + /// + /// {@tool dartpad} + /// This sample shows how to override the default label [Text] + /// widget with one that forces the menu entry to appear on one line + /// by specifying [Text.maxLines] and [Text.overflow]. + /// + /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart ** + /// {@end-tool} + final Widget? labelWidget; + /// An optional icon to display before the label. final Widget? leadingIcon; @@ -139,6 +149,7 @@ class DropdownMenu extends StatefulWidget { this.initialSelection, this.onSelected, this.requestFocusOnTap, + this.expandedInsets, required this.dropdownMenuEntries, }); @@ -228,7 +239,7 @@ class DropdownMenu extends StatefulWidget { /// The text style for the [TextField] of the [DropdownMenu]; /// - /// Defaults to the overall theme's [TextTheme.labelLarge] + /// Defaults to the overall theme's [TextTheme.bodyLarge] /// if the dropdown menu theme's value is null. final TextStyle? textStyle; @@ -277,6 +288,21 @@ class DropdownMenu extends StatefulWidget { /// contain space for padding. final List> dropdownMenuEntries; + /// Defines the menu text field's width to be equal to its parent's width + /// plus the horizontal width of the specified insets. + /// + /// If this property is null, the width of the text field will be determined + /// by the width of menu items or [DropdownMenu.width]. If this property is not null, + /// the text field's width will match the parent's width plus the specified insets. + /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same + /// as its parent's width. + /// + /// The [expandedInsets]' top and bottom are ignored, only its left and right + /// properties are used. + /// + /// Defaults to null. + final EdgeInsets? expandedInsets; + @override State> createState() => _DropdownMenuState(); } @@ -406,25 +432,41 @@ class _DropdownMenuState extends State> { { int? focusedIndex, bool enableScrollToHighlight = true} ) { final List result = []; - final double padding = leadingPadding ?? _kDefaultHorizontalPadding; - final ButtonStyle defaultStyle; - switch (textDirection) { - case TextDirection.rtl: - defaultStyle = MenuItemButton.styleFrom( - padding: EdgeInsets.only(left: _kDefaultHorizontalPadding, right: padding), - ); - case TextDirection.ltr: - defaultStyle = MenuItemButton.styleFrom( - padding: EdgeInsets.only(left: padding, right: _kDefaultHorizontalPadding), - ); - } - for (int i = 0; i < filteredEntries.length; i++) { final DropdownMenuEntry entry = filteredEntries[i]; + + // By default, when the text field has a leading icon but a menu entry doesn't + // have one, the label of the entry should have extra padding to be aligned + // with the text in the text input field. When both the text field and the + // menu entry have leading icons, the menu entry should remove the extra + // paddings so its leading icon will be aligned with the leading icon of + // the text field. + final double padding = entry.leadingIcon == null ? (leadingPadding ?? _kDefaultHorizontalPadding) : _kDefaultHorizontalPadding; + final ButtonStyle defaultStyle; + switch (textDirection) { + case TextDirection.rtl: + defaultStyle = MenuItemButton.styleFrom( + padding: EdgeInsets.only(left: _kDefaultHorizontalPadding, right: padding), + ); + case TextDirection.ltr: + defaultStyle = MenuItemButton.styleFrom( + padding: EdgeInsets.only(left: padding, right: _kDefaultHorizontalPadding), + ); + } + ButtonStyle effectiveStyle = entry.style ?? defaultStyle; final Color focusedBackgroundColor = effectiveStyle.foregroundColor?.resolve({MaterialState.focused}) ?? Theme.of(context).colorScheme.onSurface; + Widget label = entry.labelWidget ?? Text(entry.label); + if (widget.width != null) { + final double horizontalPadding = padding + _kDefaultHorizontalPadding; + label = ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.width! - horizontalPadding), + child: label, + ); + } + // Simulate the focused state because the text field should always be focused // during traversal. If the menu item has a custom foreground color, the "focused" // color will also change to foregroundColor.withOpacity(0.12). @@ -434,7 +476,7 @@ class _DropdownMenuState extends State> { ) : effectiveStyle; - final MenuItemButton menuItemButton = MenuItemButton( + final Widget menuItemButton = MenuItemButton( key: enableScrollToHighlight ? buttonItemKeys[i] : null, style: effectiveStyle, leadingIcon: entry.leadingIcon, @@ -449,7 +491,7 @@ class _DropdownMenuState extends State> { } : null, requestFocusOnHover: false, - child: Text(entry.label), + child: label, ); result.add(menuItemButton); } @@ -504,6 +546,9 @@ class _DropdownMenuState extends State> { @override void dispose() { + if (widget.controller == null) { + _textEditingController.dispose(); + } super.dispose(); } @@ -549,6 +594,106 @@ class _DropdownMenuState extends State> { final MouseCursor effectiveMouseCursor = canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click; + Widget menuAnchor = MenuAnchor( + style: effectiveMenuStyle, + controller: _controller, + menuChildren: menu, + crossAxisUnconstrained: false, + builder: (BuildContext context, MenuController controller, Widget? child) { + assert(_initialMenu != null); + final Widget trailingButton = Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + isSelected: controller.isOpen, + icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), + selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), + onPressed: () { + handlePressed(controller); + }, + ), + ); + + final Widget leadingButton = Padding( + padding: const EdgeInsets.all(8.0), + child: widget.leadingIcon ?? const SizedBox() + ); + + final Widget textField = TextField( + key: _anchorKey, + mouseCursor: effectiveMouseCursor, + canRequestFocus: canRequestFocus(), + enableInteractiveSelection: canRequestFocus(), + textAlignVertical: TextAlignVertical.center, + style: effectiveTextStyle, + controller: _textEditingController, + onEditingComplete: () { + if (currentHighlight != null) { + final DropdownMenuEntry entry = filteredEntries[currentHighlight!]; + if (entry.enabled) { + _textEditingController.text = entry.label; + _textEditingController.selection = + TextSelection.collapsed(offset: _textEditingController.text.length); + widget.onSelected?.call(entry.value); + } + } else { + widget.onSelected?.call(null); + } + if (!widget.enableSearch) { + currentHighlight = null; + } + controller.close(); + }, + onTap: () { + handlePressed(controller); + }, + onChanged: (String text) { + controller.open(); + setState(() { + filteredEntries = widget.dropdownMenuEntries; + _enableFilter = widget.enableFilter; + }); + }, + decoration: InputDecoration( + enabled: widget.enabled, + label: widget.label, + hintText: widget.hintText, + helperText: widget.helperText, + errorText: widget.errorText, + prefixIcon: widget.leadingIcon != null ? Container( + key: _leadingKey, + child: widget.leadingIcon + ) : null, + suffixIcon: trailingButton, + ).applyDefaults(effectiveInputDecorationTheme) + ); + + if (widget.expandedInsets != null) { + // If [expandedInsets] is not null, the width of the text field should depend + // on its parent width. So we don't need to use `_DropdownMenuBody` to + // calculate the children's width. + return textField; + } + + return _DropdownMenuBody( + width: widget.width, + children: [ + textField, + for (final Widget item in _initialMenu!) item, + trailingButton, + leadingButton, + ], + ); + }, + ); + + if (widget.expandedInsets != null) { + menuAnchor = Container( + alignment: AlignmentDirectional.topStart, + padding: widget.expandedInsets?.copyWith(top: 0.0, bottom: 0.0), + child: menuAnchor, + ); + } + return Shortcuts( shortcuts: _kMenuTraversalShortcuts, child: Actions( @@ -560,90 +705,7 @@ class _DropdownMenuState extends State> { onInvoke: handleDownKeyInvoke, ), }, - child: MenuAnchor( - style: effectiveMenuStyle, - controller: _controller, - menuChildren: menu, - crossAxisUnconstrained: false, - builder: (BuildContext context, MenuController controller, Widget? child) { - assert(_initialMenu != null); - final Widget trailingButton = Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - isSelected: controller.isOpen, - icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), - selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), - onPressed: () { - handlePressed(controller); - }, - ), - ); - - final Widget leadingButton = Padding( - padding: const EdgeInsets.all(8.0), - child: widget.leadingIcon ?? const SizedBox() - ); - - return _DropdownMenuBody( - width: widget.width, - children: [ - TextField( - key: _anchorKey, - mouseCursor: effectiveMouseCursor, - canRequestFocus: canRequestFocus(), - enableInteractiveSelection: canRequestFocus(), - textAlignVertical: TextAlignVertical.center, - style: effectiveTextStyle, - controller: _textEditingController, - onEditingComplete: () { - if (currentHighlight != null) { - final DropdownMenuEntry entry = filteredEntries[currentHighlight!]; - if (entry.enabled) { - _textEditingController.text = entry.label; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); - widget.onSelected?.call(entry.value); - } - } else { - widget.onSelected?.call(null); - } - if (!widget.enableSearch) { - currentHighlight = null; - } - if (_textEditingController.text.isNotEmpty) { - controller.close(); - } - }, - onTap: () { - handlePressed(controller); - }, - onChanged: (String text) { - controller.open(); - setState(() { - filteredEntries = widget.dropdownMenuEntries; - _enableFilter = widget.enableFilter; - }); - }, - decoration: InputDecoration( - enabled: widget.enabled, - label: widget.label, - hintText: widget.hintText, - helperText: widget.helperText, - errorText: widget.errorText, - prefixIcon: widget.leadingIcon != null ? Container( - key: _leadingKey, - child: widget.leadingIcon - ) : null, - suffixIcon: trailingButton, - ).applyDefaults(effectiveInputDecorationTheme) - ), - for (final Widget c in _initialMenu!) c, - trailingButton, - leadingButton, - ], - ); - }, - ), + child: menuAnchor, ), ); } @@ -883,7 +945,7 @@ class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { late final ThemeData _theme = Theme.of(context); @override - TextStyle? get textStyle => _theme.textTheme.labelLarge; + TextStyle? get textStyle => _theme.textTheme.bodyLarge; @override MenuStyle get menuStyle { diff --git a/packages/flutter/lib/src/material/elevated_button.dart b/packages/flutter/lib/src/material/elevated_button.dart index 92d57fb5d072b..9fd146dd6c22d 100644 --- a/packages/flutter/lib/src/material/elevated_button.dart +++ b/packages/flutter/lib/src/material/elevated_button.dart @@ -62,8 +62,6 @@ import 'theme_data.dart'; /// * class ElevatedButton extends ButtonStyleButton { /// Create an ElevatedButton. - /// - /// The [autofocus] and [clipBehavior] arguments must not be null. const ElevatedButton({ super.key, required super.onPressed, @@ -83,8 +81,6 @@ class ElevatedButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 12 logical pixels /// at the start, and 16 at the end, with an 8 pixel gap in between. - /// - /// The [icon] and [label] arguments must not be null. factory ElevatedButton.icon({ Key? key, required VoidCallback? onPressed, @@ -398,7 +394,7 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) { EdgeInsets.symmetric(horizontal: padding1x), EdgeInsets.symmetric(horizontal: padding1x / 2), EdgeInsets.symmetric(horizontal: padding1x / 2 / 2), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); } @@ -506,12 +502,12 @@ class _ElevatedButtonWithIcon extends ElevatedButton { const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0), const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0), const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ) : ButtonStyleButton.scaledPadding( const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0), const EdgeInsets.symmetric(horizontal: 8), const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); return super.defaultStyleOf(context).copyWith( padding: MaterialStatePropertyAll(scaledPadding), @@ -527,7 +523,7 @@ class _ElevatedButtonWithIconChild extends StatelessWidget { @override Widget build(BuildContext context) { - final double scale = MediaQuery.textScaleFactorOf(context); + final double scale = MediaQuery.textScalerOf(context).textScaleFactor; final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!; return Row( mainAxisSize: MainAxisSize.min, diff --git a/packages/flutter/lib/src/material/elevated_button_theme.dart b/packages/flutter/lib/src/material/elevated_button_theme.dart index 09964270103b7..bc2b93a778abf 100644 --- a/packages/flutter/lib/src/material/elevated_button_theme.dart +++ b/packages/flutter/lib/src/material/elevated_button_theme.dart @@ -91,8 +91,6 @@ class ElevatedButtonThemeData with Diagnosticable { /// [ButtonStyle] for [ElevatedButton]s below the overall [Theme]. class ElevatedButtonTheme extends InheritedTheme { /// Create a [ElevatedButtonTheme]. - /// - /// The [data] parameter must not be null. const ElevatedButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/expand_icon.dart b/packages/flutter/lib/src/material/expand_icon.dart index 10733d64e04f6..7672223655d49 100644 --- a/packages/flutter/lib/src/material/expand_icon.dart +++ b/packages/flutter/lib/src/material/expand_icon.dart @@ -49,7 +49,7 @@ class ExpandIcon extends StatefulWidget { /// The size of the icon. /// - /// This property must not be null. It defaults to 24.0. + /// Defaults to 24. final double size; /// The callback triggered when the icon is pressed and the state changes @@ -61,7 +61,7 @@ class ExpandIcon extends StatefulWidget { /// The padding around the icon. The entire padded icon will react to input /// gestures. /// - /// This property must not be null. It defaults to 8.0 padding on all sides. + /// Defaults to a padding of 8 on all sides. final EdgeInsetsGeometry padding; /// {@template flutter.material.ExpandIcon.color} diff --git a/packages/flutter/lib/src/material/expansion_panel.dart b/packages/flutter/lib/src/material/expansion_panel.dart index 409bb1c96c717..95eb2ad7ee801 100644 --- a/packages/flutter/lib/src/material/expansion_panel.dart +++ b/packages/flutter/lib/src/material/expansion_panel.dart @@ -74,8 +74,6 @@ typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool class ExpansionPanel { /// Creates an expansion panel to be used as a child for [ExpansionPanelList]. /// See [ExpansionPanelList] for an example on how to use this widget. - /// - /// The [headerBuilder], [body], and [isExpanded] arguments must not be null. ExpansionPanel({ required this.headerBuilder, required this.body, @@ -120,8 +118,7 @@ class ExpansionPanel { class ExpansionPanelRadio extends ExpansionPanel { /// An expansion panel that allows for radio functionality. /// - /// A unique [value] must be passed into the constructor. The - /// [headerBuilder], [body], [value] must not be null. + /// A unique [value] must be passed into the constructor. ExpansionPanelRadio({ required this.value, required super.headerBuilder, @@ -160,8 +157,6 @@ class ExpansionPanelRadio extends ExpansionPanel { class ExpansionPanelList extends StatefulWidget { /// Creates an expansion panel list widget. The [expansionCallback] is /// triggered when an expansion panel expand/collapse button is pushed. - /// - /// The [children] and [animationDuration] arguments must not be null. const ExpansionPanelList({ super.key, this.children = const [], @@ -177,10 +172,9 @@ class ExpansionPanelList extends StatefulWidget { /// Creates a radio expansion panel list widget. /// - /// This widget allows for at most one panel in the list to be open. - /// The expansion panel callback is triggered when an expansion panel - /// expand/collapse button is pushed. The [children] and [animationDuration] - /// arguments must not be null. The [children] objects must be instances + /// This widget allows for at most one panel in the list to be open. The + /// expansion panel callback is triggered when an expansion panel + /// expand/collapse button is pushed. The [children] objects must be instances /// of [ExpansionPanelRadio]. /// /// {@tool dartpad} diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 10ca52d5aab44..6756931c5866b 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -184,9 +184,9 @@ class ExpansionTileController { /// A single-line [ListTile] with an expansion arrow icon that expands or collapses /// the tile to reveal or hide the [children]. /// -/// This widget is typically used with [ListView] to create an -/// "expand / collapse" list entry. When used with scrolling widgets like -/// [ListView], a unique [PageStorageKey] must be specified to enable the +/// This widget is typically used with [ListView] to create an "expand / +/// collapse" list entry. When used with scrolling widgets like [ListView], a +/// unique [PageStorageKey] must be specified as the [key], to enable the /// [ExpansionTile] to save and restore its expanded state when it is scrolled /// in and out of view. /// @@ -668,6 +668,32 @@ class _ExpansionTileState extends State with SingleTickerProvider ); } + @override + void didUpdateWidget(covariant ExpansionTile oldWidget) { + super.didUpdateWidget(oldWidget); + final ThemeData theme = Theme.of(context); + final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context); + final ExpansionTileThemeData defaults = theme.useMaterial3 + ? _ExpansionTileDefaultsM3(context) + : _ExpansionTileDefaultsM2(context); + if (widget.collapsedShape != oldWidget.collapsedShape + || widget.shape != oldWidget.shape) { + _updateShapeBorder(expansionTileTheme, theme); + } + if (widget.collapsedTextColor != oldWidget.collapsedTextColor + || widget.textColor != oldWidget.textColor) { + _updateHeaderColor(expansionTileTheme, defaults); + } + if (widget.collapsedIconColor != oldWidget.collapsedIconColor + || widget.iconColor != oldWidget.iconColor) { + _updateIconColor(expansionTileTheme, defaults); + } + if (widget.backgroundColor != oldWidget.backgroundColor + || widget.collapsedBackgroundColor != oldWidget.collapsedBackgroundColor) { + _updateBackgroundColor(expansionTileTheme); + } + } + @override void didChangeDependencies() { final ThemeData theme = Theme.of(context); @@ -675,6 +701,14 @@ class _ExpansionTileState extends State with SingleTickerProvider final ExpansionTileThemeData defaults = theme.useMaterial3 ? _ExpansionTileDefaultsM3(context) : _ExpansionTileDefaultsM2(context); + _updateShapeBorder(expansionTileTheme, theme); + _updateHeaderColor(expansionTileTheme, defaults); + _updateIconColor(expansionTileTheme, defaults); + _updateBackgroundColor(expansionTileTheme); + super.didChangeDependencies(); + } + + void _updateShapeBorder(ExpansionTileThemeData expansionTileTheme, ThemeData theme) { _borderTween ..begin = widget.collapsedShape ?? expansionTileTheme.collapsedShape @@ -683,25 +717,33 @@ class _ExpansionTileState extends State with SingleTickerProvider bottom: BorderSide(color: Colors.transparent), ) ..end = widget.shape - ?? expansionTileTheme.collapsedShape + ?? expansionTileTheme.shape ?? Border( top: BorderSide(color: theme.dividerColor), bottom: BorderSide(color: theme.dividerColor), ); + } + + void _updateHeaderColor(ExpansionTileThemeData expansionTileTheme, ExpansionTileThemeData defaults) { _headerColorTween ..begin = widget.collapsedTextColor ?? expansionTileTheme.collapsedTextColor ?? defaults.collapsedTextColor ..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor; + } + + void _updateIconColor(ExpansionTileThemeData expansionTileTheme, ExpansionTileThemeData defaults) { _iconColorTween ..begin = widget.collapsedIconColor ?? expansionTileTheme.collapsedIconColor ?? defaults.collapsedIconColor ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor; + } + + void _updateBackgroundColor(ExpansionTileThemeData expansionTileTheme) { _backgroundColorTween ..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor ..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor; - super.didChangeDependencies(); } @override diff --git a/packages/flutter/lib/src/material/expansion_tile_theme.dart b/packages/flutter/lib/src/material/expansion_tile_theme.dart index 981c85a80b916..01d045ed692c9 100644 --- a/packages/flutter/lib/src/material/expansion_tile_theme.dart +++ b/packages/flutter/lib/src/material/expansion_tile_theme.dart @@ -210,8 +210,6 @@ class ExpansionTileThemeData with Diagnosticable { /// [ExpansionTileTheme] for [ExpansionTile]s below the overall [Theme]. class ExpansionTileTheme extends InheritedTheme { /// Applies the given theme [data] to [child]. - /// - /// The [data] and [child] arguments must not be null. const ExpansionTileTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index d0933f891bb5a..0943111084e4a 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -64,8 +64,6 @@ enum _FilledButtonVariant { filled, tonal } /// * class FilledButton extends ButtonStyleButton { /// Create a FilledButton. - /// - /// The [autofocus] and [clipBehavior] arguments must not be null. const FilledButton({ super.key, required super.onPressed, @@ -84,8 +82,6 @@ class FilledButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row with padding at the start and end /// and a gap between them. - /// - /// The [icon] and [label] arguments must not be null. factory FilledButton.icon({ Key? key, required VoidCallback? onPressed, @@ -107,8 +103,6 @@ class FilledButton extends ButtonStyleButton { /// [FilledButton] and [OutlinedButton]. They’re useful in contexts where /// a lower-priority button requires slightly more emphasis than an /// outline would give, such as "Next" in an onboarding flow. - /// - /// The [autofocus] and [clipBehavior] arguments must not be null. const FilledButton.tonal({ super.key, required super.onPressed, @@ -127,8 +121,6 @@ class FilledButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row with padding at the start and end /// and a gap between them. - /// - /// The [icon] and [label] arguments must not be null. factory FilledButton.tonalIcon({ Key? key, required VoidCallback? onPressed, @@ -285,7 +277,7 @@ class FilledButton extends ButtonStyleButton { /// each state, and "others" means all other states. /// /// The `textScaleFactor` is the value of - /// `MediaQuery.textScaleFactorOf(context)` and the names of the + /// `MediaQuery.textScalerOf(context).textScaleFactor` and the names of the /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been /// abbreviated for readability. /// @@ -411,7 +403,7 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) { EdgeInsets.symmetric(horizontal: padding1x), EdgeInsets.symmetric(horizontal: padding1x / 2), EdgeInsets.symmetric(horizontal: padding1x / 2 / 2), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); } @@ -514,12 +506,12 @@ class _FilledButtonWithIcon extends FilledButton { const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0), const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0), const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ) : ButtonStyleButton.scaledPadding( const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0), const EdgeInsets.symmetric(horizontal: 8), const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); return super.defaultStyleOf(context).copyWith( padding: MaterialStatePropertyAll(scaledPadding), @@ -535,7 +527,7 @@ class _FilledButtonWithIconChild extends StatelessWidget { @override Widget build(BuildContext context) { - final double scale = MediaQuery.textScaleFactorOf(context); + final double scale = MediaQuery.textScalerOf(context).textScaleFactor; // Adjust the gap based on the text scale factor. Start at 8, and lerp // to 4 based on how large the text is. final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!; diff --git a/packages/flutter/lib/src/material/filled_button_theme.dart b/packages/flutter/lib/src/material/filled_button_theme.dart index 72d59aae624a0..eebbf98e282d8 100644 --- a/packages/flutter/lib/src/material/filled_button_theme.dart +++ b/packages/flutter/lib/src/material/filled_button_theme.dart @@ -91,8 +91,6 @@ class FilledButtonThemeData with Diagnosticable { /// [ButtonStyle] for [FilledButton]s below the overall [Theme]. class FilledButtonTheme extends InheritedTheme { /// Create a [FilledButtonTheme]. - /// - /// The [data] parameter must not be null. const FilledButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/filter_chip.dart b/packages/flutter/lib/src/material/filter_chip.dart index 887bb05c87c1c..c56a1832b3dfd 100644 --- a/packages/flutter/lib/src/material/filter_chip.dart +++ b/packages/flutter/lib/src/material/filter_chip.dart @@ -338,7 +338,7 @@ class _FilterChipDefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart index 3b3fd352a7692..2869bd56ab377 100644 --- a/packages/flutter/lib/src/material/flexible_space_bar.dart +++ b/packages/flutter/lib/src/material/flexible_space_bar.dart @@ -156,6 +156,7 @@ class FlexibleSpaceBar extends StatefulWidget { double? minExtent, double? maxExtent, bool? isScrolledUnder, + bool? hasLeading, required double currentExtent, required Widget child, }) { @@ -164,6 +165,7 @@ class FlexibleSpaceBar extends StatefulWidget { minExtent: minExtent ?? currentExtent, maxExtent: maxExtent ?? currentExtent, isScrolledUnder: isScrolledUnder, + hasLeading: hasLeading, currentExtent: currentExtent, child: child, ); @@ -321,7 +323,7 @@ class _FlexibleSpaceBarState extends State { final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme); final EdgeInsetsGeometry padding = widget.titlePadding ?? EdgeInsetsDirectional.only( - start: effectiveCenterTitle ? 0.0 : 72.0, + start: effectiveCenterTitle && !(settings.hasLeading ?? false) ? 0.0 : 72.0, bottom: 16.0, ); final double scaleValue = Tween(begin: widget.expandedTitleScale, end: 1.0).transform(t); @@ -369,9 +371,6 @@ class FlexibleSpaceBarSettings extends InheritedWidget { /// /// Used by [Scaffold] and [SliverAppBar]. [child] must have a /// [FlexibleSpaceBar] widget in its tree for the settings to take affect. - /// - /// The required [toolbarOpacity], [minExtent], [maxExtent], [currentExtent], - /// and [child] parameters must not be null. const FlexibleSpaceBarSettings({ super.key, required this.toolbarOpacity, @@ -380,6 +379,7 @@ class FlexibleSpaceBarSettings extends InheritedWidget { required this.currentExtent, required super.child, this.isScrolledUnder, + this.hasLeading, }) : assert(minExtent >= 0), assert(maxExtent >= 0), assert(currentExtent >= 0), @@ -413,13 +413,24 @@ class FlexibleSpaceBarSettings extends InheritedWidget { /// overlaps the primary scrollable's contents. final bool? isScrolledUnder; + /// True if the FlexibleSpaceBar has a leading widget. + /// + /// This value is used by the [FlexibleSpaceBar] to determine + /// if there should be a gap between the leading widget and + /// the title. + /// + /// Null if the caller hasn't determined if the FlexibleSpaceBar + /// has a leading widget. + final bool? hasLeading; + @override bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) { return toolbarOpacity != oldWidget.toolbarOpacity || minExtent != oldWidget.minExtent || maxExtent != oldWidget.maxExtent || currentExtent != oldWidget.currentExtent - || isScrolledUnder != oldWidget.isScrolledUnder; + || isScrolledUnder != oldWidget.isScrolledUnder + || hasLeading != oldWidget.hasLeading; } } diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index e0f11dc6045c3..e692e0ab76cfa 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -67,6 +67,13 @@ enum _FloatingActionButtonType { /// ** See code in examples/api/lib/material/floating_action_button/floating_action_button.1.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This sample shows [FloatingActionButton] with additional color mappings as +/// described in: https://m3.material.io/components/floating-action-button/overview. +/// +/// ** See code in examples/api/lib/material/floating_action_button/floating_action_button.2.dart ** +/// {@end-tool} +/// /// See also: /// /// * [Scaffold], in which floating action buttons typically live. @@ -76,9 +83,8 @@ enum _FloatingActionButtonType { class FloatingActionButton extends StatelessWidget { /// Creates a circular floating action button. /// - /// The [mini] and [clipBehavior] arguments must not be null. Additionally, - /// [elevation], [highlightElevation], and [disabledElevation] (if specified) - /// must be non-negative. + /// The [elevation], [highlightElevation], and [disabledElevation] parameters, + /// if specified, must be non-negative. const FloatingActionButton({ super.key, this.child, @@ -120,10 +126,8 @@ class FloatingActionButton extends StatelessWidget { /// This constructor overrides the default size constraints of the floating /// action button. /// - /// The [clipBehavior] and [autofocus] arguments must not be null. - /// Additionally, [elevation], [focusElevation], [hoverElevation], - /// [highlightElevation], and [disabledElevation] (if specified) must be - /// non-negative. + /// The [elevation], [focusElevation], [hoverElevation], [highlightElevation], + /// and [disabledElevation] parameters, if specified, must be non-negative. const FloatingActionButton.small({ super.key, this.child, @@ -165,10 +169,8 @@ class FloatingActionButton extends StatelessWidget { /// This constructor overrides the default size constraints of the floating /// action button. /// - /// The [clipBehavior] and [autofocus] arguments must not be null. - /// Additionally, [elevation], [focusElevation], [hoverElevation], - /// [highlightElevation], and [disabledElevation] (if specified) must be - /// non-negative. + /// The [elevation], [focusElevation], [hoverElevation], [highlightElevation], + /// and [disabledElevation] parameters, if specified, must be non-negative. const FloatingActionButton.large({ super.key, this.child, @@ -208,9 +210,8 @@ class FloatingActionButton extends StatelessWidget { /// Creates a wider [StadiumBorder]-shaped floating action button with /// an optional [icon] and a [label]. /// - /// The [label], [autofocus], and [clipBehavior] arguments must not be null. - /// Additionally, [elevation], [highlightElevation], and [disabledElevation] - /// (if specified) must be non-negative. + /// The [elevation], [highlightElevation], and [disabledElevation] parameters, + /// if specified, must be non-negative. /// /// See also: /// * @@ -411,7 +412,7 @@ class FloatingActionButton extends StatelessWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// True if this is an "extended" floating action button. diff --git a/packages/flutter/lib/src/material/floating_action_button_location.dart b/packages/flutter/lib/src/material/floating_action_button_location.dart index c32fb92f166d2..a23496ce65e09 100644 --- a/packages/flutter/lib/src/material/floating_action_button_location.dart +++ b/packages/flutter/lib/src/material/floating_action_button_location.dart @@ -1004,8 +1004,8 @@ class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator { class _AnimationSwap extends CompoundAnimation { /// Creates an [_AnimationSwap]. /// - /// Both arguments must be non-null. Either can be an [_AnimationSwap] itself - /// to combine multiple animations. + /// Either argument can be an [_AnimationSwap] itself to combine multiple + /// animations. _AnimationSwap(Animation first, Animation next, this.parent, this.swapThreshold) : super(first: first, next: next); final Animation parent; diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 8fea4c9461be5..5beb239ea9d76 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -173,8 +173,6 @@ class IconButton extends StatelessWidget { /// Requires one of its ancestors to be a [Material] widget. This requirement /// no longer exists if [ThemeData.useMaterial3] is set to true. /// - /// [autofocus] argument must not be null (though it has default value). - /// /// The [icon] argument must be specified, and is typically either an [Icon] /// or an [ImageIcon]. const IconButton({ @@ -362,8 +360,6 @@ class IconButton extends StatelessWidget { /// [IconTheme] and therefore should not be explicitly given in the icon /// widget. /// - /// This property must not be null. - /// /// See [Icon], [ImageIcon]. final Widget icon; @@ -880,6 +876,12 @@ class _SelectableIconButtonState extends State<_SelectableIconButton> { ), ); } + + @override + void dispose() { + statesController.dispose(); + super.dispose(); + } } class _IconButtonM3 extends ButtonStyleButton { @@ -963,9 +965,9 @@ class _IconButtonM3 extends ButtonStyleButton { bool isIconThemeDefault(Color? color) { if (isDark) { - return color == kDefaultIconLightColor; + return identical(color, kDefaultIconLightColor); } - return color == kDefaultIconDarkColor; + return identical(color, kDefaultIconDarkColor); } final bool isDefaultColor = isIconThemeDefault(iconTheme.color); final bool isDefaultSize = iconTheme.size == const IconThemeData.fallback().size; diff --git a/packages/flutter/lib/src/material/icon_button_theme.dart b/packages/flutter/lib/src/material/icon_button_theme.dart index ee639128131cf..b598dc1075caf 100644 --- a/packages/flutter/lib/src/material/icon_button_theme.dart +++ b/packages/flutter/lib/src/material/icon_button_theme.dart @@ -89,8 +89,6 @@ class IconButtonThemeData with Diagnosticable { /// [ButtonStyle] for [IconButton]s below the overall [Theme]. class IconButtonTheme extends InheritedTheme { /// Create a [IconButtonTheme]. - /// - /// The [data] parameter must not be null. const IconButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/icons.dart b/packages/flutter/lib/src/material/icons.dart index 59f3b088499f5..5d96c9914d370 100644 --- a/packages/flutter/lib/src/material/icons.dart +++ b/packages/flutter/lib/src/material/icons.dart @@ -2154,16 +2154,16 @@ abstract final class Icons { static const IconData arrow_back_ios_outlined = IconData(0xee84, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_back_ios_new — material icon named "arrow back ios new". - static const IconData arrow_back_ios_new = IconData(0xe094, fontFamily: 'MaterialIcons'); + static const IconData arrow_back_ios_new = IconData(0xe094, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_back_ios_new — material icon named "arrow back ios new" (sharp). - static const IconData arrow_back_ios_new_sharp = IconData(0xe791, fontFamily: 'MaterialIcons'); + static const IconData arrow_back_ios_new_sharp = IconData(0xe791, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_back_ios_new — material icon named "arrow back ios new" (round). - static const IconData arrow_back_ios_new_rounded = IconData(0xf570, fontFamily: 'MaterialIcons'); + static const IconData arrow_back_ios_new_rounded = IconData(0xf570, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_back_ios_new — material icon named "arrow back ios new" (outlined). - static const IconData arrow_back_ios_new_outlined = IconData(0xee83, fontFamily: 'MaterialIcons'); + static const IconData arrow_back_ios_new_outlined = IconData(0xee83, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_circle_down — material icon named "arrow circle down". static const IconData arrow_circle_down = IconData(0xe095, fontFamily: 'MaterialIcons'); @@ -2322,16 +2322,16 @@ abstract final class Icons { static const IconData arrow_right_outlined = IconData(0xee90, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_right_alt — material icon named "arrow right alt". - static const IconData arrow_right_alt = IconData(0xe09f, fontFamily: 'MaterialIcons'); + static const IconData arrow_right_alt = IconData(0xe09f, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_right_alt — material icon named "arrow right alt" (sharp). - static const IconData arrow_right_alt_sharp = IconData(0xe79d, fontFamily: 'MaterialIcons'); + static const IconData arrow_right_alt_sharp = IconData(0xe79d, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_right_alt — material icon named "arrow right alt" (round). - static const IconData arrow_right_alt_rounded = IconData(0xf57c, fontFamily: 'MaterialIcons'); + static const IconData arrow_right_alt_rounded = IconData(0xf57c, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_right_alt — material icon named "arrow right alt" (outlined). - static const IconData arrow_right_alt_outlined = IconData(0xee8f, fontFamily: 'MaterialIcons'); + static const IconData arrow_right_alt_outlined = IconData(0xee8f, fontFamily: 'MaterialIcons', matchTextDirection: true); /// arrow_upward — material icon named "arrow upward". static const IconData arrow_upward = IconData(0xe0a0, fontFamily: 'MaterialIcons'); @@ -12900,16 +12900,16 @@ abstract final class Icons { static const IconData label_important_outline_rounded = IconData(0xf839, fontFamily: 'MaterialIcons'); /// label_off — material icon named "label off". - static const IconData label_off = IconData(0xe363, fontFamily: 'MaterialIcons'); + static const IconData label_off = IconData(0xe363, fontFamily: 'MaterialIcons', matchTextDirection: true); /// label_off — material icon named "label off" (sharp). - static const IconData label_off_sharp = IconData(0xea5c, fontFamily: 'MaterialIcons'); + static const IconData label_off_sharp = IconData(0xea5c, fontFamily: 'MaterialIcons', matchTextDirection: true); /// label_off — material icon named "label off" (round). - static const IconData label_off_rounded = IconData(0xf83b, fontFamily: 'MaterialIcons'); + static const IconData label_off_rounded = IconData(0xf83b, fontFamily: 'MaterialIcons', matchTextDirection: true); /// label_off — material icon named "label off" (outlined). - static const IconData label_off_outlined = IconData(0xf14c, fontFamily: 'MaterialIcons'); + static const IconData label_off_outlined = IconData(0xf14c, fontFamily: 'MaterialIcons', matchTextDirection: true); /// label_outline — material icon named "label outline". static const IconData label_outline = IconData(0xe364, fontFamily: 'MaterialIcons', matchTextDirection: true); @@ -13362,16 +13362,16 @@ abstract final class Icons { static const IconData list_outlined = IconData(0xf16d, fontFamily: 'MaterialIcons', matchTextDirection: true); /// list_alt — material icon named "list alt". - static const IconData list_alt = IconData(0xe385, fontFamily: 'MaterialIcons'); + static const IconData list_alt = IconData(0xe385, fontFamily: 'MaterialIcons', matchTextDirection: true); /// list_alt — material icon named "list alt" (sharp). - static const IconData list_alt_sharp = IconData(0xea7e, fontFamily: 'MaterialIcons'); + static const IconData list_alt_sharp = IconData(0xea7e, fontFamily: 'MaterialIcons', matchTextDirection: true); /// list_alt — material icon named "list alt" (round). - static const IconData list_alt_rounded = IconData(0xf85d, fontFamily: 'MaterialIcons'); + static const IconData list_alt_rounded = IconData(0xf85d, fontFamily: 'MaterialIcons', matchTextDirection: true); /// list_alt — material icon named "list alt" (outlined). - static const IconData list_alt_outlined = IconData(0xf16c, fontFamily: 'MaterialIcons'); + static const IconData list_alt_outlined = IconData(0xf16c, fontFamily: 'MaterialIcons', matchTextDirection: true); /// live_help — material icon named "live help". static const IconData live_help = IconData(0xe386, fontFamily: 'MaterialIcons', matchTextDirection: true); @@ -16224,16 +16224,16 @@ abstract final class Icons { static const IconData note_add_outlined = IconData(0xf22e, fontFamily: 'MaterialIcons'); /// note_alt — material icon named "note alt". - static const IconData note_alt = IconData(0xe44b, fontFamily: 'MaterialIcons'); + static const IconData note_alt = IconData(0xe44b, fontFamily: 'MaterialIcons', matchTextDirection: true); /// note_alt — material icon named "note alt" (sharp). - static const IconData note_alt_sharp = IconData(0xeb42, fontFamily: 'MaterialIcons'); + static const IconData note_alt_sharp = IconData(0xeb42, fontFamily: 'MaterialIcons', matchTextDirection: true); /// note_alt — material icon named "note alt" (round). - static const IconData note_alt_rounded = IconData(0xf0021, fontFamily: 'MaterialIcons'); + static const IconData note_alt_rounded = IconData(0xf0021, fontFamily: 'MaterialIcons', matchTextDirection: true); /// note_alt — material icon named "note alt" (outlined). - static const IconData note_alt_outlined = IconData(0xf22f, fontFamily: 'MaterialIcons'); + static const IconData note_alt_outlined = IconData(0xf22f, fontFamily: 'MaterialIcons', matchTextDirection: true); /// notes — material icon named "notes". static const IconData notes = IconData(0xe44c, fontFamily: 'MaterialIcons'); diff --git a/packages/flutter/lib/src/material/ink_decoration.dart b/packages/flutter/lib/src/material/ink_decoration.dart index 2492caca6959c..05434e7f63e40 100644 --- a/packages/flutter/lib/src/material/ink_decoration.dart +++ b/packages/flutter/lib/src/material/ink_decoration.dart @@ -169,9 +169,8 @@ class Ink extends StatefulWidget { /// properties of the [DecorationImage] of that [BoxDecoration] are set /// according to the arguments passed to this method. /// - /// The `image` argument must not be null. If there is no - /// intention to render anything on this image, consider using a - /// [Container] with a [BoxDecoration.image] instead. The `onImageError` + /// If there is no intention to render anything on this image, consider using + /// a [Container] with a [BoxDecoration.image] instead. The `onImageError` /// argument may be provided to listen for errors when resolving the image. /// /// The `alignment`, `repeat`, and `matchTextDirection` arguments must not diff --git a/packages/flutter/lib/src/material/ink_highlight.dart b/packages/flutter/lib/src/material/ink_highlight.dart index e294b30575977..c0d50a7940b64 100644 --- a/packages/flutter/lib/src/material/ink_highlight.dart +++ b/packages/flutter/lib/src/material/ink_highlight.dart @@ -130,7 +130,7 @@ class InkHighlight extends InteractiveInkFeature { void paintFeature(Canvas canvas, Matrix4 transform) { final Paint paint = Paint()..color = color.withAlpha(_alpha.value); final Offset? originOffset = MatrixUtils.getAsTranslation(transform); - final Rect rect = _rectCallback != null ? _rectCallback!() : Offset.zero & referenceBox.size; + final Rect rect = _rectCallback != null ? _rectCallback() : Offset.zero & referenceBox.size; if (originOffset == null) { canvas.save(); canvas.transform(transform.storage); diff --git a/packages/flutter/lib/src/material/ink_ripple.dart b/packages/flutter/lib/src/material/ink_ripple.dart index 7a574153db6cd..8f9dcde76e906 100644 --- a/packages/flutter/lib/src/material/ink_ripple.dart +++ b/packages/flutter/lib/src/material/ink_ripple.dart @@ -228,7 +228,7 @@ class InkRipple extends InteractiveInkFeature { final Paint paint = Paint()..color = color.withAlpha(alpha); Rect? rect; if (_clipCallback != null) { - rect = _clipCallback!(); + rect = _clipCallback(); } // Splash moves to the center of the reference box. final Offset center = Offset.lerp( diff --git a/packages/flutter/lib/src/material/ink_sparkle.dart b/packages/flutter/lib/src/material/ink_sparkle.dart index a46fd28cd00e2..d4d107735bc2d 100644 --- a/packages/flutter/lib/src/material/ink_sparkle.dart +++ b/packages/flutter/lib/src/material/ink_sparkle.dart @@ -285,7 +285,7 @@ class InkSparkle extends InteractiveInkFeature { if (_clipCallback != null) { _clipCanvas( canvas: canvas, - clipCallback: _clipCallback!, + clipCallback: _clipCallback, textDirection: _textDirection, customBorder: customBorder, borderRadius: _borderRadius, @@ -296,7 +296,7 @@ class InkSparkle extends InteractiveInkFeature { final Paint paint = Paint()..shader = _fragmentShader; if (_clipCallback != null) { - canvas.drawRect(_clipCallback!(), paint); + canvas.drawRect(_clipCallback(), paint); } else { canvas.drawPaint(paint); } @@ -326,50 +326,42 @@ class InkSparkle extends InteractiveInkFeature { ..setFloat(1, _color.green / 255.0) ..setFloat(2, _color.blue / 255.0) ..setFloat(3, _color.alpha / 255.0) - // uAlpha + // Composite 1 (u_alpha, u_sparkle_alpha, u_blur, u_radius_scale) ..setFloat(4, _alpha.value) - // uSparkleColor - ..setFloat(5, 1.0) + ..setFloat(5, _sparkleAlpha.value) ..setFloat(6, 1.0) - ..setFloat(7, 1.0) - ..setFloat(8, 1.0) - // uSparkleAlpha - ..setFloat(9, _sparkleAlpha.value) - // uBlur - ..setFloat(10, 1.0) + ..setFloat(7, _radiusScale.value) // uCenter - ..setFloat(11, _center.value.x) - ..setFloat(12, _center.value.y) - // uRadiusScale - ..setFloat(13, _radiusScale.value) + ..setFloat(8, _center.value.x) + ..setFloat(9, _center.value.y) // uMaxRadius - ..setFloat(14, _targetRadius) + ..setFloat(10, _targetRadius) // uResolutionScale - ..setFloat(15, 1.0 / _width) - ..setFloat(16, 1.0 / _height) + ..setFloat(11, 1.0 / _width) + ..setFloat(12, 1.0 / _height) // uNoiseScale - ..setFloat(17, _noiseDensity / _width) - ..setFloat(18, _noiseDensity / _height) + ..setFloat(13, _noiseDensity / _width) + ..setFloat(14, _noiseDensity / _height) // uNoisePhase - ..setFloat(19, noisePhase / 1000.0) + ..setFloat(15, noisePhase / 1000.0) // uCircle1 - ..setFloat(20, turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.cos(turbulenceScale * 0.55))) - ..setFloat(21, turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.sin(turbulenceScale * 0.55))) + ..setFloat(16, turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.cos(turbulenceScale * 0.55))) + ..setFloat(17, turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.sin(turbulenceScale * 0.55))) // uCircle2 - ..setFloat(22, turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.45))) - ..setFloat(23, turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.45))) + ..setFloat(18, turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.45))) + ..setFloat(19, turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.45))) // uCircle3 - ..setFloat(24, turbulenceScale + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.35))) - ..setFloat(25, turbulenceScale + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.35))) + ..setFloat(20, turbulenceScale + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.35))) + ..setFloat(21, turbulenceScale + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.35))) // uRotation1 - ..setFloat(26, math.cos(rotation1)) - ..setFloat(27, math.sin(rotation1)) + ..setFloat(22, math.cos(rotation1)) + ..setFloat(23, math.sin(rotation1)) // uRotation2 - ..setFloat(28, math.cos(rotation2)) - ..setFloat(29, math.sin(rotation2)) + ..setFloat(24, math.cos(rotation2)) + ..setFloat(25, math.sin(rotation2)) // uRotation3 - ..setFloat(30, math.cos(rotation3)) - ..setFloat(31, math.sin(rotation3)); + ..setFloat(26, math.cos(rotation3)) + ..setFloat(27, math.sin(rotation3)); } /// Transforms the canvas for an ink feature to be painted on the [canvas]. diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 157e2b31884ad..8b92f28a9d915 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:collection'; import 'package:flutter/foundation.dart'; @@ -35,8 +36,6 @@ import 'theme.dart'; /// class. abstract class InteractiveInkFeature extends InkFeature { /// Creates an InteractiveInkFeature. - /// - /// The [controller] and [referenceBox] arguments must not be null. InteractiveInkFeature({ required super.controller, required super.referenceBox, @@ -295,9 +294,6 @@ class InkResponse extends StatelessWidget { /// Creates an area of a [Material] that responds to touch. /// /// Must have an ancestor [Material] widget in which to cause ink reactions. - /// - /// The [containedInkWell], [highlightShape], [enableFeedback], - /// and [excludeFromSemantics] arguments must not be null. const InkResponse({ super.key, this.child, @@ -332,6 +328,7 @@ class InkResponse extends StatelessWidget { this.onFocusChange, this.autofocus = false, this.statesController, + this.hoverDuration, }); /// The widget below this widget in the tree. @@ -621,6 +618,11 @@ class InkResponse extends StatelessWidget { /// {@endtemplate} final MaterialStatesController? statesController; + /// The duration of the animation that animates the hover effect. + /// + /// The default is 50ms. + final Duration? hoverDuration; + @override Widget build(BuildContext context) { final _ParentInkResponseState? parentState = _ParentInkResponseProvider.maybeOf(context); @@ -659,6 +661,7 @@ class InkResponse extends StatelessWidget { getRectCallback: getRectCallback, debugCheckContext: debugCheckContext, statesController: statesController, + hoverDuration: hoverDuration, child: child, ); } @@ -715,6 +718,7 @@ class _InkResponseStateWidget extends StatefulWidget { this.getRectCallback, required this.debugCheckContext, this.statesController, + this.hoverDuration, }); final Widget? child; @@ -752,6 +756,7 @@ class _InkResponseStateWidget extends StatefulWidget { final _GetRectCallback? getRectCallback; final _CheckContext debugCheckContext; final MaterialStatesController? statesController; + final Duration? hoverDuration; @override _InkResponseState createState() => _InkResponseState(); @@ -800,8 +805,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> bool _hovering = false; final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{}; late final Map> _actionMap = >{ - ActivateIntent: CallbackAction(onInvoke: simulateTap), - ButtonActivateIntent: CallbackAction(onInvoke: simulateTap), + ActivateIntent: CallbackAction(onInvoke: activateOnIntent), + ButtonActivateIntent: CallbackAction(onInvoke: activateOnIntent), }; MaterialStatesController? internalStatesController; @@ -809,6 +814,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>(); + static const Duration _activationDuration = Duration(milliseconds: 100); + Timer? _activationTimer; + @override void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) { final bool lastAnyPressed = _anyChildInkResponsePressed; @@ -824,6 +832,25 @@ class _InkResponseState extends State<_InkResponseStateWidget> } bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty; + void activateOnIntent(Intent? intent) { + _activationTimer?.cancel(); + _activationTimer = null; + _startNewSplash(context: context); + _currentSplash?.confirm(); + _currentSplash = null; + if (widget.onTap != null) { + if (widget.enableFeedback) { + Feedback.forTap(context); + } + widget.onTap?.call(); + } + // Delay the call to `updateHighlight` to simulate a pressed delay + // and give MaterialStatesController listeners a chance to react. + _activationTimer = Timer(_activationDuration, () { + updateHighlight(_HighlightType.pressed, value: false); + }); + } + void simulateTap([Intent? intent]) { _startNewSplash(context: context); handleTap(); @@ -908,6 +935,8 @@ class _InkResponseState extends State<_InkResponseStateWidget> FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange); statesController.removeListener(handleStatesControllerChange); internalStatesController?.dispose(); + _activationTimer?.cancel(); + _activationTimer = null; super.dispose(); } @@ -920,7 +949,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> return const Duration(milliseconds: 200); case _HighlightType.hover: case _HighlightType.focus: - return const Duration(milliseconds: 50); + return widget.hoverDuration ?? const Duration(milliseconds: 50); } } @@ -1421,9 +1450,6 @@ class InkWell extends InkResponse { /// Creates an ink well. /// /// Must have an ancestor [Material] widget in which to cause ink reactions. - /// - /// The [enableFeedback], and [excludeFromSemantics] arguments - /// must not be null. const InkWell({ super.key, super.child, @@ -1456,6 +1482,7 @@ class InkWell extends InkResponse { super.onFocusChange, super.autofocus, super.statesController, + super.hoverDuration, }) : super( containedInkWell: true, highlightShape: BoxShape.rectangle, diff --git a/packages/flutter/lib/src/material/input_border.dart b/packages/flutter/lib/src/material/input_border.dart index 87ae0f6fc98de..1da20f7c5a598 100644 --- a/packages/flutter/lib/src/material/input_border.dart +++ b/packages/flutter/lib/src/material/input_border.dart @@ -31,10 +31,9 @@ import 'package:flutter/widgets.dart'; abstract class InputBorder extends ShapeBorder { /// Creates a border for an [InputDecorator]. /// - /// The [borderSide] parameter must not be null. Applications typically do - /// not specify a [borderSide] parameter because the input decorator - /// substitutes its own, using [copyWith], based on the current theme and - /// [InputDecorator.isFocused]. + /// Applications typically do not specify a [borderSide] parameter because the + /// [InputDecorator] substitutes its own, using [copyWith], based on the + /// current theme and [InputDecorator.isFocused]. const InputBorder({ this.borderSide = BorderSide.none, }); @@ -149,8 +148,7 @@ class UnderlineInputBorder extends InputBorder { /// on the current theme and [InputDecorator.isFocused]. /// /// The [borderRadius] parameter defaults to a value where the top left - /// and right corners have a circular radius of 4.0. The [borderRadius] - /// parameter must not be null. + /// and right corners have a circular radius of 4.0. const UnderlineInputBorder({ super.borderSide = const BorderSide(), this.borderRadius = const BorderRadius.only( @@ -292,10 +290,9 @@ class OutlineInputBorder extends InputBorder { /// value [BorderSide.none], the input decorator substitutes its own, using /// [copyWith], based on the current theme and [InputDecorator.isFocused]. /// - /// The [borderRadius] parameter defaults to a value where all four - /// corners have a circular radius of 4.0. The [borderRadius] parameter - /// must not be null and the corner radii must be circular, i.e. their - /// [Radius.x] and [Radius.y] values must be the same. + /// The [borderRadius] parameter defaults to a value where all four corners + /// have a circular radius of 4.0. The corner radii must be circular, i.e. + /// their [Radius.x] and [Radius.y] values must be the same. /// /// See also: /// diff --git a/packages/flutter/lib/src/material/input_chip.dart b/packages/flutter/lib/src/material/input_chip.dart index 756d557ee1bf0..13c97d4a50f4e 100644 --- a/packages/flutter/lib/src/material/input_chip.dart +++ b/packages/flutter/lib/src/material/input_chip.dart @@ -42,6 +42,16 @@ import 'theme_data.dart'; /// ** See code in examples/api/lib/material/input_chip/input_chip.0.dart ** /// {@end-tool} /// +/// +/// {@tool dartpad} +/// The following example shows how to generate [InputChip]s from +/// user text input. When the user enters a pizza topping in the text field, +/// the user is presented with a list of suggestions. When selecting one of the +/// suggestions, an [InputChip] is generated in the text field. +/// +/// ** See code in examples/api/lib/material/input_chip/input_chip.1.dart ** +/// {@end-tool} +/// /// ## Material Design 3 /// /// [InputChip] can be used for Input chips from Material Design 3. @@ -73,10 +83,8 @@ class InputChip extends StatelessWidget /// The [onPressed] and [onSelected] callbacks must not both be specified at /// the same time. /// - /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior] - /// arguments must not be null. The [pressElevation] and [elevation] must be - /// null or non-negative. Typically, [pressElevation] is greater than - /// [elevation]. + /// The [pressElevation] and [elevation] must be null or non-negative. + /// Typically, [pressElevation] is greater than [elevation]. const InputChip({ super.key, this.avatar, @@ -113,11 +121,6 @@ class InputChip extends StatelessWidget this.showCheckmark, this.checkmarkColor, this.avatarBorder = const CircleBorder(), - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - this.useDeleteButtonTooltip = true, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0); @@ -189,12 +192,6 @@ class InputChip extends StatelessWidget final ShapeBorder avatarBorder; @override final IconThemeData? iconTheme; - @override - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - final bool useDeleteButtonTooltip; @override Widget build(BuildContext context) { @@ -213,7 +210,6 @@ class InputChip extends StatelessWidget deleteIcon: resolvedDeleteIcon, onDeleted: onDeleted, deleteIconColor: deleteIconColor, - useDeleteButtonTooltip: useDeleteButtonTooltip, deleteButtonTooltipMessage: deleteButtonTooltipMessage, onSelected: onSelected, onPressed: onPressed, @@ -321,7 +317,7 @@ class _InputChipDefaultsM3 extends ChipThemeData { EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 4.0), - clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), + clampDouble(MediaQuery.textScalerOf(context).textScaleFactor - 1.0, 0.0, 1.0), )!; } diff --git a/packages/flutter/lib/src/material/input_date_picker_form_field.dart b/packages/flutter/lib/src/material/input_date_picker_form_field.dart index 4a82273b78248..a7600e4fc970d 100644 --- a/packages/flutter/lib/src/material/input_date_picker_form_field.dart +++ b/packages/flutter/lib/src/material/input_date_picker_form_field.dart @@ -42,9 +42,6 @@ class InputDatePickerFormField extends StatefulWidget { /// for [initialDate]. /// /// [firstDate] must be on or before [lastDate]. - /// - /// [firstDate], [lastDate], and [autofocus] must be non-null. - /// InputDatePickerFormField({ super.key, DateTime? initialDate, diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index d957c7aaf2948..80091f426444f 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -30,6 +30,13 @@ const Duration _kTransitionDuration = Duration(milliseconds: 167); const Curve _kTransitionCurve = Curves.fastOutSlowIn; const double _kFinalLabelScale = 0.75; +// The default duration for hint fade in/out transitions. +// +// Animating hint is not mentioned in the Material specification. +// The animation is kept for backard compatibility and a short duration +// is used to mitigate the UX impact. +const Duration _kHintFadeTransitionDuration = Duration(milliseconds: 20); + // Defines the gap in the InputDecorator's outline border where the // floating label will appear. class _InputBorderGap extends ChangeNotifier { @@ -260,7 +267,7 @@ class _BorderContainerState extends State<_BorderContainer> with TickerProviderS } } -// Used to "shake" the floating label to the left to the left and right +// Used to "shake" the floating label to the left and right // when the errorText first appears. class _Shaker extends AnimatedWidget { const _Shaker({ @@ -964,7 +971,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, containerConstraints); boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, containerConstraints); final BoxConstraints contentConstraints = containerConstraints.copyWith( - maxWidth: containerConstraints.maxWidth - contentPadding.horizontal, + maxWidth: math.max(0.0, containerConstraints.maxWidth - contentPadding.horizontal), ); boxToBaseline[prefix] = _layoutLineBox(prefix, contentConstraints); boxToBaseline[suffix] = _layoutLineBox(suffix, contentConstraints); @@ -1093,7 +1100,7 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin final double minContainerHeight = decoration.isDense! || decoration.isCollapsed || expands ? 0.0 : kMinInteractiveDimension; - final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight; + final double maxContainerHeight = math.max(0.0, boxConstraints.maxHeight - bottomHeight); final double containerHeight = expands ? maxContainerHeight : math.min(math.max(contentHeight, minContainerHeight), maxContainerHeight); @@ -1797,8 +1804,6 @@ class InputDecorator extends StatefulWidget { /// /// Null [InputDecoration] properties are initialized with the corresponding /// values from [ThemeData.inputDecorationTheme]. - /// - /// Must not be null. final InputDecoration decoration; /// The style on which to base the label, hint, counter, and error styles @@ -1956,6 +1961,7 @@ class _InputDecoratorState extends State with TickerProviderStat void dispose() { _floatingLabelController.dispose(); _shakingLabelController.dispose(); + _borderGap.dispose(); super.dispose(); } @@ -1970,6 +1976,7 @@ class _InputDecoratorState extends State with TickerProviderStat TextAlign? get textAlign => widget.textAlign; bool get isFocused => widget.isFocused; + bool get _hasError => decoration.errorText != null || decoration.error != null; bool get isHovering => widget.isHovering && decoration.enabled; bool get isEmpty => widget.isEmpty; bool get _floatingLabelEnabled { @@ -2010,7 +2017,7 @@ class _InputDecoratorState extends State with TickerProviderStat ? Colors.transparent : themeData.disabledColor; } - if (decoration.errorText != null) { + if (_hasError) { return themeData.colorScheme.error; } if (isFocused) { @@ -2106,7 +2113,7 @@ class _InputDecoratorState extends State with TickerProviderStat TextStyle _getFloatingLabelStyle(ThemeData themeData, InputDecorationTheme defaults) { TextStyle defaultTextStyle = MaterialStateProperty.resolveAs(defaults.floatingLabelStyle!, materialState); - if (decoration.errorText != null && decoration.errorStyle?.color != null) { + if (_hasError && decoration.errorStyle?.color != null) { defaultTextStyle = defaultTextStyle.copyWith(color: decoration.errorStyle?.color); } defaultTextStyle = defaultTextStyle.merge(decoration.floatingLabelStyle ?? decoration.labelStyle); @@ -2136,7 +2143,7 @@ class _InputDecoratorState extends State with TickerProviderStat if (!decoration.enabled) MaterialState.disabled, if (isFocused) MaterialState.focused, if (isHovering) MaterialState.hovered, - if (decoration.errorText != null) MaterialState.error, + if (_hasError) MaterialState.error, }; } @@ -2168,7 +2175,10 @@ class _InputDecoratorState extends State with TickerProviderStat return border.copyWith( borderSide: BorderSide( color: _getDefaultM2BorderColor(themeData), - width: (decoration.isCollapsed || decoration.border == InputBorder.none || !decoration.enabled) + width: ( + (decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed) + || decoration.border == InputBorder.none + || !decoration.enabled) ? 0.0 : isFocused ? 2.0 : 1.0, ), @@ -2189,7 +2199,7 @@ class _InputDecoratorState extends State with TickerProviderStat final String? hintText = decoration.hintText; final Widget? hint = hintText == null ? null : AnimatedOpacity( opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0, - duration: _kTransitionDuration, + duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration, curve: _kTransitionCurve, child: Text( hintText, @@ -2201,14 +2211,13 @@ class _InputDecoratorState extends State with TickerProviderStat ), ); - final bool isError = decoration.errorText != null; InputBorder? border; if (!decoration.enabled) { - border = isError ? decoration.errorBorder : decoration.disabledBorder; + border = _hasError ? decoration.errorBorder : decoration.disabledBorder; } else if (isFocused) { - border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder; + border = _hasError ? decoration.focusedErrorBorder : decoration.focusedBorder; } else { - border = isError ? decoration.errorBorder : decoration.enabledBorder; + border = _hasError ? decoration.errorBorder : decoration.enabledBorder; } border ??= _getDefaultBorder(themeData, defaults); @@ -2401,12 +2410,13 @@ class _InputDecoratorState extends State with TickerProviderStat final EdgeInsets contentPadding; final double floatingLabelHeight; - if (decoration.isCollapsed) { + if (decoration.isCollapsed + ?? themeData.inputDecorationTheme.isCollapsed) { floatingLabelHeight = 0.0; contentPadding = decorationContentPadding ?? EdgeInsets.zero; } else if (!border.isOutline) { // 4.0: the vertical gap between the inline elements and the floating label. - floatingLabelHeight = (4.0 + 0.75 * labelStyle.fontSize!) * MediaQuery.textScaleFactorOf(context); + floatingLabelHeight = (4.0 + 0.75 * labelStyle.fontSize!) * MediaQuery.textScalerOf(context).textScaleFactor; if (decoration.filled ?? false) { contentPadding = decorationContentPadding ?? (decorationIsDense ? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0) @@ -2429,7 +2439,7 @@ class _InputDecoratorState extends State with TickerProviderStat final _Decorator decorator = _Decorator( decoration: _Decoration( contentPadding: contentPadding, - isCollapsed: decoration.isCollapsed, + isCollapsed: decoration.isCollapsed ?? themeData.inputDecorationTheme.isCollapsed, floatingLabelHeight: floatingLabelHeight, floatingLabelAlignment: decoration.floatingLabelAlignment!, floatingLabelProgress: _floatingLabelAnimation.value, @@ -2551,8 +2561,6 @@ class InputDecoration { /// an instance of [UnderlineInputBorder]. If [border] is [InputBorder.none] /// then no border is drawn. /// - /// The [enabled] argument must not be null. - /// /// Only one of [prefix] and [prefixText] can be specified. /// /// Similarly, only one of [suffix] and [suffixText] can be specified. @@ -2570,13 +2578,14 @@ class InputDecoration { this.hintStyle, this.hintTextDirection, this.hintMaxLines, + this.hintFadeDuration, this.error, this.errorText, this.errorStyle, this.errorMaxLines, this.floatingLabelBehavior, this.floatingLabelAlignment, - this.isCollapsed = false, + this.isCollapsed, this.isDense, this.contentPadding, this.prefixIcon, @@ -2640,6 +2649,7 @@ class InputDecoration { helperStyle = null, helperMaxLines = null, hintMaxLines = null, + hintFadeDuration = null, error = null, errorText = null, errorStyle = null, @@ -2853,6 +2863,12 @@ class InputDecoration { /// used to handle the overflow when it is limited to single line. final int? hintMaxLines; + /// The duration of the [hintText] fade in and fade out animations. + /// + /// If null, defaults to [InputDecorationTheme.hintFadeDuration]. + /// If [InputDecorationTheme.hintFadeDuration] is null defaults to 20ms. + final Duration? hintFadeDuration; + /// Optional widget that appears below the [InputDecorator.child] and the border. /// /// If non-null, the border's color animates to red and the [helperText] is not shown. @@ -2887,7 +2903,6 @@ class InputDecoration { /// {@endtemplate} final TextStyle? errorStyle; - /// The maximum number of lines the [errorText] can occupy. /// /// Defaults to null, which means that the [errorText] will be limited @@ -2975,7 +2990,7 @@ class InputDecoration { /// A collapsed decoration cannot have [labelText], [errorText], an [icon]. /// /// To create a collapsed input decoration, use [InputDecoration.collapsed]. - final bool isCollapsed; + final bool? isCollapsed; /// An icon that appears before the [prefix] or [prefixText] and before /// the editable part of the text field, within the decoration's container. @@ -3506,6 +3521,7 @@ class InputDecoration { String? hintText, TextStyle? hintStyle, TextDirection? hintTextDirection, + Duration? hintFadeDuration, int? hintMaxLines, Widget? error, String? errorText, @@ -3560,6 +3576,7 @@ class InputDecoration { hintStyle: hintStyle ?? this.hintStyle, hintTextDirection: hintTextDirection ?? this.hintTextDirection, hintMaxLines: hintMaxLines ?? this.hintMaxLines, + hintFadeDuration: hintFadeDuration ?? this.hintFadeDuration, error: error ?? this.error, errorText: errorText ?? this.errorText, errorStyle: errorStyle ?? this.errorStyle, @@ -3613,13 +3630,14 @@ class InputDecoration { helperStyle: helperStyle ?? theme.helperStyle, helperMaxLines : helperMaxLines ?? theme.helperMaxLines, hintStyle: hintStyle ?? theme.hintStyle, + hintFadeDuration: hintFadeDuration ?? theme.hintFadeDuration, errorStyle: errorStyle ?? theme.errorStyle, errorMaxLines: errorMaxLines ?? theme.errorMaxLines, floatingLabelBehavior: floatingLabelBehavior ?? theme.floatingLabelBehavior, floatingLabelAlignment: floatingLabelAlignment ?? theme.floatingLabelAlignment, isDense: isDense ?? theme.isDense, contentPadding: contentPadding ?? theme.contentPadding, - isCollapsed: isCollapsed, + isCollapsed: isCollapsed ?? theme.isCollapsed, iconColor: iconColor ?? theme.iconColor, prefixStyle: prefixStyle ?? theme.prefixStyle, prefixIconColor: prefixIconColor ?? theme.prefixIconColor, @@ -3663,6 +3681,7 @@ class InputDecoration { && other.hintStyle == hintStyle && other.hintTextDirection == hintTextDirection && other.hintMaxLines == hintMaxLines + && other.hintFadeDuration == hintFadeDuration && other.error == error && other.errorText == errorText && other.errorStyle == errorStyle @@ -3719,6 +3738,7 @@ class InputDecoration { hintStyle, hintTextDirection, hintMaxLines, + hintFadeDuration, error, errorText, errorStyle, @@ -3773,6 +3793,7 @@ class InputDecoration { if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"', if (hintText != null) 'hintText: "$hintText"', if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"', + if (hintFadeDuration != null) 'hintFadeDuration: "$hintFadeDuration"', if (error != null) 'error: "$error"', if (errorText != null) 'errorText: "$errorText"', if (errorStyle != null) 'errorStyle: "$errorStyle"', @@ -3781,7 +3802,7 @@ class InputDecoration { if (floatingLabelAlignment != null) 'floatingLabelAlignment: $floatingLabelAlignment', if (isDense ?? false) 'isDense: $isDense', if (contentPadding != null) 'contentPadding: $contentPadding', - if (isCollapsed) 'isCollapsed: $isCollapsed', + if (isCollapsed ?? false) 'isCollapsed: $isCollapsed', if (prefixIcon != null) 'prefixIcon: $prefixIcon', if (prefixIconColor != null) 'prefixIconColor: $prefixIconColor', if (prefix != null) 'prefix: $prefix', @@ -3829,15 +3850,13 @@ class InputDecoration { class InputDecorationTheme with Diagnosticable { /// Creates a value for [ThemeData.inputDecorationTheme] that /// defines default values for [InputDecorator]. - /// - /// The values of [isDense], [isCollapsed], [filled], [floatingLabelAlignment], - /// and [border] must not be null. const InputDecorationTheme({ this.labelStyle, this.floatingLabelStyle, this.helperStyle, this.helperMaxLines, this.hintStyle, + this.hintFadeDuration, this.errorStyle, this.errorMaxLines, this.floatingLabelBehavior = FloatingLabelBehavior.auto, @@ -3908,6 +3927,9 @@ class InputDecorationTheme with Diagnosticable { /// input field and the current [Theme]. final TextStyle? hintStyle; + /// The duration of the [InputDecoration.hintText] fade in and fade out animations. + final Duration? hintFadeDuration; + /// {@macro flutter.material.inputDecoration.errorStyle} final TextStyle? errorStyle; @@ -4245,6 +4267,7 @@ class InputDecorationTheme with Diagnosticable { TextStyle? helperStyle, int? helperMaxLines, TextStyle? hintStyle, + Duration? hintFadeDuration, TextStyle? errorStyle, int? errorMaxLines, FloatingLabelBehavior? floatingLabelBehavior, @@ -4279,6 +4302,7 @@ class InputDecorationTheme with Diagnosticable { helperStyle: helperStyle ?? this.helperStyle, helperMaxLines: helperMaxLines ?? this.helperMaxLines, hintStyle: hintStyle ?? this.hintStyle, + hintFadeDuration: hintFadeDuration ?? this.hintFadeDuration, errorStyle: errorStyle ?? this.errorStyle, errorMaxLines: errorMaxLines ?? this.errorMaxLines, floatingLabelBehavior: floatingLabelBehavior ?? this.floatingLabelBehavior, @@ -4328,6 +4352,7 @@ class InputDecorationTheme with Diagnosticable { helperStyle: helperStyle ?? inputDecorationTheme.helperStyle, helperMaxLines: helperMaxLines ?? inputDecorationTheme.helperMaxLines, hintStyle: hintStyle ?? inputDecorationTheme.hintStyle, + hintFadeDuration: hintFadeDuration ?? inputDecorationTheme.hintFadeDuration, errorStyle: errorStyle ?? inputDecorationTheme.errorStyle, errorMaxLines: errorMaxLines ?? inputDecorationTheme.errorMaxLines, contentPadding: contentPadding ?? inputDecorationTheme.contentPadding, @@ -4387,6 +4412,7 @@ class InputDecorationTheme with Diagnosticable { border, alignLabelWithHint, constraints, + hintFadeDuration, ), ); @@ -4404,6 +4430,7 @@ class InputDecorationTheme with Diagnosticable { && other.helperStyle == helperStyle && other.helperMaxLines == helperMaxLines && other.hintStyle == hintStyle + && other.hintFadeDuration == hintFadeDuration && other.errorStyle == errorStyle && other.errorMaxLines == errorMaxLines && other.isDense == isDense @@ -4443,6 +4470,7 @@ class InputDecorationTheme with Diagnosticable { properties.add(DiagnosticsProperty('helperStyle', helperStyle, defaultValue: defaultTheme.helperStyle)); properties.add(IntProperty('helperMaxLines', helperMaxLines, defaultValue: defaultTheme.helperMaxLines)); properties.add(DiagnosticsProperty('hintStyle', hintStyle, defaultValue: defaultTheme.hintStyle)); + properties.add(DiagnosticsProperty('hintFadeDuration', hintFadeDuration, defaultValue: defaultTheme.hintFadeDuration)); properties.add(DiagnosticsProperty('errorStyle', errorStyle, defaultValue: defaultTheme.errorStyle)); properties.add(IntProperty('errorMaxLines', errorMaxLines, defaultValue: defaultTheme.errorMaxLines)); properties.add(DiagnosticsProperty('floatingLabelBehavior', floatingLabelBehavior, defaultValue: defaultTheme.floatingLabelBehavior)); diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 074862beb3c68..741787207d5de 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -801,8 +801,7 @@ class ListTile extends StatelessWidget { subtitleStyle = subtitleTextStyle ?? tileTheme.subtitleTextStyle ?? defaults.subtitleTextStyle!; - final Color? subtitleColor = effectiveColor - ?? (theme.useMaterial3 ? null : theme.textTheme.bodySmall!.color); + final Color? subtitleColor = effectiveColor; subtitleStyle = subtitleStyle.copyWith( color: subtitleColor, fontSize: _isDenseLayout(theme, tileTheme) ? 12.0 : null, @@ -1533,7 +1532,8 @@ class _LisTileDefaultsM2 extends ListTileThemeData { } @override - TextStyle? get subtitleTextStyle => _textTheme.bodyMedium; + TextStyle? get subtitleTextStyle => _textTheme.bodyMedium! + .copyWith(color: _textTheme.bodySmall!.color); @override TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium; diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart index f3dc20ee979cc..3b77ffd97997e 100644 --- a/packages/flutter/lib/src/material/list_tile_theme.dart +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -378,79 +378,79 @@ class ListTileTheme extends InheritedTheme { /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.dense] property instead. - bool? get dense => _data != null ? _data!.dense : _dense; + bool? get dense => _data != null ? _data.dense : _dense; /// Overrides the default value of [ListTile.shape]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.shape] property instead. - ShapeBorder? get shape => _data != null ? _data!.shape : _shape; + ShapeBorder? get shape => _data != null ? _data.shape : _shape; /// Overrides the default value of [ListTile.style]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.style] property instead. - ListTileStyle? get style => _data != null ? _data!.style : _style; + ListTileStyle? get style => _data != null ? _data.style : _style; /// Overrides the default value of [ListTile.selectedColor]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.selectedColor] property instead. - Color? get selectedColor => _data != null ? _data!.selectedColor : _selectedColor; + Color? get selectedColor => _data != null ? _data.selectedColor : _selectedColor; /// Overrides the default value of [ListTile.iconColor]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.iconColor] property instead. - Color? get iconColor => _data != null ? _data!.iconColor : _iconColor; + Color? get iconColor => _data != null ? _data.iconColor : _iconColor; /// Overrides the default value of [ListTile.textColor]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.textColor] property instead. - Color? get textColor => _data != null ? _data!.textColor : _textColor; + Color? get textColor => _data != null ? _data.textColor : _textColor; /// Overrides the default value of [ListTile.contentPadding]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.contentPadding] property instead. - EdgeInsetsGeometry? get contentPadding => _data != null ? _data!.contentPadding : _contentPadding; + EdgeInsetsGeometry? get contentPadding => _data != null ? _data.contentPadding : _contentPadding; /// Overrides the default value of [ListTile.tileColor]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.tileColor] property instead. - Color? get tileColor => _data != null ? _data!.tileColor : _tileColor; + Color? get tileColor => _data != null ? _data.tileColor : _tileColor; /// Overrides the default value of [ListTile.selectedTileColor]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.selectedTileColor] property instead. - Color? get selectedTileColor => _data != null ? _data!.selectedTileColor : _selectedTileColor; + Color? get selectedTileColor => _data != null ? _data.selectedTileColor : _selectedTileColor; /// Overrides the default value of [ListTile.horizontalTitleGap]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.horizontalTitleGap] property instead. - double? get horizontalTitleGap => _data != null ? _data!.horizontalTitleGap : _horizontalTitleGap; + double? get horizontalTitleGap => _data != null ? _data.horizontalTitleGap : _horizontalTitleGap; /// Overrides the default value of [ListTile.minVerticalPadding]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.minVerticalPadding] property instead. - double? get minVerticalPadding => _data != null ? _data!.minVerticalPadding : _minVerticalPadding; + double? get minVerticalPadding => _data != null ? _data.minVerticalPadding : _minVerticalPadding; /// Overrides the default value of [ListTile.minLeadingWidth]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.minLeadingWidth] property instead. - double? get minLeadingWidth => _data != null ? _data!.minLeadingWidth : _minLeadingWidth; + double? get minLeadingWidth => _data != null ? _data.minLeadingWidth : _minLeadingWidth; /// Overrides the default value of [ListTile.enableFeedback]. /// /// This property is obsolete: please use the [data] /// [ListTileThemeData.enableFeedback] property instead. - bool? get enableFeedback => _data != null ? _data!.enableFeedback : _enableFeedback; + bool? get enableFeedback => _data != null ? _data.enableFeedback : _enableFeedback; /// The [data] property of the closest instance of this class that /// encloses the given context. @@ -470,8 +470,6 @@ class ListTileTheme extends InheritedTheme { /// Creates a list tile theme that controls the color and style parameters for /// [ListTile]s, and merges in the current list tile theme, if any. - /// - /// The [child] argument must not be null. static Widget merge({ Key? key, bool? dense, diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index e7d76a439e072..f5ebf2d7a4a34 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -171,9 +171,7 @@ abstract class MaterialInkController { class Material extends StatefulWidget { /// Creates a piece of material. /// - /// The [type], [elevation], [borderOnForeground], - /// [clipBehavior], and [animationDuration] arguments must not be null. - /// Additionally, [elevation] must be non-negative. + /// The [elevation] must be non-negative. /// /// If a [shape] is specified, then the [borderRadius] property must be /// null and the [type] property must not be [MaterialType.circle]. If the @@ -324,7 +322,7 @@ class Material extends StatefulWidget { /// use cases. /// {@endtemplate} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// Defines the duration of animated changes for [shape], [elevation], @@ -841,9 +839,7 @@ class ShapeBorderTween extends Tween { class _MaterialInterior extends ImplicitlyAnimatedWidget { /// Creates a const instance of [_MaterialInterior]. /// - /// The [child], [shape], [clipBehavior], [color], and [shadowColor] arguments - /// must not be null. The [elevation] must be specified and greater than or - /// equal to zero. + /// The [elevation] must be specified and greater than or equal to zero. const _MaterialInterior({ required this.child, required this.shape, @@ -876,7 +872,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// The target z-coordinate at which to place this physical object relative diff --git a/packages/flutter/lib/src/material/material_button.dart b/packages/flutter/lib/src/material/material_button.dart index 365f8c5669188..4afcc231f9e8e 100644 --- a/packages/flutter/lib/src/material/material_button.dart +++ b/packages/flutter/lib/src/material/material_button.dart @@ -45,10 +45,8 @@ class MaterialButton extends StatelessWidget { /// To create a custom Material button consider using [TextButton], /// [ElevatedButton], or [OutlinedButton]. /// - /// The [autofocus] and [clipBehavior] arguments must not be null. - /// Additionally, [elevation], [hoverElevation], [focusElevation], - /// [highlightElevation], and [disabledElevation] must be non-negative, if - /// specified. + /// The [elevation], [hoverElevation], [focusElevation], [highlightElevation], + /// and [disabledElevation] arguments must be non-negative, if specified. const MaterialButton({ super.key, required this.onPressed, @@ -338,7 +336,7 @@ class MaterialButton extends StatelessWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; /// {@macro flutter.widgets.Focus.focusNode} @@ -440,17 +438,3 @@ class MaterialButton extends StatelessWidget { properties.add(DiagnosticsProperty('materialTapTargetSize', materialTapTargetSize, defaultValue: null)); } } - -/// The distinguished type of [MaterialButton]. -/// -/// This class is deprecated and will be removed in a future release. -/// -/// This mixin only exists to give the "label and icon" button widgets a distinct -/// type for the sake of [ButtonTheme]. -@Deprecated( - 'This was used to differentiate types of FlatButton, RaisedButton, and OutlineButton in ButtonTheme. ' - 'These buttons have been replaced with TextButton, ElevatedButton, and OutlinedButton, each of which have their own respective themes now. ' - 'Use one of these button classes instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.', -) -mixin MaterialButtonWithIconMixin { } diff --git a/packages/flutter/lib/src/material/material_localizations.dart b/packages/flutter/lib/src/material/material_localizations.dart index 489c23a2ca143..8cfd4b046a677 100644 --- a/packages/flutter/lib/src/material/material_localizations.dart +++ b/packages/flutter/lib/src/material/material_localizations.dart @@ -115,6 +115,15 @@ abstract class MaterialLocalizations { /// Label for "select all" edit buttons and menu items. String get selectAllButtonLabel; + /// Label for "look up" edit buttons and menu items. + String get lookUpButtonLabel; + + /// Label for "search web" edit buttons and menu items. + String get searchWebButtonLabel; + + /// Label for "share" edit buttons and menu items. + String get shareButtonLabel; + /// Label for the [AboutDialog] button that shows the [LicensePage]. String get viewLicensesButtonLabel; @@ -139,6 +148,10 @@ abstract class MaterialLocalizations { /// user interaction with elements behind it. String get modalBarrierDismissLabel; + /// Label read out by accessibility tools (TalkBack or VoiceOver) for a + /// context menu to indicate that a tap dismisses the context menu. + String get menuDismissLabel; + /// Label read out by accessibility tools (TalkBack or VoiceOver) when a /// drawer widget is opened. String get drawerLabel; @@ -166,10 +179,11 @@ abstract class MaterialLocalizations { /// Label indicating that a given date is the current date. String get currentDateLabel; - /// Label for the scrim rendered underneath the content of a modal route. + /// Label for the scrim rendered underneath a [BottomSheet]. String get scrimLabel; - /// Label for a BottomSheet. + /// Label for a [BottomSheet], used as the `modalRouteContentName` of the + /// [scrimOnTapHint]. String get bottomSheetLabel; /// Hint text announced when tapping on the scrim underneath the content of @@ -1174,6 +1188,15 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { @override String get selectAllButtonLabel => 'Select all'; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get searchWebButtonLabel => 'Search Web'; + + @override + String get shareButtonLabel => 'Share...'; + @override String get viewLicensesButtonLabel => 'View licenses'; @@ -1192,6 +1215,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { @override String get modalBarrierDismissLabel => 'Dismiss'; + @override + String get menuDismissLabel => 'Dismiss menu'; + @override ScriptCategory get scriptCategory => ScriptCategory.englishLike; diff --git a/packages/flutter/lib/src/material/material_state.dart b/packages/flutter/lib/src/material/material_state.dart index 6fad0799ffad2..59d5dfcb71ee9 100644 --- a/packages/flutter/lib/src/material/material_state.dart +++ b/packages/flutter/lib/src/material/material_state.dart @@ -735,11 +735,21 @@ class MaterialStatePropertyAll implements MaterialStateProperty { /// /// Used by widgets that expose their internal state for the sake of /// extensions that add support for additional states. See -/// [TextButton.statesController] for example. +/// [TextButton] for an example. /// /// The controller's [value] is its current set of states. Listeners /// are notified whenever the [value] changes. The [value] should only be /// changed with [update]; it should not be modified directly. +/// +/// The controller's [value] represents the set of states that a +/// widget's visual properties, typically [MaterialStateProperty] +/// values, are resolved against. It is _not_ the intrinsic state of +/// the widget. The widget is responsible for ensuring that the +/// controller's [value] tracks its intrinsic state. For example one +/// cannot request the keyboard focus for a widget by adding +/// [MaterialState.focused] to its controller. When the widget gains the +/// or loses the focus it will [update] its controller's [value] and +/// notify listeners of the change. class MaterialStatesController extends ValueNotifier> { /// Creates a MaterialStatesController. MaterialStatesController([Set? value]) : super({...?value}); diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart index 7980783745f30..3b231b234028e 100644 --- a/packages/flutter/lib/src/material/menu_anchor.dart +++ b/packages/flutter/lib/src/material/menu_anchor.dart @@ -27,6 +27,7 @@ import 'menu_style.dart'; import 'menu_theme.dart'; import 'radio.dart'; import 'text_button.dart'; +import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -303,6 +304,7 @@ class _MenuAnchorState extends State { _anchorChildren.clear(); _menuController._detach(this); _internalMenuController = null; + _menuScopeNode.dispose(); super.dispose(); } @@ -551,6 +553,7 @@ class _MenuAnchorState extends State { } _closeChildren(inDispose: inDispose); _overlayEntry?.remove(); + _overlayEntry?.dispose(); _overlayEntry = null; if (!inDispose) { // Notify that _childIsOpen changed state, but only if not @@ -1072,14 +1075,14 @@ class _MenuItemButtonState extends State { ), ); - if (_platformSupportsAccelerators() && widget.enabled) { + if (_platformSupportsAccelerators && widget.enabled) { child = MenuAcceleratorCallbackBinding( onInvoke: _handleSelect, child: child, ); } - return child; + return MergeSemantics(child: child); } void _handleFocusChange() { @@ -1099,10 +1102,16 @@ class _MenuItemButtonState extends State { void _handleSelect() { assert(_debugMenuInfo('Selected ${widget.child} menu')); - widget.onPressed?.call(); if (widget.closeOnActivate) { _MenuAnchorState._maybeOf(context)?._root._close(); } + // Delay the call to onPressed until post-frame so that the focus is + // restored to what it was before the menu was opened before the action is + // executed. + SchedulerBinding.instance.addPostFrameCallback((Duration _) { + FocusManager.instance.applyFocusChangesIfNeeded(); + widget.onPressed?.call(); + }); } void _createInternalFocusNodeIfNeeded() { @@ -1180,7 +1189,7 @@ class CheckboxMenuButton extends StatelessWidget { /// The checkbox will have different default container color and check color when /// this is true. This is only used when [ThemeData.useMaterial3] is set to true. /// - /// Must not be null. Defaults to false. + /// Defaults to false. final bool isError; /// Called when the value of the checkbox should change. @@ -1898,23 +1907,27 @@ class _SubmenuButtonState extends State { controller._anchor!._focusButton(); } } - - child = TextButton( - style: mergedStyle, - focusNode: _buttonFocusNode, - onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null, - onPressed: _enabled ? () => toggleShowMenu(context) : null, - isSemanticButton: null, - child: _MenuItemLabel( - leadingIcon: widget.leadingIcon, - trailingIcon: widget.trailingIcon, - hasSubmenu: true, - showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical, - child: child ?? const SizedBox(), + child = MergeSemantics( + child: Semantics( + expanded: controller.isOpen, + child: TextButton( + style: mergedStyle, + focusNode: _buttonFocusNode, + onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null, + onPressed: _enabled ? () => toggleShowMenu(context) : null, + isSemanticButton: null, + child: _MenuItemLabel( + leadingIcon: widget.leadingIcon, + trailingIcon: widget.trailingIcon, + hasSubmenu: true, + showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical, + child: child ?? const SizedBox(), + ), + ), ), ); - if (_enabled && _platformSupportsAccelerators()) { + if (_enabled && _platformSupportsAccelerators) { return MenuAcceleratorCallbackBinding( onInvoke: () => toggleShowMenu(context), hasSubmenu: true, @@ -2043,33 +2056,44 @@ class _LocalizedShortcutLabeler { String getShortcutLabel(MenuSerializableShortcut shortcut, MaterialLocalizations localizations) { final ShortcutSerialization serialized = shortcut.serializeForMenu(); final String keySeparator; - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: + if (_usesSymbolicModifiers) { // Use "⌃ ⇧ A" style on macOS and iOS. keySeparator = ' '; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: + } else { // Use "Ctrl+Shift+A" style. keySeparator = '+'; } if (serialized.trigger != null) { final List modifiers = []; final LogicalKeyboardKey trigger = serialized.trigger!; - // These should be in this order, to match the LogicalKeySet version. - if (serialized.alt!) { - modifiers.add(_getModifierLabel(LogicalKeyboardKey.alt, localizations)); - } - if (serialized.control!) { - modifiers.add(_getModifierLabel(LogicalKeyboardKey.control, localizations)); - } - if (serialized.meta!) { - modifiers.add(_getModifierLabel(LogicalKeyboardKey.meta, localizations)); - } - if (serialized.shift!) { - modifiers.add(_getModifierLabel(LogicalKeyboardKey.shift, localizations)); + if (_usesSymbolicModifiers) { + // macOS/iOS platform convention uses this ordering, with ⌘ always last. + if (serialized.control!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.control, localizations)); + } + if (serialized.alt!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.alt, localizations)); + } + if (serialized.shift!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.shift, localizations)); + } + if (serialized.meta!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.meta, localizations)); + } + } else { + // These should be in this order, to match the LogicalKeySet version. + if (serialized.alt!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.alt, localizations)); + } + if (serialized.control!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.control, localizations)); + } + if (serialized.meta!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.meta, localizations)); + } + if (serialized.shift!) { + modifiers.add(_getModifierLabel(LogicalKeyboardKey.shift, localizations)); + } } String? shortcutTrigger; final int logicalKeyId = trigger.keyId; @@ -2842,7 +2866,7 @@ class _MenuAcceleratorLabelState extends State { @override void initState() { super.initState(); - if (_platformSupportsAccelerators()) { + if (_platformSupportsAccelerators) { _showAccelerators = _altIsPressed(); HardwareKeyboard.instance.addHandler(_handleKeyEvent); } @@ -2851,9 +2875,9 @@ class _MenuAcceleratorLabelState extends State { @override void dispose() { - assert(_platformSupportsAccelerators() || _shortcutRegistryEntry == null); + assert(_platformSupportsAccelerators || _shortcutRegistryEntry == null); _displayLabel = ''; - if (_platformSupportsAccelerators()) { + if (_platformSupportsAccelerators) { _shortcutRegistryEntry?.dispose(); _shortcutRegistryEntry = null; _shortcutRegistry = null; @@ -2866,7 +2890,7 @@ class _MenuAcceleratorLabelState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - if (!_platformSupportsAccelerators()) { + if (!_platformSupportsAccelerators) { return; } _binding = MenuAcceleratorCallbackBinding.maybeOf(context); @@ -2894,7 +2918,7 @@ class _MenuAcceleratorLabelState extends State { } bool _handleKeyEvent(KeyEvent event) { - assert(_platformSupportsAccelerators()); + assert(_platformSupportsAccelerators); final bool altIsPressed = _altIsPressed(); if (altIsPressed != _showAccelerators) { setState(() { @@ -2907,7 +2931,7 @@ class _MenuAcceleratorLabelState extends State { } void _updateAcceleratorShortcut() { - assert(_platformSupportsAccelerators()); + assert(_platformSupportsAccelerators); _shortcutRegistryEntry?.dispose(); _shortcutRegistryEntry = null; // Before registering an accelerator as a shortcut it should meet these @@ -3190,7 +3214,7 @@ class _MenuLayout extends SingleChildLayoutDelegate { } else if (offBottom(y)) { final double newY = anchorRect.top - childSize.height; if (!offTop(newY)) { - // Only move the menu up if its parent is horizontal (MenuAchor/MenuBar). + // Only move the menu up if its parent is horizontal (MenuAnchor/MenuBar). if (parentOrientation == Axis.horizontal) { y = newY - alignmentOffset.dy; } else { @@ -3560,23 +3584,38 @@ bool _debugMenuInfo(String message, [Iterable? details]) { return true; } -bool _platformSupportsAccelerators() { +/// Whether [defaultTargetPlatform] is an Apple platform (Mac or iOS). +bool get _isApple { switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return true; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - return true; - case TargetPlatform.iOS: - case TargetPlatform.macOS: - // On iOS and macOS, pressing the Option key (a.k.a. the Alt key) causes a - // different set of characters to be generated, and the native menus don't - // support accelerators anyhow, so we just disable accelerators on these - // platforms. return false; } } +/// Whether [defaultTargetPlatform] is one that uses symbolic shortcuts. +/// +/// Mac and iOS use special symbols for modifier keys instead of their names, +/// render them in a particular order defined by Apple's human interface +/// guidelines, and format them so that the modifier keys always align. +bool get _usesSymbolicModifiers { + return _isApple; +} + + +bool get _platformSupportsAccelerators { + // On iOS and macOS, pressing the Option key (a.k.a. the Alt key) causes a + // different set of characters to be generated, and the native menus don't + // support accelerators anyhow, so we just disable accelerators on these + // platforms. + return !_isApple; +} + // BEGIN GENERATED TOKEN PROPERTIES - Menu // Do not edit by hand. The code between the "BEGIN GENERATED" and @@ -3638,6 +3677,7 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + late final TextTheme _textTheme = Theme.of(context).textTheme; @override MaterialStateProperty? get backgroundColor { @@ -3753,7 +3793,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty get textStyle { - return MaterialStatePropertyAll(Theme.of(context).textTheme.bodyLarge); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + return MaterialStatePropertyAll(_textTheme.labelLarge); } @override diff --git a/packages/flutter/lib/src/material/menu_button_theme.dart b/packages/flutter/lib/src/material/menu_button_theme.dart index 987b2c3bb7955..47bf9b4c58f1a 100644 --- a/packages/flutter/lib/src/material/menu_button_theme.dart +++ b/packages/flutter/lib/src/material/menu_button_theme.dart @@ -103,8 +103,6 @@ class MenuButtonThemeData with Diagnosticable { /// [Theme]. class MenuButtonTheme extends InheritedTheme { /// Create a [MenuButtonTheme]. - /// - /// The [data] parameter must not be null. const MenuButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/mergeable_material.dart b/packages/flutter/lib/src/material/mergeable_material.dart index 1ff2982d794e7..d8a95d6747ea1 100644 --- a/packages/flutter/lib/src/material/mergeable_material.dart +++ b/packages/flutter/lib/src/material/mergeable_material.dart @@ -19,8 +19,6 @@ import 'theme.dart'; abstract class MergeableMaterialItem { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. - /// - /// The argument is the [key], which must not be null. const MergeableMaterialItem(this.key); /// The key for this item of the list. diff --git a/packages/flutter/lib/src/material/motion.dart b/packages/flutter/lib/src/material/motion.dart new file mode 100644 index 0000000000000..e93e8226254d2 --- /dev/null +++ b/packages/flutter/lib/src/material/motion.dart @@ -0,0 +1,234 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/animation.dart'; + +// BEGIN GENERATED TOKEN PROPERTIES - Motion + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +/// The set of durations in the Material specification. +/// +/// See also: +/// +/// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) +/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) +abstract final class Durations { + /// The short1 duration (50ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration short1 = Duration(milliseconds: 50); + + /// The short2 duration (100ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration short2 = Duration(milliseconds: 100); + + /// The short3 duration (150ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration short3 = Duration(milliseconds: 150); + + /// The short4 duration (200ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration short4 = Duration(milliseconds: 200); + + /// The medium1 duration (250ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration medium1 = Duration(milliseconds: 250); + + /// The medium2 duration (300ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration medium2 = Duration(milliseconds: 300); + + /// The medium3 duration (350ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration medium3 = Duration(milliseconds: 350); + + /// The medium4 duration (400ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration medium4 = Duration(milliseconds: 400); + + /// The long1 duration (450ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration long1 = Duration(milliseconds: 450); + + /// The long2 duration (500ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration long2 = Duration(milliseconds: 500); + + /// The long3 duration (550ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration long3 = Duration(milliseconds: 550); + + /// The long4 duration (600ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration long4 = Duration(milliseconds: 600); + + /// The extralong1 duration (700ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration extralong1 = Duration(milliseconds: 700); + + /// The extralong2 duration (800ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration extralong2 = Duration(milliseconds: 800); + + /// The extralong3 duration (900ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration extralong3 = Duration(milliseconds: 900); + + /// The extralong4 duration (1000ms) in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#c009dec6-f29b-4503-b9f0-482af14a8bbd) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Duration extralong4 = Duration(milliseconds: 1000); +} + + +// TODO(guidezpl): Improve with description and assets, b/289870605 + +/// The set of easing curves in the Material specification. +/// +/// See also: +/// +/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) +/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) +/// * [Curves], for a collection of non-Material animation easing curves. +abstract final class Easing { + /// The emphasizedAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve emphasizedAccelerate = Cubic(0.3, 0.0, 0.8, 0.15); + + /// The emphasizedDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve emphasizedDecelerate = Cubic(0.05, 0.7, 0.1, 1.0); + + /// The linear easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve linear = Cubic(0.0, 0.0, 1.0, 1.0); + + /// The standard easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve standard = Cubic(0.2, 0.0, 0.0, 1.0); + + /// The standardAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve standardAccelerate = Cubic(0.3, 0.0, 1.0, 1.0); + + /// The standardDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve standardDecelerate = Cubic(0.0, 0.0, 0.0, 1.0); + + /// The legacyDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve legacyDecelerate = Cubic(0.0, 0.0, 0.2, 1.0); + + /// The legacyAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve legacyAccelerate = Cubic(0.4, 0.0, 1.0, 1.0); + + /// The legacy easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + static const Curve legacy = Cubic(0.4, 0.0, 0.2, 1.0); +} + +// END GENERATED TOKEN PROPERTIES - Motion diff --git a/packages/flutter/lib/src/material/navigation_bar.dart b/packages/flutter/lib/src/material/navigation_bar.dart index fff6c2b435ebf..e40c6247d16f8 100644 --- a/packages/flutter/lib/src/material/navigation_bar.dart +++ b/packages/flutter/lib/src/material/navigation_bar.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/widgets.dart'; import 'color_scheme.dart'; @@ -33,7 +32,7 @@ const double _kIndicatorWidth = 64; /// /// This widget does not adjust its size with the [ThemeData.visualDensity]. /// -/// The [MediaQueryData.textScaleFactor] does not adjust the size of this widget but +/// The [MediaQueryData.textScaler] does not adjust the size of this widget but /// rather the size of the [Tooltip]s displayed on long presses of the /// destinations. /// @@ -99,6 +98,7 @@ class NavigationBar extends StatelessWidget { this.indicatorShape, this.height, this.labelBehavior, + this.overlayColor, }) : assert(destinations.length >= 2), assert(0 <= selectedIndex && selectedIndex < destinations.length); @@ -183,7 +183,7 @@ class NavigationBar extends StatelessWidget { /// automatically. /// /// The height does not adjust with [ThemeData.visualDensity] or - /// [MediaQueryData.textScaleFactor] as this component loses usability at + /// [MediaQueryData.textScaler] as this component loses usability at /// larger and smaller sizes due to the truncating of labels or smaller tap /// targets. /// @@ -202,6 +202,10 @@ class NavigationBar extends StatelessWidget { /// [NavigationDestinationLabelBehavior.alwaysShow]. final NavigationDestinationLabelBehavior? labelBehavior; + /// The highlight color that's typically used to indicate that + /// the [NavigationDestination] is focused, hovered, or pressed. + final MaterialStateProperty? overlayColor; + VoidCallback _handleTap(int index) { return onDestinationSelected != null ? () => onDestinationSelected!(index) @@ -244,6 +248,7 @@ class NavigationBar extends StatelessWidget { labelBehavior: effectiveLabelBehavior, indicatorColor: indicatorColor, indicatorShape: indicatorShape, + overlayColor: overlayColor, onTap: _handleTap(i), child: destinations[i], ); @@ -296,6 +301,7 @@ class NavigationDestination extends StatelessWidget { this.selectedIcon, required this.label, this.tooltip, + this.enabled = true, }); /// The [Widget] (usually an [Icon]) that's displayed for this @@ -334,11 +340,17 @@ class NavigationDestination extends StatelessWidget { /// Defaults to null, in which case the [label] text will be used. final String? tooltip; + /// Indicates that this destination is selectable. + /// + /// Defaults to true. + final bool enabled; + @override Widget build(BuildContext context) { final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context); const Set selectedState = {MaterialState.selected}; const Set unselectedState = {}; + const Set disabledState = {MaterialState.disabled}; final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); final NavigationBarThemeData defaults = _defaultsFor(context); @@ -347,15 +359,24 @@ class NavigationDestination extends StatelessWidget { return _NavigationDestinationBuilder( label: label, tooltip: tooltip, + enabled: enabled, buildIcon: (BuildContext context) { + final IconThemeData selectedIconTheme = + navigationBarTheme.iconTheme?.resolve(selectedState) + ?? defaults.iconTheme!.resolve(selectedState)!; + final IconThemeData unselectedIconTheme = + navigationBarTheme.iconTheme?.resolve(unselectedState) + ?? defaults.iconTheme!.resolve(unselectedState)!; + final IconThemeData disabledIconTheme = + navigationBarTheme.iconTheme?.resolve(disabledState) + ?? defaults.iconTheme!.resolve(disabledState)!; + final Widget selectedIconWidget = IconTheme.merge( - data: navigationBarTheme.iconTheme?.resolve(selectedState) - ?? defaults.iconTheme!.resolve(selectedState)!, + data: enabled ? selectedIconTheme : disabledIconTheme, child: selectedIcon ?? icon, ); final Widget unselectedIconWidget = IconTheme.merge( - data: navigationBarTheme.iconTheme?.resolve(unselectedState) - ?? defaults.iconTheme!.resolve(unselectedState)!, + data: enabled ? unselectedIconTheme : disabledIconTheme, child: icon, ); @@ -383,18 +404,22 @@ class NavigationDestination extends StatelessWidget { ?? defaults.labelTextStyle!.resolve(selectedState); final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(unselectedState) ?? defaults.labelTextStyle!.resolve(unselectedState); + final TextStyle? effectiveDisabledLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(disabledState) + ?? defaults.labelTextStyle!.resolve(disabledState); + + final TextStyle? textStyle = enabled + ? _isForwardOrCompleted(animation) + ? effectiveSelectedLabelTextStyle + : effectiveUnselectedLabelTextStyle + : effectiveDisabledLabelTextStyle; + return Padding( padding: const EdgeInsets.only(top: 4), - child: _ClampTextScaleFactor( + child: MediaQuery.withClampedTextScaling( // Don't scale labels of destinations, instead, tooltip text will // upscale. - upperLimit: 1, - child: Text( - label, - style: _isForwardOrCompleted(animation) - ? effectiveSelectedLabelTextStyle - : effectiveUnselectedLabelTextStyle, - ), + maxScaleFactor: 1.0, + child: Text(label, style: textStyle), ), ); }, @@ -421,6 +446,7 @@ class _NavigationDestinationBuilder extends StatefulWidget { required this.buildLabel, required this.label, this.tooltip, + this.enabled = true, }); /// Builds the icon for a destination in a [NavigationBar]. @@ -459,6 +485,11 @@ class _NavigationDestinationBuilder extends StatefulWidget { /// Defaults to null, in which case the [label] text will be used. final String? tooltip; + /// Indicates that this destination is selectable. + /// + /// Defaults to true. + final bool enabled; + @override State<_NavigationDestinationBuilder> createState() => _NavigationDestinationBuilderState(); } @@ -478,8 +509,9 @@ class _NavigationDestinationBuilderState extends State<_NavigationDestinationBui child: _IndicatorInkWell( iconKey: iconKey, labelBehavior: info.labelBehavior, - customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape, - onTap: info.onTap, + customBorder: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape, + overlayColor: info.overlayColor ?? navigationBarTheme.overlayColor, + onTap: widget.enabled ? info.onTap : null, child: Row( children: [ Expanded( @@ -501,6 +533,7 @@ class _IndicatorInkWell extends InkResponse { const _IndicatorInkWell({ required this.iconKey, required this.labelBehavior, + super.overlayColor, super.customBorder, super.onTap, super.child, @@ -538,6 +571,7 @@ class _NavigationDestinationInfo extends InheritedWidget { required this.labelBehavior, required this.indicatorColor, required this.indicatorShape, + required this.overlayColor, required this.onTap, required super.child, }); @@ -604,6 +638,12 @@ class _NavigationDestinationInfo extends InheritedWidget { /// This is used by destinations to override the indicator shape. final ShapeBorder? indicatorShape; + /// The highlight color that's typically used to indicate that + /// the [NavigationDestination] is focused, hovered, or pressed. + /// + /// This is used by destinations to override the overlay color. + final MaterialStateProperty? overlayColor; + /// The callback that should be called when this destination is tapped. /// /// This is computed by calling [NavigationBar.onDestinationSelected] @@ -1018,53 +1058,6 @@ class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate { } } -/// Utility Widgets - -// Clamps [MediaQueryData.textScaleFactor] so that if it is greater than -// [upperLimit] or less than [lowerLimit], [upperLimit] or [lowerLimit] will be -// used instead for the [child] widget. -// -// Example: -// -// ```dart -// _ClampTextScaleFactor( -// upperLimit: 2.0, -// child: const Text('Foo'), // If textScaleFactor is 3.0, this will only scale 2x. -// ) -// ``` -class _ClampTextScaleFactor extends StatelessWidget { - /// Clamps the text scale factor of descendants by modifying the [MediaQuery] - /// surrounding [child]. - const _ClampTextScaleFactor({ - this.upperLimit = double.infinity, - required this.child, - }); - - /// The maximum amount that the text scale factor should be for the [child] - /// widget. - /// - /// If this is `1.5`, the textScaleFactor for child widgets will never be - /// greater than `1.5`. - final double upperLimit; - - /// The [Widget] that should have its (and its descendants) text scale factor - /// clamped. - final Widget child; - - @override - Widget build(BuildContext context) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: clampDouble(MediaQuery.textScaleFactorOf(context), - 0.0, - upperLimit, - ), - ), - child: child, - ); - } -} - /// Widget that listens to an animation, and rebuilds when the animation changes /// [AnimationStatus]. /// @@ -1371,9 +1364,11 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { return MaterialStateProperty.resolveWith((Set states) { return IconThemeData( size: 24.0, - color: states.contains(MaterialState.selected) - ? _colors.onSecondaryContainer - : _colors.onSurfaceVariant, + color: states.contains(MaterialState.disabled) + ? _colors.onSurfaceVariant.withOpacity(0.38) + : states.contains(MaterialState.selected) + ? _colors.onSecondaryContainer + : _colors.onSurfaceVariant, ); }); } @@ -1384,9 +1379,11 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { @override MaterialStateProperty? get labelTextStyle { return MaterialStateProperty.resolveWith((Set states) { final TextStyle style = _textTheme.labelMedium!; - return style.apply(color: states.contains(MaterialState.selected) - ? _colors.onSurface - : _colors.onSurfaceVariant + return style.apply(color: states.contains(MaterialState.disabled) + ? _colors.onSurfaceVariant.withOpacity(0.38) + : states.contains(MaterialState.selected) + ? _colors.onSurface + : _colors.onSurfaceVariant ); }); } diff --git a/packages/flutter/lib/src/material/navigation_bar_theme.dart b/packages/flutter/lib/src/material/navigation_bar_theme.dart index ae4781d17b738..adb9e3985cec1 100644 --- a/packages/flutter/lib/src/material/navigation_bar_theme.dart +++ b/packages/flutter/lib/src/material/navigation_bar_theme.dart @@ -52,6 +52,7 @@ class NavigationBarThemeData with Diagnosticable { this.labelTextStyle, this.iconTheme, this.labelBehavior, + this.overlayColor, }); /// Overrides the default value of [NavigationBar.height]. @@ -91,6 +92,9 @@ class NavigationBarThemeData with Diagnosticable { /// Overrides the default value of [NavigationBar.labelBehavior]. final NavigationDestinationLabelBehavior? labelBehavior; + /// Overrides the default value of [NavigationBar.overlayColor]. + final MaterialStateProperty? overlayColor; + /// Creates a copy of this object with the given fields replaced with the /// new values. NavigationBarThemeData copyWith({ @@ -104,6 +108,7 @@ class NavigationBarThemeData with Diagnosticable { MaterialStateProperty? labelTextStyle, MaterialStateProperty? iconTheme, NavigationDestinationLabelBehavior? labelBehavior, + MaterialStateProperty? overlayColor, }) { return NavigationBarThemeData( height: height ?? this.height, @@ -116,6 +121,7 @@ class NavigationBarThemeData with Diagnosticable { labelTextStyle: labelTextStyle ?? this.labelTextStyle, iconTheme: iconTheme ?? this.iconTheme, labelBehavior: labelBehavior ?? this.labelBehavior, + overlayColor: overlayColor ?? this.overlayColor, ); } @@ -139,6 +145,7 @@ class NavigationBarThemeData with Diagnosticable { labelTextStyle: MaterialStateProperty.lerp(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp), iconTheme: MaterialStateProperty.lerp(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp), labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior, + overlayColor: MaterialStateProperty.lerp(a?.overlayColor, b?.overlayColor, t, Color.lerp), ); } @@ -154,6 +161,7 @@ class NavigationBarThemeData with Diagnosticable { labelTextStyle, iconTheme, labelBehavior, + overlayColor, ); @override @@ -165,16 +173,17 @@ class NavigationBarThemeData with Diagnosticable { return false; } return other is NavigationBarThemeData - && other.height == height - && other.backgroundColor == backgroundColor - && other.elevation == elevation - && other.shadowColor == shadowColor - && other.surfaceTintColor == surfaceTintColor - && other.indicatorColor == indicatorColor - && other.indicatorShape == indicatorShape - && other.labelTextStyle == labelTextStyle - && other.iconTheme == iconTheme - && other.labelBehavior == labelBehavior; + && other.height == height + && other.backgroundColor == backgroundColor + && other.elevation == elevation + && other.shadowColor == shadowColor + && other.surfaceTintColor == surfaceTintColor + && other.indicatorColor == indicatorColor + && other.indicatorShape == indicatorShape + && other.labelTextStyle == labelTextStyle + && other.iconTheme == iconTheme + && other.labelBehavior == labelBehavior + && other.overlayColor == overlayColor; } @override @@ -190,6 +199,7 @@ class NavigationBarThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('labelTextStyle', labelTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty>('iconTheme', iconTheme, defaultValue: null)); properties.add(DiagnosticsProperty('labelBehavior', labelBehavior, defaultValue: null)); + properties.add(DiagnosticsProperty>('overlayColor', overlayColor, defaultValue: null)); } } @@ -206,8 +216,6 @@ class NavigationBarThemeData with Diagnosticable { class NavigationBarTheme extends InheritedTheme { /// Creates a navigation rail theme that controls the /// [NavigationBarThemeData] properties for a [NavigationBar]. - /// - /// The data argument must not be null. const NavigationBarTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/navigation_drawer.dart b/packages/flutter/lib/src/material/navigation_drawer.dart index f52e20274f8de..3c4edbe172ed8 100644 --- a/packages/flutter/lib/src/material/navigation_drawer.dart +++ b/packages/flutter/lib/src/material/navigation_drawer.dart @@ -165,12 +165,13 @@ class NavigationDrawer extends StatelessWidget { destinationIndex += 1; } } + final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context); return Drawer( - backgroundColor: backgroundColor, - shadowColor: shadowColor, - surfaceTintColor: surfaceTintColor, - elevation: elevation, + backgroundColor: backgroundColor ?? navigationDrawerTheme.backgroundColor, + shadowColor: shadowColor ?? navigationDrawerTheme.shadowColor, + surfaceTintColor: surfaceTintColor ?? navigationDrawerTheme.surfaceTintColor, + elevation: elevation ?? navigationDrawerTheme.elevation, child: SafeArea( bottom: false, child: ListView( @@ -192,6 +193,7 @@ class NavigationDrawerDestination extends StatelessWidget { required this.icon, this.selectedIcon, required this.label, + this.enabled = true, }); /// Sets the color of the [Material] that holds all of the [Drawer]'s @@ -228,12 +230,20 @@ class NavigationDrawerDestination extends StatelessWidget { /// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant]. final Widget label; + /// Indicates that this destination is selectable. + /// + /// Defaults to true. + final bool enabled; + @override Widget build(BuildContext context) { const Set selectedState = { MaterialState.selected }; const Set unselectedState = {}; + const Set disabledState = { + MaterialState.disabled + }; final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context); @@ -246,13 +256,13 @@ class NavigationDrawerDestination extends StatelessWidget { return _NavigationDestinationBuilder( buildIcon: (BuildContext context) { final Widget selectedIconWidget = IconTheme.merge( - data: navigationDrawerTheme.iconTheme?.resolve(selectedState) ?? - defaults.iconTheme!.resolve(selectedState)!, + data: navigationDrawerTheme.iconTheme?.resolve(enabled ? selectedState : disabledState) ?? + defaults.iconTheme!.resolve(enabled ? selectedState : disabledState)!, child: selectedIcon ?? icon, ); final Widget unselectedIconWidget = IconTheme.merge( - data: navigationDrawerTheme.iconTheme?.resolve(unselectedState) ?? - defaults.iconTheme!.resolve(unselectedState)!, + data: navigationDrawerTheme.iconTheme?.resolve(enabled ? unselectedState : disabledState) ?? + defaults.iconTheme!.resolve(enabled ? unselectedState : disabledState)!, child: icon, ); @@ -262,11 +272,12 @@ class NavigationDrawerDestination extends StatelessWidget { }, buildLabel: (BuildContext context) { final TextStyle? effectiveSelectedLabelTextStyle = - navigationDrawerTheme.labelTextStyle?.resolve(selectedState) ?? - defaults.labelTextStyle!.resolve(selectedState); + navigationDrawerTheme.labelTextStyle?.resolve(enabled ? selectedState : disabledState) ?? + defaults.labelTextStyle!.resolve(enabled ? selectedState : disabledState); final TextStyle? effectiveUnselectedLabelTextStyle = - navigationDrawerTheme.labelTextStyle?.resolve(unselectedState) ?? - defaults.labelTextStyle!.resolve(unselectedState); + navigationDrawerTheme.labelTextStyle?.resolve(enabled ? unselectedState : disabledState) ?? + defaults.labelTextStyle!.resolve(enabled ? unselectedState : disabledState); + return DefaultTextStyle( style: _isForwardOrCompleted(animation) ? effectiveSelectedLabelTextStyle! @@ -274,6 +285,7 @@ class NavigationDrawerDestination extends StatelessWidget { child: label, ); }, + enabled: enabled, ); } } @@ -295,6 +307,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { const _NavigationDestinationBuilder({ required this.buildIcon, required this.buildLabel, + this.enabled = true, }); /// Builds the icon for a destination in a [NavigationDrawer]. @@ -321,12 +334,26 @@ class _NavigationDestinationBuilder extends StatelessWidget { /// animation is decreasing or dismissed. final WidgetBuilder buildLabel; + /// Indicates that this destination is selectable. + /// + /// Defaults to true. + final bool enabled; + @override Widget build(BuildContext context) { final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context); final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context); final NavigationDrawerThemeData defaults = _NavigationDrawerDefaultsM3(context); + final Row destinationBody = Row( + children: [ + const SizedBox(width: 16), + buildIcon(context), + const SizedBox(width: 12), + buildLabel(context), + ], + ); + return Padding( padding: info.tilePadding, child: _NavigationDestinationSemantics( @@ -334,7 +361,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight, child: InkWell( highlightColor: Colors.transparent, - onTap: info.onTap, + onTap: enabled ? info.onTap : null, customBorder: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!, child: Stack( alignment: Alignment.center, @@ -346,14 +373,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width, height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height, ), - Row( - children: [ - const SizedBox(width: 16), - buildIcon(context), - const SizedBox(width: 12), - buildLabel(context), - ], - ), + destinationBody ], ), ), @@ -701,7 +721,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData { return MaterialStateProperty.resolveWith((Set states) { return IconThemeData( size: 24.0, - color: states.contains(MaterialState.selected) + color: states.contains(MaterialState.disabled) + ? _colors.onSurfaceVariant.withOpacity(0.38) + : states.contains(MaterialState.selected) ? _colors.onSecondaryContainer : _colors.onSurfaceVariant, ); @@ -713,7 +735,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData { return MaterialStateProperty.resolveWith((Set states) { final TextStyle style = _textTheme.labelLarge!; return style.apply( - color: states.contains(MaterialState.selected) + color: states.contains(MaterialState.disabled) + ? _colors.onSurfaceVariant.withOpacity(0.38) + : states.contains(MaterialState.selected) ? _colors.onSecondaryContainer : _colors.onSurfaceVariant, ); diff --git a/packages/flutter/lib/src/material/navigation_drawer_theme.dart b/packages/flutter/lib/src/material/navigation_drawer_theme.dart index e16a2a3e3d6fa..7a06587572cc1 100644 --- a/packages/flutter/lib/src/material/navigation_drawer_theme.dart +++ b/packages/flutter/lib/src/material/navigation_drawer_theme.dart @@ -219,8 +219,6 @@ class NavigationDrawerThemeData with Diagnosticable { class NavigationDrawerTheme extends InheritedTheme { /// Creates a navigation rail theme that controls the /// [NavigationDrawerThemeData] properties for a [NavigationDrawer]. - /// - /// The data argument must not be null. const NavigationDrawerTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/navigation_rail.dart b/packages/flutter/lib/src/material/navigation_rail.dart index 4219d52d302ad..0e47262b2ecaf 100644 --- a/packages/flutter/lib/src/material/navigation_rail.dart +++ b/packages/flutter/lib/src/material/navigation_rail.dart @@ -80,8 +80,8 @@ class NavigationRail extends StatefulWidget { /// [minExtendedWidth] is specified, it must be non-negative and greater than /// [minWidth]. /// - /// The argument [extended] must not be null. [extended] can only be set to - /// true when the [labelType] is null or [NavigationRailLabelType.none]. + /// The [extended] argument can only be set to true when the [labelType] is + /// null or [NavigationRailLabelType.none]. /// /// If [backgroundColor], [elevation], [groupAlignment], [labelType], /// [unselectedLabelTextStyle], [selectedLabelTextStyle], @@ -581,9 +581,9 @@ class _RailDestination extends StatelessWidget { ); final ThemeData theme = Theme.of(context); - + final TextDirection textDirection = Directionality.of(context); final bool material3 = theme.useMaterial3; - final EdgeInsets destinationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context)); + final EdgeInsets destinationPadding = (padding ?? EdgeInsets.zero).resolve(textDirection); Offset indicatorOffset; bool applyXOffset = false; @@ -594,19 +594,27 @@ class _RailDestination extends StatelessWidget { child: icon, ); final Widget styledLabel = DefaultTextStyle( - style: labelTextStyle, + style: disabled + ? labelTextStyle.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.38)) + : labelTextStyle, child: label, ); Widget content; + // The indicator height is fixed and equal to _kIndicatorHeight. + // When the icon height is larger than the indicator height the indicator + // vertical offset is used to vertically center the indicator. + final bool isLargeIconSize = iconTheme.size != null && iconTheme.size! > _kIndicatorHeight; + final double indicatorVerticalOffset = isLargeIconSize ? (iconTheme.size! - _kIndicatorHeight) / 2 : 0; + switch (labelType) { case NavigationRailLabelType.none: // Split the destination spacing across the top and bottom to keep the icon centered. final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null; indicatorOffset = Offset( minWidth / 2 + destinationPadding.left, - _verticalDestinationSpacingM3 / 2 + destinationPadding.top, + _verticalDestinationSpacingM3 / 2 + destinationPadding.top + indicatorVerticalOffset, ); final Widget iconPart = Column( children: [ @@ -685,9 +693,15 @@ class _RailDestination extends StatelessWidget { final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding); final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2); final double indicatorVerticalPadding = destinationPadding.top; - indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding); + indicatorOffset = Offset( + minWidth / 2 + indicatorHorizontalPadding, + indicatorVerticalPadding + indicatorVerticalOffset, + ); if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) { - indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding); + indicatorOffset = Offset( + minWidth / 2 + _horizontalDestinationSpacingM3, + indicatorVerticalPadding + indicatorVerticalOffset, + ); } content = Container( constraints: BoxConstraints( @@ -732,9 +746,15 @@ class _RailDestination extends StatelessWidget { final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel); final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2); final double indicatorVerticalPadding = destinationPadding.top; - indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding); + indicatorOffset = Offset( + minWidth / 2 + indicatorHorizontalPadding, + indicatorVerticalPadding + indicatorVerticalOffset, + ); if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) { - indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding); + indicatorOffset = Offset( + minWidth / 2 + _horizontalDestinationSpacingM3, + indicatorVerticalPadding + indicatorVerticalOffset, + ); } content = Container( constraints: BoxConstraints( @@ -778,6 +798,7 @@ class _RailDestination extends StatelessWidget { useMaterial3: material3, indicatorOffset: indicatorOffset, applyXOffset: applyXOffset, + textDirection: textDirection, child: content, ), ), @@ -801,6 +822,7 @@ class _IndicatorInkWell extends InkResponse { required this.useMaterial3, required this.indicatorOffset, required this.applyXOffset, + required this.textDirection, }) : super( containedInkWell: true, highlightShape: BoxShape.rectangle, @@ -809,16 +831,25 @@ class _IndicatorInkWell extends InkResponse { ); final bool useMaterial3; + // The offset used to position Ink highlight. final Offset indicatorOffset; + // Whether the horizontal offset from indicatorOffset should be used to position Ink highlight. // If true, Ink highlight uses the indicator horizontal offset. If false, Ink highlight is centered horizontally. final bool applyXOffset; + // The text direction used to adjust the indicator horizontal offset. + final TextDirection textDirection; + @override RectCallback? getRectCallback(RenderBox referenceBox) { if (useMaterial3) { - final double indicatorHorizontalCenter = applyXOffset ? indicatorOffset.dx : referenceBox.size.width / 2; + final double boxWidth = referenceBox.size.width; + double indicatorHorizontalCenter = applyXOffset ? indicatorOffset.dx : boxWidth / 2; + if (textDirection == TextDirection.rtl) { + indicatorHorizontalCenter = boxWidth - indicatorHorizontalCenter; + } return () { return Rect.fromLTWH( indicatorHorizontalCenter - (_kCircularIndicatorDiameter / 2), @@ -915,9 +946,9 @@ enum NavigationRailLabelType { class NavigationRailDestination { /// Creates a destination that is used with [NavigationRail.destinations]. /// - /// [icon] and [label] must be non-null. When the [NavigationRail.labelType] - /// is [NavigationRailLabelType.none], the label is still used for semantics, - /// and may still be used if [NavigationRail.extended] is true. + /// When the [NavigationRail.labelType] is [NavigationRailLabelType.none], the + /// label is still used for semantics, and may still be used if + /// [NavigationRail.extended] is true. const NavigationRailDestination({ required this.icon, Widget? selectedIcon, diff --git a/packages/flutter/lib/src/material/navigation_rail_theme.dart b/packages/flutter/lib/src/material/navigation_rail_theme.dart index b8a3f788754b8..664d2cb57fd3f 100644 --- a/packages/flutter/lib/src/material/navigation_rail_theme.dart +++ b/packages/flutter/lib/src/material/navigation_rail_theme.dart @@ -236,8 +236,6 @@ class NavigationRailThemeData with Diagnosticable { class NavigationRailTheme extends InheritedTheme { /// Creates a navigation rail theme that controls the /// [NavigationRailThemeData] properties for a [NavigationRail]. - /// - /// The data argument must not be null. const NavigationRailTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index 8ee11e3b3e72d..c8875b2ac2baa 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -67,8 +67,6 @@ import 'theme_data.dart'; /// * class OutlinedButton extends ButtonStyleButton { /// Create an OutlinedButton. - /// - /// The [autofocus] and [clipBehavior] arguments must not be null. const OutlinedButton({ super.key, required super.onPressed, @@ -88,8 +86,6 @@ class OutlinedButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 12 logical pixels /// at the start, and 16 at the end, with an 8 pixel gap in between. - /// - /// The [icon] and [label] arguments must not be null. factory OutlinedButton.icon({ Key? key, required VoidCallback? onPressed, @@ -354,7 +350,7 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) { EdgeInsets.symmetric(horizontal: padding1x), EdgeInsets.symmetric(horizontal: padding1x / 2), EdgeInsets.symmetric(horizontal: padding1x / 2 / 2), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); } @@ -439,7 +435,7 @@ class _OutlinedButtonWithIcon extends OutlinedButton { const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0), const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0), const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); return super.defaultStyleOf(context).copyWith( padding: MaterialStatePropertyAll(scaledPadding), @@ -458,7 +454,7 @@ class _OutlinedButtonWithIconChild extends StatelessWidget { @override Widget build(BuildContext context) { - final double scale = MediaQuery.textScaleFactorOf(context); + final double scale = MediaQuery.textScalerOf(context).textScaleFactor; final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!; return Row( mainAxisSize: MainAxisSize.min, diff --git a/packages/flutter/lib/src/material/outlined_button_theme.dart b/packages/flutter/lib/src/material/outlined_button_theme.dart index 5dead513f803d..724f1e7d4f601 100644 --- a/packages/flutter/lib/src/material/outlined_button_theme.dart +++ b/packages/flutter/lib/src/material/outlined_button_theme.dart @@ -91,8 +91,6 @@ class OutlinedButtonThemeData with Diagnosticable { /// [ButtonStyle] for [OutlinedButton]s below the overall [Theme]. class OutlinedButtonTheme extends InheritedTheme { /// Create a [OutlinedButtonTheme]. - /// - /// The [data] parameter must not be null. const OutlinedButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 73d677ff4cd78..db781f5376473 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -20,6 +20,9 @@ import 'theme.dart'; /// fullscreen modal dialog. On iOS, those routes animate from the bottom to the /// top rather than horizontally. /// +/// If `barrierDismissible` is true, then pressing the escape key on the keyboard +/// will cause the current route to be popped with null as the value. +/// /// The type `T` specifies the return type of the route which can be supplied as /// the route is popped from the stack via [Navigator.pop] by providing the /// optional `result` argument. @@ -31,15 +34,13 @@ import 'theme.dart'; /// * [MaterialPage], which is a [Page] of this class. class MaterialPageRoute extends PageRoute with MaterialRouteTransitionMixin { /// Construct a MaterialPageRoute whose contents are defined by [builder]. - /// - /// The values of [builder], [maintainState], and [PageRoute.fullscreenDialog] - /// must not be null. MaterialPageRoute({ required this.builder, super.settings, this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, + super.barrierDismissible = false, }) { assert(opaque); } diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index 56dc56fbb0212..45eb851d778fe 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -17,6 +17,7 @@ import 'icon_button.dart'; import 'icons.dart'; import 'ink_decoration.dart'; import 'material_localizations.dart'; +import 'material_state.dart'; import 'progress_indicator.dart'; import 'theme.dart'; @@ -28,6 +29,26 @@ import 'theme.dart'; /// Data is read lazily from a [DataTableSource]. The widget is presented /// as a [Card]. /// +/// If the [key] is a [PageStorageKey], the [initialFirstRowIndex] is persisted +/// to [PageStorage]. +/// +/// {@tool dartpad} +/// +/// This sample shows how to display a [DataTable] with three columns: name, +/// age, and role. The columns are defined by three [DataColumn] objects. The +/// table contains three rows of data for three example users, the data for +/// which is defined by three [DataRow] objects. +/// +/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// +/// This example shows how paginated data tables can supported sorted data. +/// +/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart ** +/// {@end-tool} +/// /// See also: /// /// * [DataTable], which is not paginated. @@ -50,15 +71,11 @@ class PaginatedDataTable extends StatefulWidget { /// order is ascending, this should be true (the default), otherwise it should /// be false. /// - /// The [source] must not be null. The [source] should be a long-lived - /// [DataTableSource]. The same source should be provided each time a - /// particular [PaginatedDataTable] widget is created; avoid creating a new - /// [DataTableSource] with each new instance of the [PaginatedDataTable] - /// widget unless the data table really is to now show entirely different - /// data from a new source. - /// - /// The [rowsPerPage] and [availableRowsPerPage] must not be null (they - /// both have defaults, though, so don't have to be specified). + /// The [source] should be a long-lived [DataTableSource]. The same source + /// should be provided each time a particular [PaginatedDataTable] widget is + /// created; avoid creating a new [DataTableSource] with each new instance of + /// the [PaginatedDataTable] widget unless the data table really is to now + /// show entirely different data from a new source. /// /// Themed by [DataTableTheme]. [DataTableThemeData.decoration] is ignored. /// To modify the border or background color of the [PaginatedDataTable], use @@ -94,14 +111,15 @@ class PaginatedDataTable extends StatefulWidget { this.checkboxHorizontalMargin, this.controller, this.primary, + this.headingRowColor, }) : assert(actions == null || (header != null)), assert(columns.isNotEmpty), assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight), assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'), - dataRowMinHeight = dataRowHeight ?? dataRowMinHeight ?? kMinInteractiveDimension, - dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight ?? kMinInteractiveDimension, + dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, + dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight, assert(rowsPerPage > 0), assert(() { if (onRowsPerPageChanged != null) { @@ -139,13 +157,15 @@ class PaginatedDataTable extends StatefulWidget { /// The current primary sort key's column. /// - /// See [DataTable.sortColumnIndex]. + /// See [DataTable.sortColumnIndex] for details. + /// + /// The direction of the sort is specified using [sortAscending]. final int? sortColumnIndex; /// Whether the column mentioned in [sortColumnIndex], if any, is sorted /// in ascending order. /// - /// See [DataTable.sortAscending]. + /// See [DataTable.sortAscending] for details. final bool sortAscending; /// Invoked when the user selects or unselects every row, using the @@ -168,13 +188,13 @@ class PaginatedDataTable extends StatefulWidget { /// /// This value is optional and defaults to [kMinInteractiveDimension] if not /// specified. - final double dataRowMinHeight; + final double? dataRowMinHeight; /// The maximum height of each row (excluding the row that contains column headings). /// - /// This value is optional and defaults to kMinInteractiveDimension if not + /// This value is optional and defaults to [kMinInteractiveDimension] if not /// specified. - final double dataRowMaxHeight; + final double? dataRowMaxHeight; /// The height of the heading row. /// @@ -240,7 +260,7 @@ class PaginatedDataTable extends StatefulWidget { /// and no affordance will be provided to change the value. final ValueChanged? onRowsPerPageChanged; - /// The data source which provides data to show in each row. Must be non-null. + /// The data source which provides data to show in each row. /// /// This object should generally have a lifetime longer than the /// [PaginatedDataTable] widget itself; it should be reused each time the @@ -266,6 +286,9 @@ class PaginatedDataTable extends StatefulWidget { /// {@macro flutter.widgets.scroll_view.primary} final bool? primary; + /// {@macro flutter.material.dataTable.headingRowColor} + final MaterialStateProperty? headingRowColor; + @override PaginatedDataTableState createState() => PaginatedDataTableState(); } @@ -294,10 +317,27 @@ class PaginatedDataTableState extends State { if (oldWidget.source != widget.source) { oldWidget.source.removeListener(_handleDataSourceChanged); widget.source.addListener(_handleDataSourceChanged); - _handleDataSourceChanged(); + _updateCaches(); } } + @override + void reassemble() { + super.reassemble(); + // This function is called during hot reload. + // + // Normally, if the data source changes, it would notify its listeners and + // thus trigger _handleDataSourceChanged(), which clears the row cache and + // causes the widget to rebuild. + // + // During a hot reload, though, a data source can change in ways that will + // invalidate the row cache (e.g. adding or removing columns) without ever + // triggering a notification, leaving the PaginatedDataTable in an invalid + // state. This method handles this case by clearing the cache any time the + // widget is involved in a hot reload. + _updateCaches(); + } + @override void dispose() { widget.source.removeListener(_handleDataSourceChanged); @@ -305,12 +345,14 @@ class PaginatedDataTableState extends State { } void _handleDataSourceChanged() { - setState(() { - _rowCount = widget.source.rowCount; - _rowCountApproximate = widget.source.isRowCountApproximate; - _selectedRowCount = widget.source.selectedRowCount; - _rows.clear(); - }); + setState(_updateCaches); + } + + void _updateCaches() { + _rowCount = widget.source.rowCount; + _rowCountApproximate = widget.source.isRowCountApproximate; + _selectedRowCount = widget.source.selectedRowCount; + _rows.clear(); } /// Ensures that the given row is visible. @@ -456,7 +498,7 @@ class PaginatedDataTableState extends State { Text( localizations.pageRowsInfoTitle( _firstRowIndex + 1, - _firstRowIndex + widget.rowsPerPage, + math.min(_firstRowIndex + widget.rowsPerPage, _rowCount), _rowCount, _rowCountApproximate, ), @@ -554,6 +596,7 @@ class PaginatedDataTableState extends State { showCheckboxColumn: widget.showCheckboxColumn, showBottomBorder: true, rows: _getRows(_firstRowIndex, widget.rowsPerPage), + headingRowColor: widget.headingRowColor, ), ), ), diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index c4846e8dd80b7..520868c42d9d9 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -14,6 +14,7 @@ import 'icon_button.dart'; import 'icons.dart'; import 'ink_well.dart'; import 'list_tile.dart'; +import 'list_tile_theme.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'material_state.dart'; @@ -32,7 +33,6 @@ import 'tooltip.dart'; const Duration _kMenuDuration = Duration(milliseconds: 300); const double _kMenuCloseIntervalEnd = 2.0 / 3.0; -const double _kMenuHorizontalPadding = 16.0; const double _kMenuDividerHeight = 16.0; const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; @@ -216,8 +216,6 @@ class PopupMenuItem extends PopupMenuEntry { /// Creates an item for a popup menu. /// /// By default, the item is [enabled]. - /// - /// The `enabled` and `height` arguments must not be null. const PopupMenuItem({ super.key, this.value, @@ -255,7 +253,11 @@ class PopupMenuItem extends PopupMenuEntry { /// If a [height] greater than the height of the sum of the padding and [child] /// is provided, then the padding's effect will not be visible. /// - /// When null, the horizontal padding defaults to 16.0 on both sides. + /// If this is null and [ThemeData.useMaterial3] is true, the horizontal padding + /// defaults to 12.0 on both sides. + /// + /// If this is null and [ThemeData.useMaterial3] is false, the horizontal padding + /// defaults to 16.0 on both sides. final EdgeInsets? padding; /// The text style of the popup menu item. @@ -372,7 +374,7 @@ class PopupMenuItemState> extends State { child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: widget.height), - padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + padding: widget.padding ?? (theme.useMaterial3 ? _PopupMenuDefaultsM3.menuHorizontalPadding : _PopupMenuDefaultsM2.menuHorizontalPadding), child: buildChild(), ), ); @@ -393,7 +395,11 @@ class PopupMenuItemState> extends State { onTap: widget.enabled ? handleTap : null, canRequestFocus: widget.enabled, mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor), - child: item, + child: ListTileTheme.merge( + contentPadding: EdgeInsets.zero, + titleTextStyle: style, + child: item, + ), ), ), ); @@ -466,8 +472,6 @@ class CheckedPopupMenuItem extends PopupMenuItem { /// /// By default, the menu item is [enabled] but unchecked. To mark the item as /// checked, set [checked] to true. - /// - /// The `checked` and `enabled` arguments must not be null. const CheckedPopupMenuItem({ super.key, super.value, @@ -475,8 +479,10 @@ class CheckedPopupMenuItem extends PopupMenuItem { super.enabled, super.padding, super.height, + super.labelTextStyle, super.mouseCursor, super.child, + super.onTap, }); /// Whether to display a checkmark next to the menu item. @@ -529,14 +535,27 @@ class _CheckedPopupMenuItemState extends PopupMenuItemState states = { + if (widget.checked) MaterialState.selected, + }; + final MaterialStateProperty? effectiveLabelTextStyle = widget.labelTextStyle + ?? popupMenuTheme.labelTextStyle + ?? defaults.labelTextStyle; return IgnorePointer( - child: ListTile( - enabled: widget.enabled, - leading: FadeTransition( - opacity: _opacity, - child: Icon(_controller.isDismissed ? null : Icons.done), + child: ListTileTheme.merge( + contentPadding: EdgeInsets.zero, + child: ListTile( + enabled: widget.enabled, + titleTextStyle: effectiveLabelTextStyle?.resolve(states), + leading: FadeTransition( + opacity: _opacity, + child: Icon(_controller.isDismissed ? null : Icons.done), + ), + title: widget.child, ), - title: widget.child, ), ); } @@ -881,7 +900,7 @@ class _PopupMenuRoute extends PopupRoute { /// Show a popup menu that contains the `items` at `position`. /// -/// `items` should be non-null and not empty. +/// The `items` parameter must not be empty. /// /// If `initialValue` is specified then the first item with a matching value /// will be highlighted and the value of `position` gives the rectangle whose @@ -976,7 +995,7 @@ Future showMenu({ shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, semanticLabel: semanticLabel, - barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierLabel: MaterialLocalizations.of(context).menuDismissLabel, shape: shape, color: color, capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), @@ -1078,8 +1097,6 @@ typedef PopupMenuItemBuilder = List> Function(BuildContext /// * [showMenu], a method to dynamically show a popup menu at a given location. class PopupMenuButton extends StatefulWidget { /// Creates a button that shows a popup menu. - /// - /// The [itemBuilder] argument must not be null. const PopupMenuButton({ super.key, required this.itemBuilder, @@ -1100,6 +1117,7 @@ class PopupMenuButton extends StatefulWidget { this.enabled = true, this.shape, this.color, + this.iconColor, this.enableFeedback, this.constraints, this.position, @@ -1183,11 +1201,11 @@ class PopupMenuButton extends StatefulWidget { /// Whether this popup menu button is interactive. /// - /// Must be non-null, defaults to `true` + /// Defaults to true. /// - /// If `true` the button will respond to presses by displaying the menu. + /// If true, the button will respond to presses by displaying the menu. /// - /// If `false`, the button is styled with the disabled color from the + /// If false, the button is styled with the disabled color from the /// current [Theme] and will not respond to presses or show the popup /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called. /// @@ -1210,6 +1228,13 @@ class PopupMenuButton extends StatefulWidget { /// Theme.of(context).cardColor is used. final Color? color; + /// If provided, this color is used for the button icon. + /// + /// If this property is null, then [PopupMenuThemeData.iconColor] is used. + /// If [PopupMenuThemeData.iconColor] is also null then defaults to + /// [IconThemeData.color]. + final Color? iconColor; + /// Whether detected gestures should provide acoustic and/or haptic feedback. /// /// For example, on Android a tap will produce a clicking sound and a @@ -1258,7 +1283,7 @@ class PopupMenuButton extends StatefulWidget { /// /// The [clipBehavior] argument is used the clip shape of the menu. /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; @override @@ -1344,6 +1369,7 @@ class PopupMenuButtonState extends State> { @override Widget build(BuildContext context) { final IconThemeData iconTheme = IconTheme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final bool enableFeedback = widget.enableFeedback ?? PopupMenuTheme.of(context).enableFeedback ?? true; @@ -1367,8 +1393,8 @@ class PopupMenuButtonState extends State> { icon: widget.icon ?? Icon(Icons.adaptive.more), padding: widget.padding, splashRadius: widget.splashRadius, - iconSize: widget.iconSize ?? iconTheme.size, - color: widget.color ?? iconTheme.color, + iconSize: widget.iconSize ?? popupMenuTheme.iconSize ?? iconTheme.size, + color: widget.iconColor ?? popupMenuTheme.iconColor ?? iconTheme.color, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, @@ -1406,6 +1432,8 @@ class _PopupMenuDefaultsM2 extends PopupMenuThemeData { @override TextStyle? get textStyle => _textTheme.subtitle1; + + static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 16.0); } // BEGIN GENERATED TOKEN PROPERTIES - PopupMenu @@ -1445,5 +1473,9 @@ class _PopupMenuDefaultsM3 extends PopupMenuThemeData { @override ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 12.0); } // END GENERATED TOKEN PROPERTIES - PopupMenu diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart index 368492d0c51a3..5d2c675398c5f 100644 --- a/packages/flutter/lib/src/material/popup_menu_theme.dart +++ b/packages/flutter/lib/src/material/popup_menu_theme.dart @@ -55,6 +55,8 @@ class PopupMenuThemeData with Diagnosticable { this.enableFeedback, this.mouseCursor, this.position, + this.iconColor, + this.iconSize, }); /// The background color of the popup menu. @@ -95,6 +97,12 @@ class PopupMenuThemeData with Diagnosticable { /// popup menu appear directly over the button that was used to create it. final PopupMenuPosition? position; + /// The color of the icon in the popup menu button. + final Color? iconColor; + + /// The size of the icon in the popup menu button. + final double? iconSize; + /// Creates a copy of this object with the given fields replaced with the /// new values. PopupMenuThemeData copyWith({ @@ -108,6 +116,8 @@ class PopupMenuThemeData with Diagnosticable { bool? enableFeedback, MaterialStateProperty? mouseCursor, PopupMenuPosition? position, + Color? iconColor, + double? iconSize, }) { return PopupMenuThemeData( color: color ?? this.color, @@ -120,6 +130,8 @@ class PopupMenuThemeData with Diagnosticable { enableFeedback: enableFeedback ?? this.enableFeedback, mouseCursor: mouseCursor ?? this.mouseCursor, position: position ?? this.position, + iconColor: iconColor ?? this.iconColor, + iconSize: iconSize ?? this.iconSize, ); } @@ -143,6 +155,8 @@ class PopupMenuThemeData with Diagnosticable { enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, position: t < 0.5 ? a?.position : b?.position, + iconColor: Color.lerp(a?.iconColor, b?.iconColor, t), + iconSize: lerpDouble(a?.iconSize, b?.iconSize, t), ); } @@ -158,6 +172,8 @@ class PopupMenuThemeData with Diagnosticable { enableFeedback, mouseCursor, position, + iconColor, + iconSize, ); @override @@ -178,7 +194,9 @@ class PopupMenuThemeData with Diagnosticable { && other.labelTextStyle == labelTextStyle && other.enableFeedback == enableFeedback && other.mouseCursor == mouseCursor - && other.position == position; + && other.position == position + && other.iconColor == iconColor + && other.iconSize == iconSize; } @override @@ -194,6 +212,8 @@ class PopupMenuThemeData with Diagnosticable { properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(EnumProperty('position', position, defaultValue: null)); + properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); + properties.add(DoubleProperty('iconSize', iconSize, defaultValue: null)); } } @@ -205,8 +225,6 @@ class PopupMenuThemeData with Diagnosticable { class PopupMenuTheme extends InheritedTheme { /// Creates a popup menu theme that controls the configurations for /// popup menus in its widget subtree. - /// - /// The data argument must not be null. const PopupMenuTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index 8dc13967c147b..2a1cfa80e8f53 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -865,8 +865,21 @@ class RefreshProgressIndicator extends CircularProgressIndicator { super.semanticsLabel, super.semanticsValue, super.strokeCap, + this.elevation = 2.0, + this.indicatorMargin = const EdgeInsets.all(4.0), + this.indicatorPadding = const EdgeInsets.all(12.0), }); + /// {@macro flutter.material.material.elevation} + final double elevation; + + /// The amount of space by which to inset the whole indicator. + /// It accommodates the [elevation] of the indicator. + final EdgeInsetsGeometry indicatorMargin; + + /// The amount of space by which to inset the inner refresh indicator. + final EdgeInsetsGeometry indicatorPadding; + /// Default stroke width. static const double defaultStrokeWidth = 2.5; @@ -913,6 +926,10 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { // Last value received from the widget before null. double? _lastValue; + /// Force casting the widget as [RefreshProgressIndicator]. + @override + RefreshProgressIndicator get widget => super.widget as RefreshProgressIndicator; + // Always show the indeterminate version of the circular progress indicator. // // When value is non-null the sweep of the progress indicator arrow's arc @@ -973,13 +990,13 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { child: Container( width: _indicatorSize, height: _indicatorSize, - margin: const EdgeInsets.all(4.0), // accommodate the shadow + margin: widget.indicatorMargin, child: Material( type: MaterialType.circle, color: backgroundColor, - elevation: 2.0, + elevation: widget.elevation, child: Padding( - padding: const EdgeInsets.all(12.0), + padding: widget.indicatorPadding, child: Opacity( opacity: opacity, child: Transform.rotate( diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index 99677cfff7560..d8bb4961f1148 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -109,11 +109,12 @@ class RangeSlider extends StatefulWidget { /// Creates a Material Design range slider. /// /// The range slider widget itself does not maintain any state. Instead, when - /// the state of the slider changes, the widget calls the [onChanged] callback. - /// Most widgets that use a range slider will listen for the [onChanged] callback - /// and rebuild the slider with new [values] to update the visual appearance of - /// the slider. To know when the value starts to change, or when it is done - /// changing, set the optional callbacks [onChangeStart] and/or [onChangeEnd]. + /// the state of the slider changes, the widget calls the [onChanged] + /// callback. Most widgets that use a range slider will listen for the + /// [onChanged] callback and rebuild the slider with new [values] to update + /// the visual appearance of the slider. To know when the value starts to + /// change, or when it is done changing, set the optional callbacks + /// [onChangeStart] and/or [onChangeEnd]. /// /// * [values], which determines currently selected values for this range /// slider. @@ -128,11 +129,15 @@ class RangeSlider extends StatefulWidget { /// [inactiveColor] properties, although more fine-grained control of the /// appearance is achieved using a [SliderThemeData]. /// - /// The [values], [min], [max] must not be null. The [min] must be less than - /// or equal to the [max]. [values].start must be less than or equal to - /// [values].end. [values].start and [values].end must be greater than or - /// equal to the [min] and less than or equal to the [max]. The [divisions] - /// must be null or greater than 0. + /// The [min] must be less than or equal to the [max]. + /// + /// The [RangeValues.start] attribute of the [values] parameter must be less + /// than or equal to its [RangeValues.end] attribute. The [RangeValues.start] + /// and [RangeValues.end] attributes of the [values] parameter must be greater + /// than or equal to the [min] parameter and less than or equal to the [max] + /// parameter. + /// + /// The [divisions] parameter must be null or greater than zero. RangeSlider({ super.key, required this.values, @@ -484,10 +489,9 @@ class _RangeSliderState extends State with TickerProviderStateMixin enableController.dispose(); startPositionController.dispose(); endPositionController.dispose(); - if (overlayEntry != null) { - overlayEntry!.remove(); - overlayEntry = null; - } + overlayEntry?.remove(); + overlayEntry?.dispose(); + overlayEntry = null; super.dispose(); } @@ -842,8 +846,9 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix parent: _state.valueIndicatorController, curve: Curves.fastOutSlowIn, )..addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed && _state.overlayEntry != null) { - _state.overlayEntry!.remove(); + if (status == AnimationStatus.dismissed) { + _state.overlayEntry?.remove(); + _state.overlayEntry?.dispose(); _state.overlayEntry = null; } }); @@ -895,8 +900,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix late TapGestureRecognizer _tap; bool _active = false; late RangeValues _newValues; - late Offset _startThumbCenter; - late Offset _endThumbCenter; + Offset _startThumbCenter = Offset.zero; + Offset _endThumbCenter = Offset.zero; Rect? overlayStartRect; Rect? overlayEndRect; diff --git a/packages/flutter/lib/src/material/refresh_indicator.dart b/packages/flutter/lib/src/material/refresh_indicator.dart index 47791c416ecb4..0fed2b47849ed 100644 --- a/packages/flutter/lib/src/material/refresh_indicator.dart +++ b/packages/flutter/lib/src/material/refresh_indicator.dart @@ -275,6 +275,7 @@ class RefreshIndicatorState extends State with TickerProviderS late Future _pendingRefreshFuture; bool? _isIndicatorAtTop; double? _dragOffset; + late Color _effectiveValueColor = widget.color ?? Theme.of(context).colorScheme.primary; static final Animatable _threeQuarterTween = Tween(begin: 0.0, end: 0.75); static final Animatable _kDragSizeFactorLimitTween = Tween(begin: 0.0, end: _kDragSizeFactorLimit); @@ -293,15 +294,7 @@ class RefreshIndicatorState extends State with TickerProviderS @override void didChangeDependencies() { - final ThemeData theme = Theme.of(context); - _valueColor = _positionController.drive( - ColorTween( - begin: (widget.color ?? theme.colorScheme.primary).withOpacity(0.0), - end: (widget.color ?? theme.colorScheme.primary).withOpacity(1.0), - ).chain(CurveTween( - curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit), - )), - ); + _setupColorTween(); super.didChangeDependencies(); } @@ -309,15 +302,7 @@ class RefreshIndicatorState extends State with TickerProviderS void didUpdateWidget(covariant RefreshIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.color != widget.color) { - final ThemeData theme = Theme.of(context); - _valueColor = _positionController.drive( - ColorTween( - begin: (widget.color ?? theme.colorScheme.primary).withOpacity(0.0), - end: (widget.color ?? theme.colorScheme.primary).withOpacity(1.0), - ).chain(CurveTween( - curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit), - )), - ); + _setupColorTween(); } } @@ -328,6 +313,28 @@ class RefreshIndicatorState extends State with TickerProviderS super.dispose(); } + void _setupColorTween() { + // Reset the current value color. + _effectiveValueColor = widget.color ?? Theme.of(context).colorScheme.primary; + final Color color = _effectiveValueColor; + if (color.alpha == 0x00) { + // Set an always stopped animation instead of a driven tween. + _valueColor = AlwaysStoppedAnimation(color); + } else { + // Respect the alpha of the given color. + _valueColor = _positionController.drive( + ColorTween( + begin: color.withAlpha(0), + end: color.withAlpha(color.alpha), + ).chain( + CurveTween( + curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit), + ), + ), + ); + } + } + bool _shouldStart(ScrollNotification notification) { // If the notification.dragDetails is null, this scroll is not triggered by // user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll. @@ -448,7 +455,7 @@ class RefreshIndicatorState extends State with TickerProviderS newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit); } _positionController.value = clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds - if (_mode == _RefreshIndicatorMode.drag && _valueColor.value!.alpha == 0xFF) { + if (_mode == _RefreshIndicatorMode.drag && _valueColor.value!.alpha == _effectiveValueColor.alpha) { _mode = _RefreshIndicatorMode.armed; } } diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index a91f2a47e82bd..6a928e0da809d 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -5,6 +5,7 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'debug.dart'; @@ -76,6 +77,7 @@ class ReorderableListView extends StatefulWidget { this.onReorderStart, this.onReorderEnd, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, this.proxyDecorator, this.buildDefaultDragHandles = true, @@ -96,8 +98,10 @@ class ReorderableListView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, }) : assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ), assert( children.every((Widget w) => w.key != null), @@ -142,6 +146,7 @@ class ReorderableListView extends StatefulWidget { this.onReorderStart, this.onReorderEnd, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, this.proxyDecorator, this.buildDefaultDragHandles = true, @@ -163,8 +168,10 @@ class ReorderableListView extends StatefulWidget { this.autoScrollerVelocityScalar, }) : assert(itemCount >= 0), assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ); /// {@macro flutter.widgets.reorderable_list.itemBuilder} @@ -269,6 +276,9 @@ class ReorderableListView extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.itemExtentBuilder} + final ItemExtentBuilder? itemExtentBuilder; + /// {@macro flutter.widgets.list_view.prototypeItem} final Widget? prototypeItem; @@ -440,6 +450,7 @@ class _ReorderableListViewState extends State { sliver: SliverReorderableList( itemBuilder: _itemBuilder, itemExtent: widget.itemExtent, + itemExtentBuilder: widget.itemExtentBuilder, prototypeItem: widget.prototypeItem, itemCount: widget.itemCount, onReorder: widget.onReorder, diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index fbcd472801923..87fb31857eb8b 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -279,6 +279,12 @@ class ScaffoldMessengerState extends State with TickerProvide /// the SnackBar to be visible. /// /// {@tool dartpad} + /// Here is an example showing how to display a [SnackBar] with [showSnackBar] + /// + /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.0.dart ** + /// {@end-tool} + /// + /// {@tool dartpad} /// Here is an example showing that a floating [SnackBar] appears above [Scaffold.floatingActionButton]. /// /// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart ** @@ -1136,7 +1142,29 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { } final double snackBarYOffsetBase; - if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating) { + final bool showAboveFab = switch (currentFloatingActionButtonLocation) { + FloatingActionButtonLocation.startTop + || FloatingActionButtonLocation.centerTop + || FloatingActionButtonLocation.endTop + || FloatingActionButtonLocation.miniStartTop + || FloatingActionButtonLocation.miniCenterTop + || FloatingActionButtonLocation.miniEndTop => false, + FloatingActionButtonLocation.startDocked + || FloatingActionButtonLocation.startFloat + || FloatingActionButtonLocation.centerDocked + || FloatingActionButtonLocation.centerFloat + || FloatingActionButtonLocation.endContained + || FloatingActionButtonLocation.endDocked + || FloatingActionButtonLocation.endFloat + || FloatingActionButtonLocation.miniStartDocked + || FloatingActionButtonLocation.miniStartFloat + || FloatingActionButtonLocation.miniCenterDocked + || FloatingActionButtonLocation.miniCenterFloat + || FloatingActionButtonLocation.miniEndDocked + || FloatingActionButtonLocation.miniEndFloat => true, + FloatingActionButtonLocation() => true, + }; + if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating && showAboveFab) { snackBarYOffsetBase = floatingActionButtonRect.top; } else { // SnackBarBehavior.fixed applies a SafeArea automatically. @@ -1614,7 +1642,7 @@ class Scaffold extends StatefulWidget { /// This is useful if the app bar's [AppBar.backgroundColor] is not /// completely opaque. /// - /// This property is false by default. It must not be null. + /// This property is false by default. /// /// See also: /// @@ -2488,7 +2516,7 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto double get _floatingActionButtonVisibilityValue => _floatingActionButtonVisibilityController.value; /// Sets the current value of the visibility animation for the - /// [Scaffold.floatingActionButton]. This value must not be null. + /// [Scaffold.floatingActionButton]. set _floatingActionButtonVisibilityValue(double newValue) { _floatingActionButtonVisibilityController.value = clampDouble(newValue, _floatingActionButtonVisibilityController.lowerBound, @@ -2724,8 +2752,6 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto Color _bodyScrimColor = Colors.black; /// Whether to show a [ModalBarrier] over the body of the scaffold. - /// - /// The `value` parameter must not be null. void showBodyScrim(bool value, double opacity) { if (_showBodyScrim == value && _bodyScrimColor.opacity == opacity) { return; @@ -3052,8 +3078,6 @@ class ScaffoldFeatureController { /// curve specified with the [curve] argument, after the finger is released. In /// such a case, the value of [startingPoint] would be the progress of the /// animation at the time when the finger was released. -/// -/// The [startingPoint] and [curve] arguments must not be null. class _BottomSheetSuspendedCurve extends ParametricCurve { /// Creates a suspended curve. const _BottomSheetSuspendedCurve( diff --git a/packages/flutter/lib/src/material/scrollbar_theme.dart b/packages/flutter/lib/src/material/scrollbar_theme.dart index ac519a66b7a9e..5cee3bfd45414 100644 --- a/packages/flutter/lib/src/material/scrollbar_theme.dart +++ b/packages/flutter/lib/src/material/scrollbar_theme.dart @@ -172,8 +172,6 @@ class ScrollbarThemeData with Diagnosticable { /// Linearly interpolate between two Scrollbar themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static ScrollbarThemeData lerp(ScrollbarThemeData? a, ScrollbarThemeData? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/search.dart b/packages/flutter/lib/src/material/search.dart index e465c5492ebb0..507af6df90d55 100644 --- a/packages/flutter/lib/src/material/search.dart +++ b/packages/flutter/lib/src/material/search.dart @@ -377,6 +377,15 @@ abstract class SearchDelegate { } _SearchPageRoute? _route; + + /// Releases the resources. + @mustCallSuper + void dispose() { + _currentBodyNotifier.dispose(); + _focusNode?.dispose(); + _queryTextController.dispose(); + _proxyAnimation.parent = null; + } } /// Describes the body that is currently shown under the [AppBar] in the diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index e9b3037ffcfcb..32fcad2862a76 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -87,6 +87,19 @@ typedef ViewBuilder = Widget Function(Iterable suggestions); /// ** See code in examples/api/lib/material/search_anchor/search_anchor.1.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows how to fetch the search suggestions from a remote API. +/// +/// ** See code in examples/api/lib/material/search_anchor/search_anchor.3.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example demonstrates fetching the search suggestions asynchronously and +/// debouncing network calls. +/// +/// ** See code in examples/api/lib/material/search_anchor/search_anchor.4.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SearchBar], a widget that defines a search bar. @@ -113,6 +126,7 @@ class SearchAnchor extends StatefulWidget { this.headerHintStyle, this.dividerColor, this.viewConstraints, + this.textCapitalization, required this.builder, required this.suggestionsBuilder, }); @@ -128,8 +142,6 @@ class SearchAnchor extends StatefulWidget { /// /// ** See code in examples/api/lib/material/search_anchor/search_anchor.0.dart ** /// {@end-tool} - /// - /// The [suggestionsBuilder] argument must not be null. factory SearchAnchor.bar({ Widget? barLeading, Iterable? barTrailing, @@ -157,6 +169,7 @@ class SearchAnchor extends StatefulWidget { BoxConstraints? viewConstraints, bool? isFullScreen, SearchController searchController, + TextCapitalization textCapitalization, required SuggestionsBuilder suggestionsBuilder }) = _SearchAnchorWithSearchBar; @@ -273,12 +286,13 @@ class SearchAnchor extends StatefulWidget { /// ``` final BoxConstraints? viewConstraints; + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization? textCapitalization; + /// Called to create a widget which can open a search view route when it is tapped. /// /// The widget returned by this builder is faded out when it is tapped. /// At the same time a search view route is faded in. - /// - /// This must not be null. final SearchAnchorChildBuilder builder; /// Called to get the suggestion list for the search view. @@ -328,7 +342,8 @@ class _SearchAnchorState extends State { } void _openView() { - Navigator.of(context).push(_SearchViewRoute( + final NavigatorState navigator = Navigator.of(context); + navigator.push(_SearchViewRoute( viewLeading: widget.viewLeading, viewTrailing: widget.viewTrailing, viewHintText: widget.viewHintText, @@ -348,6 +363,8 @@ class _SearchAnchorState extends State { anchorKey: _anchorKey, searchController: _searchController, suggestionsBuilder: widget.suggestionsBuilder, + textCapitalization: widget.textCapitalization, + capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), )); } @@ -413,10 +430,12 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { this.viewHeaderHintStyle, this.dividerColor, this.viewConstraints, + this.textCapitalization, required this.showFullScreenView, required this.anchorKey, required this.searchController, required this.suggestionsBuilder, + required this.capturedThemes, }); final ValueGetter? toggleVisibility; @@ -434,10 +453,12 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { final TextStyle? viewHeaderHintStyle; final Color? dividerColor; final BoxConstraints? viewConstraints; + final TextCapitalization? textCapitalization; final bool showFullScreenView; final GlobalKey anchorKey; final SearchController searchController; final SuggestionsBuilder suggestionsBuilder; + final CapturedThemes capturedThemes; @override Color? get barrierColor => Colors.transparent; @@ -450,7 +471,6 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { late final SearchViewThemeData viewDefaults; late final SearchViewThemeData viewTheme; - late final DividerThemeData dividerTheme; final RectTween _rectTween = RectTween(); Rect? getRect() { @@ -485,7 +505,6 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { void updateViewConfig(BuildContext context) { viewDefaults = _SearchViewDefaultsM3(context, isFullScreen: showFullScreenView); viewTheme = SearchViewTheme.of(context); - dividerTheme = DividerTheme.of(context); } void updateTweens(BuildContext context) { @@ -559,29 +578,29 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { curve: _kViewFadeOnInterval, reverseCurve: _kViewFadeOnInterval.flipped, ), - child: _ViewContent( - viewLeading: viewLeading, - viewTrailing: viewTrailing, - viewHintText: viewHintText, - viewBackgroundColor: viewBackgroundColor, - viewElevation: viewElevation, - viewSurfaceTintColor: viewSurfaceTintColor, - viewSide: viewSide, - viewShape: viewShape, - viewHeaderTextStyle: viewHeaderTextStyle, - viewHeaderHintStyle: viewHeaderHintStyle, - dividerColor: dividerColor, - showFullScreenView: showFullScreenView, - animation: curvedAnimation, - topPadding: topPadding, - viewMaxWidth: _rectTween.end!.width, - viewRect: viewRect, - viewDefaults: viewDefaults, - viewTheme: viewTheme, - dividerTheme: dividerTheme, - viewBuilder: viewBuilder, - searchController: searchController, - suggestionsBuilder: suggestionsBuilder, + child: capturedThemes.wrap( + _ViewContent( + viewLeading: viewLeading, + viewTrailing: viewTrailing, + viewHintText: viewHintText, + viewBackgroundColor: viewBackgroundColor, + viewElevation: viewElevation, + viewSurfaceTintColor: viewSurfaceTintColor, + viewSide: viewSide, + viewShape: viewShape, + viewHeaderTextStyle: viewHeaderTextStyle, + viewHeaderHintStyle: viewHeaderHintStyle, + dividerColor: dividerColor, + showFullScreenView: showFullScreenView, + animation: curvedAnimation, + topPadding: topPadding, + viewMaxWidth: _rectTween.end!.width, + viewRect: viewRect, + viewBuilder: viewBuilder, + searchController: searchController, + suggestionsBuilder: suggestionsBuilder, + textCapitalization: textCapitalization, + ), ), ); } @@ -607,14 +626,12 @@ class _ViewContent extends StatefulWidget { this.viewHeaderTextStyle, this.viewHeaderHintStyle, this.dividerColor, + this.textCapitalization, required this.showFullScreenView, required this.topPadding, required this.animation, required this.viewMaxWidth, required this.viewRect, - required this.viewDefaults, - required this.viewTheme, - required this.dividerTheme, required this.searchController, required this.suggestionsBuilder, }); @@ -631,14 +648,12 @@ class _ViewContent extends StatefulWidget { final TextStyle? viewHeaderTextStyle; final TextStyle? viewHeaderHintStyle; final Color? dividerColor; + final TextCapitalization? textCapitalization; final bool showFullScreenView; final double topPadding; final Animation animation; final double viewMaxWidth; final Rect viewRect; - final SearchViewThemeData viewDefaults; - final SearchViewThemeData viewTheme; - final DividerThemeData dividerTheme; final SearchController searchController; final SuggestionsBuilder suggestionsBuilder; @@ -727,39 +742,43 @@ class _ViewContentState extends State<_ViewContent> { ), ]; + final SearchViewThemeData viewDefaults = _SearchViewDefaultsM3(context, isFullScreen: widget.showFullScreenView); + final SearchViewThemeData viewTheme = SearchViewTheme.of(context); + final DividerThemeData dividerTheme = DividerTheme.of(context); + final Color effectiveBackgroundColor = widget.viewBackgroundColor - ?? widget.viewTheme.backgroundColor - ?? widget.viewDefaults.backgroundColor!; + ?? viewTheme.backgroundColor + ?? viewDefaults.backgroundColor!; final Color effectiveSurfaceTint = widget.viewSurfaceTintColor - ?? widget.viewTheme.surfaceTintColor - ?? widget.viewDefaults.surfaceTintColor!; + ?? viewTheme.surfaceTintColor + ?? viewDefaults.surfaceTintColor!; final double effectiveElevation = widget.viewElevation - ?? widget.viewTheme.elevation - ?? widget.viewDefaults.elevation!; + ?? viewTheme.elevation + ?? viewDefaults.elevation!; final BorderSide? effectiveSide = widget.viewSide - ?? widget.viewTheme.side - ?? widget.viewDefaults.side; + ?? viewTheme.side + ?? viewDefaults.side; OutlinedBorder effectiveShape = widget.viewShape - ?? widget.viewTheme.shape - ?? widget.viewDefaults.shape!; + ?? viewTheme.shape + ?? viewDefaults.shape!; if (effectiveSide != null) { effectiveShape = effectiveShape.copyWith(side: effectiveSide); } final Color effectiveDividerColor = widget.dividerColor - ?? widget.viewTheme.dividerColor - ?? widget.dividerTheme.color - ?? widget.viewDefaults.dividerColor!; + ?? viewTheme.dividerColor + ?? dividerTheme.color + ?? viewDefaults.dividerColor!; final TextStyle? effectiveTextStyle = widget.viewHeaderTextStyle - ?? widget.viewTheme.headerTextStyle - ?? widget.viewDefaults.headerTextStyle; + ?? viewTheme.headerTextStyle + ?? viewDefaults.headerTextStyle; final TextStyle? effectiveHintStyle = widget.viewHeaderHintStyle - ?? widget.viewTheme.headerHintStyle + ?? viewTheme.headerHintStyle ?? widget.viewHeaderTextStyle - ?? widget.viewTheme.headerTextStyle - ?? widget.viewDefaults.headerHintStyle; + ?? viewTheme.headerTextStyle + ?? viewDefaults.headerHintStyle; final Widget viewDivider = DividerTheme( - data: widget.dividerTheme.copyWith(color: effectiveDividerColor), + data: dividerTheme.copyWith(color: effectiveDividerColor), child: const Divider(height: 1), ); @@ -811,6 +830,7 @@ class _ViewContentState extends State<_ViewContent> { onChanged: (_) { updateSuggestions(); }, + textCapitalization: widget.textCapitalization, ), ), ), @@ -871,6 +891,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { super.viewConstraints, super.isFullScreen, super.searchController, + super.textCapitalization, required super.suggestionsBuilder }) : super( viewHintText: viewHintText ?? barHintText, @@ -898,6 +919,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor { padding: barPadding ?? const MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 16.0)), leading: barLeading ?? const Icon(Icons.search), trailing: barTrailing, + textCapitalization: textCapitalization, ); } ); @@ -1009,6 +1031,7 @@ class SearchBar extends StatefulWidget { this.padding, this.textStyle, this.hintStyle, + this.textCapitalization, }); /// Controls the text being edited in the search bar's text field. @@ -1127,6 +1150,9 @@ class SearchBar extends StatefulWidget { /// The default text color is [ColorScheme.onSurfaceVariant]. final MaterialStateProperty? hintStyle; + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization? textCapitalization; + @override State createState() => _SearchBarState(); } @@ -1148,6 +1174,9 @@ class _SearchBarState extends State { @override void dispose() { _internalStatesController.dispose(); + if (widget.focusNode == null) { + _focusNode.dispose(); + } super.dispose(); } @@ -1177,6 +1206,7 @@ class _SearchBarState extends State { final BorderSide? effectiveSide = resolve(widget.side, searchBarTheme.side, defaults.side); final EdgeInsetsGeometry? effectivePadding = resolve(widget.padding, searchBarTheme.padding, defaults.padding); final MaterialStateProperty? effectiveOverlayColor = widget.overlayColor ?? searchBarTheme.overlayColor ?? defaults.overlayColor; + final TextCapitalization effectiveTextCapitalization = widget.textCapitalization ?? searchBarTheme.textCapitalization ?? defaults.textCapitalization!; final Set states = _internalStatesController.value; final TextStyle? effectiveHintStyle = widget.hintStyle?.resolve(states) @@ -1260,6 +1290,7 @@ class _SearchBarState extends State { // smaller than 48.0 isDense: true, )), + textCapitalization: effectiveTextCapitalization, ), ), ) @@ -1340,6 +1371,9 @@ class _SearchBarDefaultsM3 extends SearchBarThemeData { @override BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0); + + @override + TextCapitalization get textCapitalization => TextCapitalization.none; } // END GENERATED TOKEN PROPERTIES - SearchBar diff --git a/packages/flutter/lib/src/material/search_bar_theme.dart b/packages/flutter/lib/src/material/search_bar_theme.dart index c738d0e9e7813..94e225dd0ccd9 100644 --- a/packages/flutter/lib/src/material/search_bar_theme.dart +++ b/packages/flutter/lib/src/material/search_bar_theme.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import '../../services.dart'; import 'material_state.dart'; import 'theme.dart'; @@ -47,6 +48,7 @@ class SearchBarThemeData with Diagnosticable { this.textStyle, this.hintStyle, this.constraints, + this.textCapitalization, }); /// Overrides the default value of the [SearchBar.elevation]. @@ -82,6 +84,9 @@ class SearchBarThemeData with Diagnosticable { /// Overrides the value of size constraints for [SearchBar]. final BoxConstraints? constraints; + /// Overrides the value of [SearchBar.textCapitalization]. + final TextCapitalization? textCapitalization; + /// Creates a copy of this object but with the given fields replaced with the /// new values. SearchBarThemeData copyWith({ @@ -96,6 +101,7 @@ class SearchBarThemeData with Diagnosticable { MaterialStateProperty? textStyle, MaterialStateProperty? hintStyle, BoxConstraints? constraints, + TextCapitalization? textCapitalization, }) { return SearchBarThemeData( elevation: elevation ?? this.elevation, @@ -109,6 +115,7 @@ class SearchBarThemeData with Diagnosticable { textStyle: textStyle ?? this.textStyle, hintStyle: hintStyle ?? this.hintStyle, constraints: constraints ?? this.constraints, + textCapitalization: textCapitalization ?? this.textCapitalization, ); } @@ -131,6 +138,7 @@ class SearchBarThemeData with Diagnosticable { textStyle: MaterialStateProperty.lerp(a?.textStyle, b?.textStyle, t, TextStyle.lerp), hintStyle: MaterialStateProperty.lerp(a?.hintStyle, b?.hintStyle, t, TextStyle.lerp), constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), + textCapitalization: t < 0.5 ? a?.textCapitalization : b?.textCapitalization, ); } @@ -147,6 +155,7 @@ class SearchBarThemeData with Diagnosticable { textStyle, hintStyle, constraints, + textCapitalization, ); @override @@ -168,7 +177,8 @@ class SearchBarThemeData with Diagnosticable { && other.padding == padding && other.textStyle == textStyle && other.hintStyle == hintStyle - && other.constraints == constraints; + && other.constraints == constraints + && other.textCapitalization == textCapitalization; } @override @@ -185,6 +195,7 @@ class SearchBarThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('textStyle', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty>('hintStyle', hintStyle, defaultValue: null)); properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: null)); + properties.add(DiagnosticsProperty('textCapitalization', textCapitalization, defaultValue: null)); } // Special case because BorderSide.lerp() doesn't support null arguments diff --git a/packages/flutter/lib/src/material/search_view_theme.dart b/packages/flutter/lib/src/material/search_view_theme.dart index ec893000c2e83..db583be66c5d3 100644 --- a/packages/flutter/lib/src/material/search_view_theme.dart +++ b/packages/flutter/lib/src/material/search_view_theme.dart @@ -187,7 +187,7 @@ class SearchViewThemeData with Diagnosticable { /// /// * [SearchViewThemeData], which describes the actual configuration of a search view /// theme. -class SearchViewTheme extends InheritedWidget { +class SearchViewTheme extends InheritedTheme { /// Creates a const theme that controls the configurations for the search view /// created by the [SearchAnchor] widget. const SearchViewTheme({ @@ -212,6 +212,11 @@ class SearchViewTheme extends InheritedWidget { return searchViewTheme?.data ?? Theme.of(context).searchViewTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + return SearchViewTheme(data: data, child: child); + } + @override bool updateShouldNotify(SearchViewTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/segmented_button.dart b/packages/flutter/lib/src/material/segmented_button.dart index 9e056b37d1f46..df5ab3a6d8e9a 100644 --- a/packages/flutter/lib/src/material/segmented_button.dart +++ b/packages/flutter/lib/src/material/segmented_button.dart @@ -21,7 +21,7 @@ import 'tooltip.dart'; /// Data describing a segment of a [SegmentedButton]. class ButtonSegment { - /// Construct a SegmentData + /// Construct a [ButtonSegment]. /// /// One of [icon] or [label] must be non-null. const ButtonSegment({ @@ -96,7 +96,7 @@ class ButtonSegment { /// [ToggleButtons]. /// * [Radio], an alternative way to present the user with a mutually exclusive set of options. /// * [FilterChip], [ChoiceChip], which can be used when you need to show more than five options. -class SegmentedButton extends StatelessWidget { +class SegmentedButton extends StatefulWidget { /// Creates a const [SegmentedButton]. /// /// [segments] must contain at least one segment, but it is recommended @@ -235,27 +235,53 @@ class SegmentedButton extends StatelessWidget { /// Defaults to an [Icon] with [Icons.check]. final Widget? selectedIcon; - bool get _enabled => onSelectionChanged != null; + @override + State> createState() => SegmentedButtonState(); +} + +/// State for [SegmentedButton]. +@visibleForTesting +class SegmentedButtonState extends State> { + bool get _enabled => widget.onSelectionChanged != null; + + /// Controllers for the [ButtonSegment]s. + @visibleForTesting + final Map, MaterialStatesController> statesControllers = , MaterialStatesController>{}; + + @override + void didUpdateWidget(covariant SegmentedButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget != widget) { + statesControllers.removeWhere((ButtonSegment segment, MaterialStatesController controller) { + if (widget.segments.contains(segment)) { + return false; + } else { + controller.dispose(); + return true; + } + }); + } + } void _handleOnPressed(T segmentValue) { if (!_enabled) { return; } - final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue); - final bool validChange = emptySelectionAllowed || !onlySelectedSegment; + final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue); + final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment; if (validChange) { - final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment); + final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment); final Set pressedSegment = {segmentValue}; late final Set updatedSelection; if (toggle) { - updatedSelection = selected.contains(segmentValue) - ? selected.difference(pressedSegment) - : selected.union(pressedSegment); + updatedSelection = widget.selected.contains(segmentValue) + ? widget.selected.difference(pressedSegment) + : widget.selected.union(pressedSegment); } else { updatedSelection = pressedSegment; } - if (!setEquals(updatedSelection, selected)) { - onSelectionChanged!(updatedSelection); + if (!setEquals(updatedSelection, widget.selected)) { + widget.onSelectionChanged!(updatedSelection); } } } @@ -271,7 +297,7 @@ class SegmentedButton extends StatelessWidget { final Set currentState = _enabled ? enabledState : disabledState; P? effectiveValue

    (P? Function(ButtonStyle? style) getProperty) { - late final P? widgetValue = getProperty(style); + late final P? widgetValue = getProperty(widget.style); late final P? themeValue = getProperty(theme.style); late final P? defaultValue = getProperty(defaults.style); return widgetValue ?? themeValue ?? defaultValue; @@ -305,25 +331,24 @@ class SegmentedButton extends StatelessWidget { ); } - final ButtonStyle segmentStyle = segmentStyleFor(style); + final ButtonStyle segmentStyle = segmentStyleFor(widget.style); final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style)); - final Widget? selectedIcon = showSelectedIcon - ? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon + final Widget? selectedIcon = widget.showSelectedIcon + ? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon : null; Widget buttonFor(ButtonSegment segment) { final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink(); - final bool segmentSelected = selected.contains(segment.value); - final Widget? icon = (segmentSelected && showSelectedIcon) + final bool segmentSelected = widget.selected.contains(segment.value); + final Widget? icon = (segmentSelected && widget.showSelectedIcon) ? selectedIcon : segment.label != null ? segment.icon : null; - final MaterialStatesController controller = MaterialStatesController( - { + final MaterialStatesController controller = statesControllers.putIfAbsent(segment, () => MaterialStatesController()); + controller.value = { if (segmentSelected) MaterialState.selected, - } - ); + }; final Widget button = icon != null ? TextButton.icon( @@ -350,7 +375,7 @@ class SegmentedButton extends StatelessWidget { return MergeSemantics( child: Semantics( checked: segmentSelected, - inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true, + inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true, child: buttonWithTooltip, ), ); @@ -363,7 +388,7 @@ class SegmentedButton extends StatelessWidget { final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide); final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide); - final List buttons = segments.map(buttonFor).toList(); + final List buttons = widget.segments.map(buttonFor).toList(); return Material( type: MaterialType.transparency, @@ -374,7 +399,7 @@ class SegmentedButton extends StatelessWidget { child: TextButtonTheme( data: TextButtonThemeData(style: segmentThemeStyle), child: _SegmentedButtonRenderWidget( - segments: segments, + segments: widget.segments, enabledBorder: _enabled ? enabledBorder : disabledBorder, disabledBorder: disabledBorder, direction: direction, @@ -383,6 +408,14 @@ class SegmentedButton extends StatelessWidget { ), ); } + + @override + void dispose() { + for (final MaterialStatesController controller in statesControllers.values) { + controller.dispose(); + } + super.dispose(); + } } class _SegmentedButtonRenderWidget extends MultiChildRenderObjectWidget { const _SegmentedButtonRenderWidget({ diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart index 433de56181c54..e97441e7a80a9 100644 --- a/packages/flutter/lib/src/material/selectable_text.dart +++ b/packages/flutter/lib/src/material/selectable_text.dart @@ -174,9 +174,9 @@ class SelectableText extends StatefulWidget { /// closest enclosing [DefaultTextStyle]. /// - /// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle], - /// [selectionWidthStyle] and [data] parameters must not be null. If specified, - /// the [maxLines] argument must be greater than zero. + /// If the [showCursor], [autofocus], [dragStartBehavior], + /// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are + /// specified, the [maxLines] argument must be greater than zero. const SelectableText( String this.data, { super.key, @@ -185,7 +185,13 @@ class SelectableText extends StatefulWidget { this.strutStyle, this.textAlign, this.textDirection, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) this.textScaleFactor, + this.textScaler, this.showCursor = false, this.autofocus = false, @Deprecated( @@ -218,14 +224,16 @@ class SelectableText extends StatefulWidget { (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), + assert( + textScaler == null || textScaleFactor == null, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ), textSpan = null; /// Creates a selectable text widget with a [TextSpan]. /// - /// The [textSpan] parameter must not be null and only contain [TextSpan] in - /// [textSpan].children. Other type of [InlineSpan] is not allowed. - /// - /// The [autofocus] and [dragStartBehavior] arguments must not be null. + /// The [TextSpan.children] attribute of the [textSpan] parameter must only + /// contain [TextSpan]s. Other types of [InlineSpan] are not allowed. const SelectableText.rich( TextSpan this.textSpan, { super.key, @@ -234,7 +242,13 @@ class SelectableText extends StatefulWidget { this.strutStyle, this.textAlign, this.textDirection, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) this.textScaleFactor, + this.textScaler, this.showCursor = false, this.autofocus = false, @Deprecated( @@ -267,6 +281,10 @@ class SelectableText extends StatefulWidget { (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", ), + assert( + textScaler == null || textScaleFactor == null, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ), data = null; /// The text to display. @@ -322,8 +340,16 @@ class SelectableText extends StatefulWidget { final TextDirection? textDirection; /// {@macro flutter.widgets.editableText.textScaleFactor} + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) final double? textScaleFactor; + /// {@macro flutter.painting.textPainter.textScaler} + final TextScaler? textScaler; + /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; @@ -457,6 +483,7 @@ class SelectableText extends StatefulWidget { properties.add(EnumProperty('textAlign', textAlign, defaultValue: null)); properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); + properties.add(DiagnosticsProperty('textScaler', textScaler, defaultValue: null)); properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); @@ -509,6 +536,7 @@ class _SelectableTextState extends State implements TextSelectio super.didUpdateWidget(oldWidget); if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { _controller.removeListener(_onControllerChanged); + _controller.dispose(); _controller = _TextSpanEditingController( textSpan: widget.textSpan ?? TextSpan(text: widget.data), ); @@ -671,6 +699,10 @@ class _SelectableTextState extends State implements TextSelectio if (effectiveTextStyle == null || effectiveTextStyle.inherit) { effectiveTextStyle = defaultTextStyle.style.merge(widget.style ?? _controller._textSpan.style); } + final TextScaler? effectiveScaler = widget.textScaler ?? switch (widget.textScaleFactor) { + null => null, + final double textScaleFactor => TextScaler.linear(textScaleFactor), + }; final Widget child = RepaintBoundary( child: EditableText( key: editableTextKey, @@ -686,7 +718,7 @@ class _SelectableTextState extends State implements TextSelectio strutStyle: widget.strutStyle ?? const StrutStyle(), textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textDirection: widget.textDirection, - textScaleFactor: widget.textScaleFactor, + textScaler: effectiveScaler, autofocus: widget.autofocus, forceLine: false, minLines: widget.minLines, diff --git a/packages/flutter/lib/src/material/shaders/ink_sparkle.frag b/packages/flutter/lib/src/material/shaders/ink_sparkle.frag index aa82583921fbb..f9f3ce6be3dd5 100644 --- a/packages/flutter/lib/src/material/shaders/ink_sparkle.frag +++ b/packages/flutter/lib/src/material/shaders/ink_sparkle.frag @@ -11,22 +11,19 @@ precision highp float; // TODO(antrob): Put these in a more logical order (e.g. separate consts vs varying, etc) layout(location = 0) uniform vec4 u_color; -layout(location = 1) uniform float u_alpha; -layout(location = 2) uniform vec4 u_sparkle_color; -layout(location = 3) uniform float u_sparkle_alpha; -layout(location = 4) uniform float u_blur; -layout(location = 5) uniform vec2 u_center; -layout(location = 6) uniform float u_radius_scale; -layout(location = 7) uniform float u_max_radius; -layout(location = 8) uniform vec2 u_resolution_scale; -layout(location = 9) uniform vec2 u_noise_scale; -layout(location = 10) uniform float u_noise_phase; -layout(location = 11) uniform vec2 u_circle1; -layout(location = 12) uniform vec2 u_circle2; -layout(location = 13) uniform vec2 u_circle3; -layout(location = 14) uniform vec2 u_rotation1; -layout(location = 15) uniform vec2 u_rotation2; -layout(location = 16) uniform vec2 u_rotation3; +// u_alpha, u_sparkle_alpha, u_blur, u_radius_scale +layout(location = 1) uniform vec4 u_composite_1; +layout(location = 2) uniform vec2 u_center; +layout(location = 3) uniform float u_max_radius; +layout(location = 4) uniform vec2 u_resolution_scale; +layout(location = 5) uniform vec2 u_noise_scale; +layout(location = 6) uniform float u_noise_phase; +layout(location = 7) uniform vec2 u_circle1; +layout(location = 8) uniform vec2 u_circle2; +layout(location = 9) uniform vec2 u_circle3; +layout(location = 10) uniform vec2 u_rotation1; +layout(location = 11) uniform vec2 u_rotation2; +layout(location = 12) uniform vec2 u_rotation3; layout(location = 0) out vec4 fragColor; @@ -36,6 +33,11 @@ const float PI_ROTATE_LEFT = PI * -0.0078125; const float ONE_THIRD = 1./3.; const vec2 TURBULENCE_SCALE = vec2(0.8); +float u_alpha = u_composite_1.x; +float u_sparkle_alpha = u_composite_1.y; +float u_blur = u_composite_1.z; +float u_radius_scale = u_composite_1.w; + float triangle_noise(highp vec2 n) { n = fract(n * vec2(5.3987, 5.4421)); n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); @@ -99,6 +101,5 @@ void main() { float sparkle = sparkle(density_uv, u_noise_phase) * ring * turbulence * u_sparkle_alpha; float wave_alpha = soft_circle(p, u_center, radius, u_blur) * u_alpha * u_color.a; vec4 wave_color = vec4(u_color.rgb * wave_alpha, wave_alpha); - vec4 sparkle_color = vec4(u_sparkle_color.rgb * u_sparkle_color.a, u_sparkle_color.a); - fragColor = mix(wave_color, sparkle_color, sparkle); + fragColor = mix(wave_color, vec4(1.0), sparkle); } diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 6680aaa144b17..c28a03bec17cb 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -654,10 +654,9 @@ class _SliderState extends State with TickerProviderStateMixin { valueIndicatorController.dispose(); enableController.dispose(); positionController.dispose(); - if (overlayEntry != null) { - overlayEntry!.remove(); - overlayEntry = null; - } + overlayEntry?.remove(); + overlayEntry?.dispose(); + overlayEntry = null; _focusNode?.dispose(); super.dispose(); } @@ -889,8 +888,8 @@ class _SliderState extends State with TickerProviderStateMixin { // This needs to be updated when accessibility // guidelines are available on the material specs page // https://m3.material.io/components/sliders/accessibility. - ? math.min(MediaQuery.textScaleFactorOf(context), 1.3) - : MediaQuery.textScaleFactorOf(context); + ? MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.3).textScaleFactor + : MediaQuery.textScalerOf(context).textScaleFactor; return Semantics( container: true, @@ -1116,8 +1115,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { parent: _state.valueIndicatorController, curve: Curves.fastOutSlowIn, )..addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed && _state.overlayEntry != null) { - _state.overlayEntry!.remove(); + if (status == AnimationStatus.dismissed) { + _state.overlayEntry?.remove(); + _state.overlayEntry?.dispose(); _state.overlayEntry = null; } }); @@ -1484,6 +1484,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { } void _startInteraction(Offset globalPosition) { + if (!_state.mounted) { + return; + } _state.showValueIndicator(); if (!_active && isInteractive) { switch (allowedInteraction) { diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 492de070e7288..18648fe459367 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -49,8 +49,6 @@ import 'theme.dart'; /// the [RangeSlider]'s tick marks. class SliderTheme extends InheritedTheme { /// Applies the given theme [data] to [child]. - /// - /// The [data] and [child] arguments must not be null. const SliderTheme({ super.key, required this.data, @@ -655,8 +653,6 @@ class SliderThemeData with Diagnosticable { /// Linearly interpolate between two slider themes. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) { if (identical(a, b)) { diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 5bed2ab347ce5..85df795b313cd 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -83,8 +83,6 @@ enum SnackBarClosedReason { /// * class SnackBarAction extends StatefulWidget { /// Creates an action for a [SnackBar]. - /// - /// The [label] and [onPressed] arguments must be non-null. const SnackBarAction({ super.key, this.textColor, @@ -127,7 +125,7 @@ class SnackBarAction extends StatefulWidget { /// The button label. final String label; - /// The callback to be called when the button is pressed. Must not be null. + /// The callback to be called when the button is pressed. /// /// This callback will be called at most once each time this action is /// displayed in a [SnackBar]. @@ -250,6 +248,13 @@ class _SnackBarActionState extends State { /// ** See code in examples/api/lib/material/snack_bar/snack_bar.1.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example demonstrates the various [SnackBar] widget components, +/// including an optional icon, in either floating or fixed format. +/// +/// ** See code in examples/api/lib/material/snack_bar/snack_bar.2.dart ** +/// {@end-tool} +/// /// See also: /// /// * [ScaffoldMessenger.of], to obtain the current [ScaffoldMessengerState], @@ -265,8 +270,7 @@ class _SnackBarActionState extends State { class SnackBar extends StatefulWidget { /// Creates a snack bar. /// - /// The [content] argument must be non-null. The [elevation] must be null or - /// non-negative. + /// The [elevation] must be null or non-negative. const SnackBar({ super.key, required this.content, @@ -276,6 +280,7 @@ class SnackBar extends StatefulWidget { this.padding, this.width, this.shape, + this.hitTestBehavior, this.behavior, this.action, this.actionOverflowThreshold, @@ -324,6 +329,8 @@ class SnackBar extends StatefulWidget { /// If this property is null, then [SnackBarThemeData.insetPadding] of /// [ThemeData.snackBarTheme] is used. If that is also null, then the default is /// `EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0)`. + /// + /// If this property is not null and [hitTestBehavior] is null, then [hitTestBehavior] default is [HitTestBehavior.deferToChild]. final EdgeInsetsGeometry? margin; /// The amount of padding to apply to the snack bar's content and optional @@ -377,6 +384,13 @@ class SnackBar extends StatefulWidget { /// circular corner radius of 4.0. final ShapeBorder? shape; + /// Defines how the snack bar area, including margin, will behave during hit testing. + /// + /// If this property is null and [margin] is not null, then [HitTestBehavior.deferToChild] is used by default. + /// + /// Please refer to [HitTestBehavior] for a detailed explanation of every behavior. + final HitTestBehavior? hitTestBehavior; + /// This defines the behavior and location of the snack bar. /// /// Defines where a [SnackBar] should appear within a [Scaffold] and how its @@ -449,12 +463,12 @@ class SnackBar extends StatefulWidget { /// The direction in which the SnackBar can be dismissed. /// - /// Cannot be null, defaults to [DismissDirection.down]. + /// Defaults to [DismissDirection.down]. final DismissDirection dismissDirection; /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; // API for ScaffoldMessengerState.showSnackBar(): @@ -482,6 +496,7 @@ class SnackBar extends StatefulWidget { padding: padding, width: width, shape: shape, + hitTestBehavior: hitTestBehavior, behavior: behavior, action: action, actionOverflowThreshold: actionOverflowThreshold, @@ -769,6 +784,7 @@ class _SnackBarState extends State { key: const Key('dismissible'), direction: widget.dismissDirection, resizeDuration: null, + behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque), onDismissed: (DismissDirection direction) { ScaffoldMessenger.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe); }, diff --git a/packages/flutter/lib/src/material/snack_bar_theme.dart b/packages/flutter/lib/src/material/snack_bar_theme.dart index 5f976148d70fc..938c0b9e83515 100644 --- a/packages/flutter/lib/src/material/snack_bar_theme.dart +++ b/packages/flutter/lib/src/material/snack_bar_theme.dart @@ -17,14 +17,17 @@ enum SnackBarBehavior { /// Fixes the [SnackBar] at the bottom of the [Scaffold]. /// /// The exception is that the [SnackBar] will be shown above a - /// [BottomNavigationBar]. Additionally, the [SnackBar] will cause other - /// non-fixed widgets inside [Scaffold] to be pushed above (for example, the - /// [FloatingActionButton]). + /// [BottomNavigationBar] or a [NavigationBar]. Additionally, the [SnackBar] + /// will cause other non-fixed widgets inside [Scaffold] to be pushed above + /// (for example, the [FloatingActionButton]). fixed, /// This behavior will cause [SnackBar] to be shown above other widgets in the - /// [Scaffold]. This includes being displayed above a [BottomNavigationBar] - /// and a [FloatingActionButton]. + /// [Scaffold]. This includes being displayed above a [BottomNavigationBar] or + /// a [NavigationBar], and a [FloatingActionButton] when its location is on the + /// bottom. When the floating action button location is on the top, this behavior + /// will cause the [SnackBar] to be shown above other widgets in the [Scaffold] + /// except the floating action button. /// /// See for more details. floating, @@ -193,8 +196,6 @@ class SnackBarThemeData with Diagnosticable { /// Linearly interpolate between two SnackBar Themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static SnackBarThemeData lerp(SnackBarThemeData? a, SnackBarThemeData? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index b6ab24a64a74c..470fe0975f91c 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -134,8 +134,6 @@ const double _kTriangleHeight = _kStepSize * 0.866025; // Triangle height. sqrt( @immutable class Step { /// Creates a step for a [Stepper]. - /// - /// The [title], [content], and [state] arguments must not be null. const Step({ required this.title, this.subtitle, @@ -197,8 +195,6 @@ class Stepper extends StatefulWidget { /// This widget is not meant to be rebuilt with a different list of steps /// unless a key is provided in order to distinguish the old stepper from the /// new one. - /// - /// The [steps], [type], and [currentStep] arguments must not be null. const Stepper({ super.key, required this.steps, diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 0cfc6ff81550e..975bbb54c820f 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -69,6 +69,13 @@ enum _SwitchType { material, adaptive } /// ** See code in examples/api/lib/material/switch/switch.2.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows how to use the ambient [CupertinoThemeData] to style all +/// widgets which would otherwise use iOS defaults. +/// +/// ** See code in examples/api/lib/material/switch/switch.3.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SwitchListTile], which combines this widget with a [ListTile] so that @@ -171,8 +178,6 @@ class Switch extends StatelessWidget { _switchType = _SwitchType.adaptive; /// Whether this switch is on or off. - /// - /// This property must not be null. final bool value; /// Called when the user toggles the switch on or off. @@ -705,7 +710,7 @@ class _MaterialSwitch extends StatefulWidget { final MaterialStateProperty? overlayColor; final double? splashRadius; final FocusNode? focusNode; - final Function(bool)? onFocusChange; + final ValueChanged? onFocusChange; final bool autofocus; final Size size; diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index be09ce380c799..3ee9a39071c1d 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.dart @@ -261,8 +261,6 @@ class SwitchListTile extends StatelessWidget { assert(inactiveThumbImage != null || onInactiveThumbImageError == null); /// Whether this switch is checked. - /// - /// This property must not be null. final bool value; /// Called when the user toggles the switch on or off. diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart index 87c43d7acf3bc..1f45037ad0ea8 100644 --- a/packages/flutter/lib/src/material/tab_bar_theme.dart +++ b/packages/flutter/lib/src/material/tab_bar_theme.dart @@ -32,6 +32,7 @@ class TabBarTheme with Diagnosticable { this.indicatorColor, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelPadding, this.labelStyle, @@ -55,6 +56,9 @@ class TabBarTheme with Diagnosticable { /// Overrides the default value for [TabBar.dividerColor]. final Color? dividerColor; + /// Overrides the default value for [TabBar.dividerHeight]. + final double? dividerHeight; + /// Overrides the default value for [TabBar.labelColor]. /// /// If [labelColor] is a [MaterialStateColor], then the effective color will @@ -101,6 +105,7 @@ class TabBarTheme with Diagnosticable { Color? indicatorColor, TabBarIndicatorSize? indicatorSize, Color? dividerColor, + double? dividerHeight, Color? labelColor, EdgeInsetsGeometry? labelPadding, TextStyle? labelStyle, @@ -116,6 +121,7 @@ class TabBarTheme with Diagnosticable { indicatorColor: indicatorColor ?? this.indicatorColor, indicatorSize: indicatorSize ?? this.indicatorSize, dividerColor: dividerColor ?? this.dividerColor, + dividerHeight: dividerHeight ?? this.dividerHeight, labelColor: labelColor ?? this.labelColor, labelPadding: labelPadding ?? this.labelPadding, labelStyle: labelStyle ?? this.labelStyle, @@ -135,8 +141,6 @@ class TabBarTheme with Diagnosticable { /// Linearly interpolate between two tab bar themes. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static TabBarTheme lerp(TabBarTheme a, TabBarTheme b, double t) { if (identical(a, b)) { @@ -147,6 +151,7 @@ class TabBarTheme with Diagnosticable { indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t), indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize, dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t), + dividerHeight: t < 0.5 ? a.dividerHeight : b.dividerHeight, labelColor: Color.lerp(a.labelColor, b.labelColor, t), labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t), labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t), @@ -165,6 +170,7 @@ class TabBarTheme with Diagnosticable { indicatorColor, indicatorSize, dividerColor, + dividerHeight, labelColor, labelPadding, labelStyle, @@ -189,6 +195,7 @@ class TabBarTheme with Diagnosticable { && other.indicatorColor == indicatorColor && other.indicatorSize == indicatorSize && other.dividerColor == dividerColor + && other.dividerHeight == dividerHeight && other.labelColor == labelColor && other.labelPadding == labelPadding && other.labelStyle == labelStyle diff --git a/packages/flutter/lib/src/material/tab_controller.dart b/packages/flutter/lib/src/material/tab_controller.dart index 0229fe17c418e..507b74288e86b 100644 --- a/packages/flutter/lib/src/material/tab_controller.dart +++ b/packages/flutter/lib/src/material/tab_controller.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'constants.dart'; @@ -95,12 +96,12 @@ class TabController extends ChangeNotifier { /// Creates an object that manages the state required by [TabBar] and a /// [TabBarView]. /// - /// The [length] must not be null or negative. Typically it's a value greater - /// than one, i.e. typically there are two or more tabs. The [length] must - /// match [TabBar.tabs]'s and [TabBarView.children]'s length. + /// The [length] must not be negative. Typically it's a value greater than + /// one, i.e. typically there are two or more tabs. The [length] must match + /// [TabBar.tabs]'s and [TabBarView.children]'s length. /// - /// The `initialIndex` must be valid given [length] and must not be null. If - /// [length] is zero, then `initialIndex` must be 0 (the default). + /// The `initialIndex` must be valid given [length]. If [length] is zero, then + /// `initialIndex` must be 0 (the default). TabController({ int initialIndex = 0, Duration? animationDuration, @@ -114,7 +115,11 @@ class TabController extends ChangeNotifier { _animationController = AnimationController.unbounded( value: initialIndex.toDouble(), vsync: vsync, - ); + ) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } // Private constructor used by `_copyWith`. This allows a new TabController to // be created without having to create a new animationController. @@ -342,8 +347,6 @@ class DefaultTabController extends StatefulWidget { /// /// The [length] argument is typically greater than one. The [length] must /// match [TabBar.tabs]'s and [TabBarView.children]'s length. - /// - /// The [initialIndex] argument must not be null. const DefaultTabController({ super.key, required this.length, diff --git a/packages/flutter/lib/src/material/tab_indicator.dart b/packages/flutter/lib/src/material/tab_indicator.dart index dbe30d136d4a1..1b46a7f06e290 100644 --- a/packages/flutter/lib/src/material/tab_indicator.dart +++ b/packages/flutter/lib/src/material/tab_indicator.dart @@ -17,8 +17,6 @@ import 'colors.dart'; /// or the entire tab with [TabBarIndicatorSize.tab]. class UnderlineTabIndicator extends Decoration { /// Create an underline style selected tab indicator. - /// - /// The [borderSide] and [insets] arguments must not be null. const UnderlineTabIndicator({ this.borderRadius, this.borderSide = const BorderSide(width: 2.0, color: Colors.white), diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 756e61c7f2dbc..235e611eddf1c 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -231,6 +231,8 @@ class _TabStyle extends AnimatedWidget { // details: https://github.com/flutter/flutter/pull/109541#issuecomment-1294241417 Color selectedColor = labelColor ?? tabBarTheme.labelColor + ?? labelStyle?.color + ?? tabBarTheme.labelStyle?.color ?? defaults.labelColor!; final Color unselectedColor; @@ -243,6 +245,8 @@ class _TabStyle extends AnimatedWidget { // when labelColor is a MaterialStateColor. unselectedColor = unselectedLabelColor ?? tabBarTheme.unselectedLabelColor + ?? unselectedLabelStyle?.color + ?? tabBarTheme.unselectedLabelStyle?.color ?? (themeData.useMaterial3 ? defaults.unselectedLabelColor! : selectedColor.withAlpha(0xB2)); // 70% alpha @@ -267,18 +271,18 @@ class _TabStyle extends AnimatedWidget { // To enable TextStyle.lerp(style1, style2, value), both styles must have // the same value of inherit. Force that to be inherit=true here. - final TextStyle defaultStyle = (labelStyle + final TextStyle selectedStyle = (labelStyle ?? tabBarTheme.labelStyle ?? defaults.labelStyle! ).copyWith(inherit: true); - final TextStyle defaultUnselectedStyle = (unselectedLabelStyle + final TextStyle unselectedStyle = (unselectedLabelStyle ?? tabBarTheme.unselectedLabelStyle ?? labelStyle ?? defaults.unselectedLabelStyle! ).copyWith(inherit: true); final TextStyle textStyle = isSelected - ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)! - : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value)!; + ? TextStyle.lerp(selectedStyle, unselectedStyle, animation.value)! + : TextStyle.lerp(unselectedStyle, selectedStyle, animation.value)!; final Color color = _resolveWithLabelColor(context).resolve(states); return DefaultTextStyle( @@ -387,6 +391,39 @@ double _indexChangeProgress(TabController controller) { return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs(); } +class _DividerPainter extends CustomPainter { + _DividerPainter({ + required this.dividerColor, + required this.dividerHeight, + }); + + final Color dividerColor; + final double dividerHeight; + + @override + void paint(Canvas canvas, Size size) { + if (dividerHeight <= 0.0) { + return; + } + + final Paint paint = Paint() + ..color = dividerColor + ..strokeWidth = dividerHeight; + + canvas.drawLine( + Offset(0, size.height - (paint.strokeWidth / 2)), + Offset(size.width, size.height - (paint.strokeWidth / 2)), + paint, + ); + } + + @override + bool shouldRepaint(_DividerPainter oldDelegate) { + return oldDelegate.dividerColor != dividerColor + || oldDelegate.dividerHeight != dividerHeight; + } +} + class _IndicatorPainter extends CustomPainter { _IndicatorPainter({ required this.controller, @@ -397,6 +434,8 @@ class _IndicatorPainter extends CustomPainter { required this.indicatorPadding, required this.labelPaddings, this.dividerColor, + this.dividerHeight, + required this.showDivider, }) : super(repaint: controller.animation) { if (old != null) { saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); @@ -408,8 +447,10 @@ class _IndicatorPainter extends CustomPainter { final TabBarIndicatorSize? indicatorSize; final EdgeInsetsGeometry indicatorPadding; final List tabKeys; - final Color? dividerColor; final List labelPaddings; + final Color? dividerColor; + final double? dividerHeight; + final bool showDivider; // _currentTabOffsets and _currentTextDirection are set each time TabBar // layout is completed. These values can be null when TabBar contains no @@ -501,9 +542,11 @@ class _IndicatorPainter extends CustomPainter { size: _currentRect!.size, textDirection: _currentTextDirection, ); - if (dividerColor != null) { - final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = 1; - canvas.drawLine(Offset(0, size.height), Offset(size.width, size.height), dividerPaint); + if (showDivider && dividerHeight !> 0) { + final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = dividerHeight!; + final Offset dividerP1 = Offset(0, size.height - (dividerPaint.strokeWidth / 2)); + final Offset dividerP2 = Offset(size.width, size.height - (dividerPaint.strokeWidth / 2)); + canvas.drawLine(dividerP1, dividerP2, dividerPaint); } _painter!.paint(canvas, _currentRect!.topLeft, configuration); } @@ -693,15 +736,15 @@ class _TabBarScrollController extends ScrollController { class TabBar extends StatefulWidget implements PreferredSizeWidget { /// Creates a Material Design primary tab bar. /// - /// The [tabs] argument must not be null and its length must match the [controller]'s + /// The length of the [tabs] argument must match the [controller]'s /// [TabController.length]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. /// - /// The [indicatorWeight] parameter defaults to 2, and must not be null. + /// The [indicatorWeight] parameter defaults to 2. /// - /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null. + /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero]. /// /// If [indicator] is not null or provided from [TabBarTheme], /// then [indicatorWeight] and [indicatorColor] are ignored. @@ -718,6 +761,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.indicator, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelStyle, this.labelPadding, @@ -768,6 +812,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.indicator, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelStyle, this.labelPadding, @@ -895,6 +940,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn. final Color? dividerColor; + /// The height of the divider. + /// + /// If null and [ThemeData.useMaterial3] is true, [TabBarTheme.dividerHeight] is used. + /// If that is also null and [ThemeData.useMaterial3] is true, 1.0 will be used. + /// Otherwise divider will not be drawn. + final double? dividerHeight; + /// The color of selected tab labels. /// /// If null, then [TabBarTheme.labelColor] is used. If that is also null and @@ -907,8 +959,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// [MaterialState.selected] state, i.e. if the [Tab] is selected or not, /// ignoring [unselectedLabelColor] even if it's non-null. /// - /// The color specified in the [labelStyle] and the [TabBarTheme.labelStyle] - /// do not affect the effective [labelColor]. + /// When this color or the [TabBarTheme.labelColor] is specified, it overrides + /// the [TextStyle.color] specified for the [labelStyle] or the + /// [TabBarTheme.labelStyle]. /// /// See also: /// @@ -927,9 +980,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// will be used, otherwise unselected tab labels are rendered with /// [labelColor] at 70% opacity. /// - /// The color specified in the [unselectedLabelStyle] and the - /// [TabBarTheme.unselectedLabelStyle] are ignored in [unselectedLabelColor]'s - /// precedence calculation. + /// When this color or the [TabBarTheme.unselectedLabelColor] is specified, it + /// overrides the [TextStyle.color] specified for the [unselectedLabelStyle] + /// or the [TabBarTheme.unselectedLabelStyle]. /// /// See also: /// @@ -938,27 +991,32 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// The text style of the selected tab labels. /// - /// This does not influence color of the tab labels even if [TextStyle.color] - /// is non-null. Refer [labelColor] to color selected tab labels instead. + /// The color specified in [labelStyle] and [TabBarTheme.labelStyle] is used + /// to style the label when [labelColor] or [TabBarTheme.labelColor] are not + /// specified. /// /// If [unselectedLabelStyle] is null, then this text style will be used for /// both selected and unselected label styles. /// - /// If this property is null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall] + /// If this property is null, then [TabBarTheme.labelStyle] will be used. + /// + /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall] /// will be used, otherwise the text style of the [ThemeData.primaryTextTheme]'s /// [TextTheme.bodyLarge] definition is used. final TextStyle? labelStyle; /// The text style of the unselected tab labels. /// - /// This does not influence color of the tab labels even if [TextStyle.color] - /// is non-null. Refer [unselectedLabelColor] to color unselected tab labels - /// instead. + /// The color specified in [unselectedLabelStyle] and [TabBarTheme.unselectedLabelStyle] + /// is used to style the label when [unselectedLabelColor] or [TabBarTheme.unselectedLabelColor] + /// are not specified. + /// + /// If this property is null, then [TabBarTheme.unselectedLabelStyle] will be used. /// - /// If this property is null and [ThemeData.useMaterial3] is true, - /// [TextTheme.titleSmall] will be used, otherwise then the [labelStyle] value - /// is used. If [labelStyle] is null, the text style of the - /// [ThemeData.primaryTextTheme]'s [TextTheme.bodyLarge] definition is used. + /// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall] + /// will be used, otherwise then the [labelStyle] value is used. If [labelStyle] is null, + /// the text style of the [ThemeData.primaryTextTheme]'s [TextTheme.bodyLarge] + /// definition is used. final TextStyle? unselectedLabelStyle; /// The padding added to each of the tab labels. @@ -1154,8 +1212,8 @@ class _TabBarState extends State { TabBarTheme get _defaults { if (Theme.of(context).useMaterial3) { return widget._isPrimary - ? _TabsPrimaryDefaultsM3(context, widget.isScrollable) - : _TabsSecondaryDefaultsM3(context, widget.isScrollable); + ? _TabsPrimaryDefaultsM3(context, widget.isScrollable) + : _TabsSecondaryDefaultsM3(context, widget.isScrollable); } else { return _TabsDefaultsM2(context, widget.isScrollable); } @@ -1172,10 +1230,7 @@ class _TabBarState extends State { return tabBarTheme.indicator!; } - Color color = widget.indicatorColor - ?? (theme.useMaterial3 - ? tabBarTheme.indicatorColor ?? _defaults.indicatorColor! - : Theme.of(context).indicatorColor); + Color color = widget.indicatorColor ?? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!; // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator @@ -1269,8 +1324,10 @@ class _TabBarState extends State { indicatorPadding: widget.indicatorPadding, tabKeys: _tabKeys, old: _indicatorPainter, - dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null, labelPaddings: _labelPaddings, + dividerColor: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor, + dividerHeight: widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight, + showDivider: theme.useMaterial3 && !widget.isScrollable, ); } @@ -1299,7 +1356,9 @@ class _TabBarState extends State { widget.indicatorWeight != oldWidget.indicatorWeight || widget.indicatorSize != oldWidget.indicatorSize || widget.indicatorPadding != oldWidget.indicatorPadding || - widget.indicator != oldWidget.indicator) { + widget.indicator != oldWidget.indicator || + widget.dividerColor != oldWidget.dividerColor || + widget.dividerHeight != oldWidget.dividerHeight) { _initIndicatorPainter(); } @@ -1321,6 +1380,7 @@ class _TabBarState extends State { _controller!.removeListener(_handleTabControllerTick); } _controller = null; + _scrollController?.dispose(); // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } @@ -1475,6 +1535,7 @@ class _TabBarState extends State { Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); assert(_debugScheduleCheckHasValidTabsCount()); + final ThemeData theme = Theme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context); final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!; assert(_debugTabAlignmentIsValid(effectiveTabAlignment)); @@ -1486,7 +1547,6 @@ class _TabBarState extends State { ); } - final List wrappedTabs = List.generate(widget.tabs.length, (int index) { const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0; EdgeInsetsGeometry? adjustedPadding; @@ -1627,6 +1687,24 @@ class _TabBarState extends State { child: tabBar, ), ); + if (theme.useMaterial3) { + final AlignmentGeometry effectiveAlignment = switch (effectiveTabAlignment) { + TabAlignment.center => Alignment.center, + TabAlignment.start || TabAlignment.startOffset || TabAlignment.fill => AlignmentDirectional.centerStart, + }; + + tabBar = CustomPaint( + painter: _DividerPainter( + dividerColor: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor!, + dividerHeight: widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight!, + ), + child: Align( + heightFactor: 1.0, + alignment: effectiveAlignment, + child: tabBar, + ), + ); + } } else if (widget.padding != null) { tabBar = Padding( padding: widget.padding!, @@ -1706,7 +1784,7 @@ class TabBarView extends StatefulWidget { class _TabBarViewState extends State { TabController? _controller; - late PageController _pageController; + PageController? _pageController; late List _childrenWithKey; int? _currentIndex; int _warpUnderwayCount = 0; @@ -1748,7 +1826,7 @@ class _TabBarViewState extends State { void _jumpToPage(int page) { _warpUnderwayCount += 1; - _pageController.jumpToPage(page); + _pageController!.jumpToPage(page); _warpUnderwayCount -= 1; } @@ -1758,7 +1836,7 @@ class _TabBarViewState extends State { required Curve curve, }) async { _warpUnderwayCount += 1; - await _pageController.animateToPage(page, duration: duration, curve: curve); + await _pageController!.animateToPage(page, duration: duration, curve: curve); _warpUnderwayCount -= 1; } @@ -1773,6 +1851,8 @@ class _TabBarViewState extends State { super.didChangeDependencies(); _updateTabController(); _currentIndex = _controller!.index; + // TODO(chunhtai): https://github.com/flutter/flutter/issues/134253 + _pageController?.dispose(); _pageController = PageController( initialPage: _currentIndex!, viewportFraction: widget.viewportFraction, @@ -1787,6 +1867,13 @@ class _TabBarViewState extends State { _currentIndex = _controller!.index; _jumpToPage(_currentIndex!); } + if (widget.viewportFraction != oldWidget.viewportFraction) { + _pageController?.dispose(); + _pageController = PageController( + initialPage: _currentIndex!, + viewportFraction: widget.viewportFraction, + ); + } // While a warp is under way, we stop updating the tab page contents. // This is tracked in https://github.com/flutter/flutter/issues/31269. if (widget.children != oldWidget.children && _warpUnderwayCount == 0) { @@ -1800,6 +1887,7 @@ class _TabBarViewState extends State { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = null; + _pageController?.dispose(); // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } @@ -1820,7 +1908,7 @@ class _TabBarViewState extends State { } void _warpToCurrentIndex() { - if (!mounted || _pageController.page == _currentIndex!.toDouble()) { + if (!mounted || _pageController!.page == _currentIndex!.toDouble()) { return; } @@ -1880,7 +1968,7 @@ class _TabBarViewState extends State { } void _syncControllerOffset() { - _controller!.offset = clampDouble(_pageController.page! - _controller!.index, -1.0, 1.0); + _controller!.offset = clampDouble(_pageController!.page! - _controller!.index, -1.0, 1.0); } // Called when the PageView scrolls @@ -1898,15 +1986,16 @@ class _TabBarViewState extends State { } _scrollUnderwayCount += 1; + final double page = _pageController!.page!; if (notification is ScrollUpdateNotification && !_controller!.indexIsChanging) { - final bool pageChanged = (_pageController.page! - _controller!.index).abs() > 1.0; + final bool pageChanged = (page - _controller!.index).abs() > 1.0; if (pageChanged) { - _controller!.index = _pageController.page!.round(); + _controller!.index = page.round(); _currentIndex =_controller!.index; } _syncControllerOffset(); } else if (notification is ScrollEndNotification) { - _controller!.index = _pageController.page!.round(); + _controller!.index = page.round(); _currentIndex = _controller!.index; if (!_controller!.indexIsChanging) { _syncControllerOffset(); @@ -1965,8 +2054,6 @@ class _TabBarViewState extends State { /// Used by [TabPageSelector] to indicate the selected page. class TabPageSelectorIndicator extends StatelessWidget { /// Creates an indicator used by [TabPageSelector]. - /// - /// The [backgroundColor], [borderColor], and [size] parameters must not be null. const TabPageSelectorIndicator({ super.key, required this.backgroundColor, @@ -2177,6 +2264,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => _colors.surfaceVariant; + @override + double? get dividerHeight => 1.0; + @override Color? get indicatorColor => _colors.primary; @@ -2224,7 +2314,7 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override - TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; + TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; static double indicatorWeight = 3.0; } @@ -2241,6 +2331,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => _colors.surfaceVariant; + @override + double? get dividerHeight => 1.0; + @override Color? get indicatorColor => _colors.primary; @@ -2288,7 +2381,7 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; @override - TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; + TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; } // END GENERATED TOKEN PROPERTIES - Tabs diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart index 8e5c2db54885a..e29b5684585cf 100644 --- a/packages/flutter/lib/src/material/text_button.dart +++ b/packages/flutter/lib/src/material/text_button.dart @@ -73,9 +73,7 @@ import 'theme_data.dart'; /// * /// * class TextButton extends ButtonStyleButton { - /// Create a TextButton. - /// - /// The [autofocus] and [clipBehavior] arguments must not be null. + /// Create a [TextButton]. const TextButton({ super.key, required super.onPressed, @@ -96,8 +94,6 @@ class TextButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 8 logical pixels /// at the ends, with an 8 pixel gap in between. - /// - /// The [icon] and [label] arguments must not be null. factory TextButton.icon({ Key? key, required VoidCallback? onPressed, @@ -248,7 +244,7 @@ class TextButton extends ButtonStyleButton { /// each state and "others" means all other states. /// /// The `textScaleFactor` is the value of - /// `MediaQuery.textScaleFactorOf(context)` and the names of the + /// `MediaQuery.textScalerOf(context).textScaleFactor` and the names of the /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been /// abbreviated for readability. /// @@ -385,7 +381,7 @@ EdgeInsetsGeometry _scaledPadding(BuildContext context) { useMaterial3 ? const EdgeInsets.symmetric(horizontal: 12, vertical: 8) : const EdgeInsets.all(8), const EdgeInsets.symmetric(horizontal: 8), const EdgeInsets.symmetric(horizontal: 4), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); } @@ -500,7 +496,7 @@ class _TextButtonWithIcon extends TextButton { useMaterial3 ? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8) : const EdgeInsets.all(8), const EdgeInsets.symmetric(horizontal: 4), const EdgeInsets.symmetric(horizontal: 4), - MediaQuery.textScaleFactorOf(context), + MediaQuery.textScalerOf(context).textScaleFactor, ); return super.defaultStyleOf(context).copyWith( padding: MaterialStatePropertyAll(scaledPadding), @@ -519,7 +515,7 @@ class _TextButtonWithIconChild extends StatelessWidget { @override Widget build(BuildContext context) { - final double scale = MediaQuery.textScaleFactorOf(context); + final double scale = MediaQuery.textScalerOf(context).textScaleFactor; final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!; return Row( mainAxisSize: MainAxisSize.min, diff --git a/packages/flutter/lib/src/material/text_button_theme.dart b/packages/flutter/lib/src/material/text_button_theme.dart index 4b13a922fbf72..79a2f4016385d 100644 --- a/packages/flutter/lib/src/material/text_button_theme.dart +++ b/packages/flutter/lib/src/material/text_button_theme.dart @@ -91,8 +91,6 @@ class TextButtonThemeData with Diagnosticable { /// [ButtonStyle] for [TextButton]s below the overall [Theme]. class TextButtonTheme extends InheritedTheme { /// Create a [TextButtonTheme]. - /// - /// The [data] parameter must not be null. const TextButtonTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index f2d6716f5ec79..181be1a631849 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -178,6 +178,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete /// /// {@macro flutter.widgets.editableText.accessibility} /// +/// {@tool dartpad} +/// This sample shows how to style a text field to match a filled or outlined +/// Material Design 3 text field. +/// +/// ** See code in examples/api/lib/material/text_field/text_field.2.dart ** +/// {@end-tool} +/// /// See also: /// /// * [TextFormField], which integrates with the [Form] widget. @@ -231,13 +238,7 @@ class TextField extends StatefulWidget { /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default - /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and - /// must not be null. - /// - /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect], - /// [scrollPadding], [maxLines], [maxLength], [selectionHeightStyle], - /// [selectionWidthStyle], [enableSuggestions], and - /// [enableIMEPersonalizedLearning] arguments must not be null. + /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively. /// /// See also: /// @@ -422,7 +423,12 @@ class TextField extends StatefulWidget { /// /// This text style is also used as the base style for the [decoration]. /// - /// If null, defaults to the `titleMedium` text style from the current [Theme]. + /// If null, [TextTheme.bodyLarge] will be used. When the text field is disabled, + /// [TextTheme.bodyLarge] with an opacity of 0.38 will be used instead. + /// + /// If null and [ThemeData.useMaterial3] is false, [TextTheme.titleMedium] will + /// be used. When the text field is disabled, [TextTheme.titleMedium] with + /// [ThemeData.disabledColor] will be used instead. final TextStyle? style; /// {@macro flutter.widgets.editableText.strutStyle} @@ -798,7 +804,7 @@ class TextField extends StatefulWidget { decoration: TextDecoration.underline, decorationColor: Colors.red, decorationStyle: TextDecorationStyle.wavy, - ); + ); /// Default builder for [TextField]'s spell check suggestions toolbar. /// @@ -938,7 +944,7 @@ class _TextFieldState extends State with RestorationMixin implements bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!; - bool get _hasError => widget.decoration?.errorText != null || _hasIntrinsicError; + bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError; Color get _errorColor => widget.decoration?.errorStyle?.color ?? Theme.of(context).colorScheme.error; @@ -1241,7 +1247,8 @@ class _TextFieldState extends State with RestorationMixin implements final ThemeData theme = Theme.of(context); final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context); - final TextStyle style = _getInputStyleForState(theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!).merge(widget.style); + final TextStyle? providedStyle = MaterialStateProperty.resolveAs(widget.style, _materialState); + final TextStyle style = _getInputStyleForState(theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!).merge(providedStyle); final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness; final TextEditingController controller = _effectiveController; final FocusNode focusNode = _effectiveFocusNode; @@ -1283,6 +1290,7 @@ class _TextFieldState extends State with RestorationMixin implements Color? autocorrectionTextRectColor; Radius? cursorRadius = widget.cursorRadius; VoidCallback? handleDidGainAccessibilityFocus; + VoidCallback? handleDidLoseAccessibilityFocus; switch (theme.platform) { case TargetPlatform.iOS: @@ -1313,6 +1321,9 @@ class _TextFieldState extends State with RestorationMixin implements _effectiveFocusNode.requestFocus(); } }; + handleDidLoseAccessibilityFocus = () { + _effectiveFocusNode.unfocus(); + }; case TargetPlatform.android: case TargetPlatform.fuchsia: @@ -1330,6 +1341,15 @@ class _TextFieldState extends State with RestorationMixin implements cursorOpacityAnimates ??= false; cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + handleDidGainAccessibilityFocus = () { + // Automatically activate the TextField when it receives accessibility focus. + if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + _effectiveFocusNode.requestFocus(); + } + }; + handleDidLoseAccessibilityFocus = () { + _effectiveFocusNode.unfocus(); + }; case TargetPlatform.windows: forcePressEnabled = false; @@ -1344,6 +1364,9 @@ class _TextFieldState extends State with RestorationMixin implements _effectiveFocusNode.requestFocus(); } }; + handleDidLoseAccessibilityFocus = () { + _effectiveFocusNode.unfocus(); + }; } Widget child = RepaintBoundary( @@ -1471,6 +1494,7 @@ class _TextFieldState extends State with RestorationMixin implements _requestKeyboard(); }, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, + onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus, child: child, ); }, diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index b388d932aa8e2..a283b96915167 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -131,7 +131,7 @@ class TextFormField extends FormField { int? minLines, bool expands = false, int? maxLength, - ValueChanged? onChanged, + this.onChanged, GestureTapCallback? onTap, TapRegionCallback? onTapOutside, VoidCallback? onEditingComplete, @@ -193,9 +193,7 @@ class TextFormField extends FormField { .applyDefaults(Theme.of(field.context).inputDecorationTheme); void onChangedHandler(String value) { field.didChange(value); - if (onChanged != null) { - onChanged(value); - } + onChanged?.call(value); } return UnmanagedRestorationScope( bucket: field.bucket, @@ -272,6 +270,12 @@ class TextFormField extends FormField { /// initialize its [TextEditingController.text] with [initialValue]. final TextEditingController? controller; + /// {@template flutter.material.TextFormField.onChanged} + /// Called when the user initiates a change to the TextField's + /// value: when they have inserted or deleted text or reset the form. + /// {@endtemplate} + final ValueChanged? onChanged; + static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { return AdaptiveTextSelectionToolbar.editableText( editableTextState: editableTextState, @@ -365,10 +369,11 @@ class _TextFormFieldState extends FormFieldState { @override void reset() { - // setState will be called in the superclass, so even though state is being - // manipulated, no setState call is needed here. + // Set the controller value before calling super.reset() to let + // _handleControllerChanged suppress the change. _effectiveController.text = widget.initialValue ?? ''; super.reset(); + _textFormField.onChanged?.call(_effectiveController.text); } void _handleControllerChanged() { diff --git a/packages/flutter/lib/src/material/text_selection_theme.dart b/packages/flutter/lib/src/material/text_selection_theme.dart index 676cdc38a6462..b7e659ae4d498 100644 --- a/packages/flutter/lib/src/material/text_selection_theme.dart +++ b/packages/flutter/lib/src/material/text_selection_theme.dart @@ -140,8 +140,6 @@ class TextSelectionThemeData with Diagnosticable { class TextSelectionTheme extends InheritedTheme { /// Creates a text selection theme widget that specifies the text /// selection properties for all widgets below it in the widget tree. - /// - /// The data argument must not be null. const TextSelectionTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/text_selection_toolbar.dart b/packages/flutter/lib/src/material/text_selection_toolbar.dart index 9927cd11bd4d4..61b047a3cc966 100644 --- a/packages/flutter/lib/src/material/text_selection_toolbar.dart +++ b/packages/flutter/lib/src/material/text_selection_toolbar.dart @@ -8,11 +8,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart' show listEquals; import 'package:flutter/rendering.dart'; +import 'color_scheme.dart'; import 'debug.dart'; import 'icon_button.dart'; import 'icons.dart'; import 'material.dart'; import 'material_localizations.dart'; +import 'theme.dart'; const double _kToolbarHeight = 44.0; const double _kToolbarContentDistance = 8.0; @@ -650,13 +652,34 @@ class _TextSelectionToolbarContainer extends StatelessWidget { final Widget child; + // These colors were taken from a screenshot of a Pixel 6 emulator running + // Android API level 34. + static const Color _defaultColorLight = Color(0xffffffff); + static const Color _defaultColorDark = Color(0xff424242); + + static Color _getColor(ColorScheme colorScheme) { + final bool isDefaultSurface = switch (colorScheme.brightness) { + Brightness.light => identical(ThemeData().colorScheme.surface, colorScheme.surface), + Brightness.dark => identical(ThemeData.dark().colorScheme.surface, colorScheme.surface), + }; + if (!isDefaultSurface) { + return colorScheme.surface; + } + return switch (colorScheme.brightness) { + Brightness.light => _defaultColorLight, + Brightness.dark => _defaultColorDark, + }; + } + @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); return Material( // This value was eyeballed to match the native text selection menu on - // a Pixel 2 running Android 10. - borderRadius: const BorderRadius.all(Radius.circular(7.0)), + // a Pixel 6 emulator running Android API level 34. + borderRadius: const BorderRadius.all(Radius.circular(_kToolbarHeight / 2)), clipBehavior: Clip.antiAlias, + color: _getColor(theme.colorScheme), elevation: 1.0, type: MaterialType.card, child: child, diff --git a/packages/flutter/lib/src/material/text_selection_toolbar_text_button.dart b/packages/flutter/lib/src/material/text_selection_toolbar_text_button.dart index ca7e3c0ab291f..844da98ce053e 100644 --- a/packages/flutter/lib/src/material/text_selection_toolbar_text_button.dart +++ b/packages/flutter/lib/src/material/text_selection_toolbar_text_button.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; -import 'colors.dart'; +import 'color_scheme.dart'; import 'constants.dart'; import 'text_button.dart'; import 'theme.dart'; @@ -130,20 +130,47 @@ class TextSelectionToolbarTextButton extends StatelessWidget { ); } + // These colors were taken from a screenshot of a Pixel 6 emulator running + // Android API level 34. + static const Color _defaultForegroundColorLight = Color(0xff000000); + static const Color _defaultForegroundColorDark = Color(0xffffffff); + + // The background color is hardcoded to transparent by default so the buttons + // are the color of the container behind them. For example TextSelectionToolbar + // hardcodes the color value, and TextSelectionToolbarTextButtons that are its + // children become that color. + static const Color _defaultBackgroundColorTransparent = Color(0x00000000); + + static Color _getForegroundColor(ColorScheme colorScheme) { + final bool isDefaultOnSurface = switch (colorScheme.brightness) { + Brightness.light => identical(ThemeData().colorScheme.onSurface, colorScheme.onSurface), + Brightness.dark => identical(ThemeData.dark().colorScheme.onSurface, colorScheme.onSurface), + }; + if (!isDefaultOnSurface) { + return colorScheme.onSurface; + } + return switch (colorScheme.brightness) { + Brightness.light => _defaultForegroundColorLight, + Brightness.dark => _defaultForegroundColorDark, + }; + } + @override Widget build(BuildContext context) { - // TODO(hansmuller): Should be colorScheme.onSurface - final ThemeData theme = Theme.of(context); - final bool isDark = theme.colorScheme.brightness == Brightness.dark; - final Color foregroundColor = isDark ? Colors.white : Colors.black87; - + final ColorScheme colorScheme = Theme.of(context).colorScheme; return TextButton( style: TextButton.styleFrom( - foregroundColor: foregroundColor, + backgroundColor: _defaultBackgroundColorTransparent, + foregroundColor: _getForegroundColor(colorScheme), shape: const RoundedRectangleBorder(), minimumSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension), padding: padding, alignment: alignment, + textStyle: const TextStyle( + // This value was eyeballed from a screenshot of a Pixel 6 emulator + // running Android API level 34. + fontWeight: FontWeight.w400, + ), ), onPressed: onPressed, child: child, diff --git a/packages/flutter/lib/src/material/theme.dart b/packages/flutter/lib/src/material/theme.dart index 8bbc12cefe7ca..c55efc2aeb291 100644 --- a/packages/flutter/lib/src/material/theme.dart +++ b/packages/flutter/lib/src/material/theme.dart @@ -36,8 +36,6 @@ const Duration kThemeAnimationDuration = Duration(milliseconds: 200); /// the [MaterialApp.theme] argument. class Theme extends StatelessWidget { /// Applies the given theme [data] to [child]. - /// - /// The [data] and [child] arguments must not be null. const Theme({ super.key, required this.data, @@ -201,8 +199,7 @@ class ThemeDataTween extends Tween { class AnimatedTheme extends ImplicitlyAnimatedWidget { /// Creates an animated theme. /// - /// By default, the theme transition uses a linear curve. The [data] and - /// [child] arguments must not be null. + /// By default, the theme transition uses a linear curve. const AnimatedTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 57dfafcddbb42..ff00c152dc7b9 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -179,68 +179,15 @@ enum MaterialTapTargetSize { /// for the subtree that appears below the new [Theme], or insert a widget /// that creates a new BuildContext, like [Builder]. /// -/// {@tool snippet} -/// In this example, the [Container] widget uses [Theme.of] to retrieve the -/// primary color from the theme's [colorScheme] to draw an amber square. -/// The [Builder] widget separates the parent theme's [BuildContext] from the -/// child's [BuildContext]. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/theme_data.png) -/// -/// ```dart -/// Theme( -/// data: ThemeData.from( -/// colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.amber), -/// ), -/// child: Builder( -/// builder: (BuildContext context) { -/// return Container( -/// width: 100, -/// height: 100, -/// color: Theme.of(context).colorScheme.primary, -/// ); -/// }, -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// {@tool snippet} -/// -/// This sample creates a [MaterialApp] with a [Theme] whose -/// [ColorScheme] is based on [Colors.blue], but with the color -/// scheme's [ColorScheme.secondary] color overridden to be green. The -/// [AppBar] widget uses the color scheme's [ColorScheme.primary] as -/// its default background color and the [FloatingActionButton] widget -/// uses the color scheme's [ColorScheme.secondary] for its default -/// background. By default, the [Text] widget uses -/// [TextTheme.bodyMedium], and the color of that [TextStyle] has been -/// changed to purple. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/material_app_theme_data.png) +/// {@tool dartpad} +/// This example demonstrates how a typical [MaterialApp] specifies +/// and uses a custom [Theme]. The theme's [ColorScheme] is based on a +/// single "seed" color and configures itself to match the platform's +/// current light or dark color configuration. The theme overrides the +/// default configuration of [FloatingActionButton] to show how to +/// customize the appearance a class of components. /// -/// ```dart -/// MaterialApp( -/// theme: ThemeData( -/// colorScheme: ColorScheme.fromSwatch().copyWith( -/// secondary: Colors.green, -/// ), -/// textTheme: const TextTheme(bodyMedium: TextStyle(color: Colors.purple)), -/// ), -/// home: Scaffold( -/// appBar: AppBar( -/// title: const Text('ThemeData Demo'), -/// ), -/// floatingActionButton: FloatingActionButton( -/// child: const Icon(Icons.add), -/// onPressed: () {}, -/// ), -/// body: const Center( -/// child: Text('Button pressed 0 times'), -/// ), -/// ), -/// ) -/// ``` +/// ** See code in examples/api/lib/material/theme_data/theme_data.0.dart ** /// {@end-tool} /// /// See for @@ -388,11 +335,6 @@ class ThemeData with Diagnosticable { ToggleButtonsThemeData? toggleButtonsTheme, TooltipThemeData? tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - AndroidOverscrollIndicator? androidOverscrollIndicator, @Deprecated( 'No longer used by the framework, please remove any reference to it. ' 'For more information, consult the migration guide at ' @@ -439,7 +381,7 @@ class ThemeData with Diagnosticable { pageTransitionsTheme ??= const PageTransitionsTheme(); scrollbarTheme ??= const ScrollbarThemeData(); visualDensity ??= VisualDensity.defaultDensityForPlatform(platform); - useMaterial3 ??= false; + useMaterial3 ??= true; final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 ? useInkSparkle ? InkSparkle.splashFactory : InkRipple.splashFactory @@ -543,7 +485,7 @@ class ThemeData with Diagnosticable { } textTheme = defaultTextTheme.merge(textTheme); primaryTextTheme = defaultPrimaryTextTheme.merge(primaryTextTheme); - iconTheme ??= isDark ? const IconThemeData(color: kDefaultIconLightColor) : const IconThemeData(color: kDefaultIconDarkColor); + iconTheme ??= isDark ? IconThemeData(color: kDefaultIconLightColor) : IconThemeData(color: kDefaultIconDarkColor); primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); // COMPONENT THEMES @@ -689,7 +631,6 @@ class ThemeData with Diagnosticable { toggleButtonsTheme: toggleButtonsTheme, tooltipTheme: tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator: androidOverscrollIndicator, toggleableActiveColor: toggleableActiveColor, selectedRowColor: selectedRowColor, errorColor: errorColor, @@ -800,11 +741,6 @@ class ThemeData with Diagnosticable { required this.toggleButtonsTheme, required this.tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - this.androidOverscrollIndicator, @Deprecated( 'No longer used by the framework, please remove any reference to it. ' 'For more information, consult the migration guide at ' @@ -848,8 +784,6 @@ class ThemeData with Diagnosticable { /// Create a [ThemeData] based on the colors in the given [colorScheme] and /// text styles of the optional [textTheme]. /// - /// The [colorScheme] can not be null. - /// /// If [colorScheme].brightness is [Brightness.dark] then /// [ThemeData.applyElevationOverlayColor] will be set to true to support /// the Material dark theme method for indicating elevation by applying @@ -903,7 +837,7 @@ class ThemeData with Diagnosticable { ); } - /// A default light blue theme. + /// A default light theme. /// /// This theme does not contain text geometry. Instead, it is expected that /// this theme is localized using text geometry using [ThemeData.localize]. @@ -912,7 +846,7 @@ class ThemeData with Diagnosticable { useMaterial3: useMaterial3, ); - /// A default dark theme with a teal secondary [ColorScheme] color. + /// A default dark theme. /// /// This theme does not contain text geometry. Instead, it is expected that /// this theme is localized using text geometry using [ThemeData.localize]. @@ -1083,21 +1017,16 @@ class ThemeData with Diagnosticable { /// splash with sparkle effects. final InteractiveInkFeatureFactory splashFactory; - /// A temporary flag used to opt-in to Material 3 features. + /// A temporary flag that can be used to opt-out of Material 3 features. /// - /// If true, then widgets that have been migrated to Material 3 will - /// use new colors, typography and other features of Material 3. If false, - /// they will use the Material 2 look and feel. + /// This flag is _true_ by default. If false, then components will + /// continue to use the colors, typography and other features of + /// Material 2. /// - /// During the migration to Material 3, turning this on may yield - /// inconsistent look and feel in your app as some widgets are migrated - /// while others have yet to be. - /// - /// Defaults to false. When the Material 3 specification is complete - /// and all widgets are migrated on stable, we will change this flag to be - /// true by default. After that change has landed on stable, we will deprecate - /// this flag and remove all uses of it. At that point, the `material` library - /// will aim to only support Material 3. + /// In the long run this flag will be deprecated and eventually + /// only Material 3 will be supported. We recommend that applications + /// migrate to Material 3 as soon as that's practical. Until that migration + /// is complete, this flag can be set to false. /// /// ## Defaults /// @@ -1131,14 +1060,13 @@ class ThemeData with Diagnosticable { /// * Typography: [Typography] (see table above) /// /// ### Components - /// \* *new* means the new widgets/methods created since the last stable release. /// * Badges: [Badge] /// * Bottom app bar: [BottomAppBar] /// * Bottom sheets: [BottomSheet] /// * Buttons /// - Common buttons: [ElevatedButton], [FilledButton], [FilledButton.tonal], [OutlinedButton], [TextButton] /// - FAB: [FloatingActionButton], [FloatingActionButton.extended] - /// - Icon buttons: [IconButton], [IconButton.filled] (*new*), [IconButton.filledTonal] (*new*), [IconButton.outlined] (*new*) + /// - Icon buttons: [IconButton], [IconButton.filled] (*new*), [IconButton.filledTonal], [IconButton.outlined] /// - Segmented buttons: [SegmentedButton] (replacing [ToggleButtons]) /// * Cards: [Card] /// * Checkbox: [Checkbox], [CheckboxListTile] @@ -1156,11 +1084,11 @@ class ThemeData with Diagnosticable { /// * Navigation rail: [NavigationRail] /// * Progress indicators: [CircularProgressIndicator], [LinearProgressIndicator] /// * Radio button: [Radio], [RadioListTile] - /// * Search: [SearchBar] (*new*), [SearchAnchor] (*new*), + /// * Search: [SearchBar], [SearchAnchor], /// * Snack bar: [SnackBar] /// * Slider: [Slider], [RangeSlider] /// * Switch: [Switch], [SwitchListTile] - /// * Tabs: [TabBar], [TabBar.secondary] (*new*) + /// * Tabs: [TabBar], [TabBar.secondary] /// * TextFields: [TextField] together with its [InputDecoration] /// * Time pickers: [showTimePicker], [TimePickerDialog] /// * Top app bar: [AppBar], [SliverAppBar], [SliverAppBar.medium], [SliverAppBar.large] @@ -1508,27 +1436,6 @@ class ThemeData with Diagnosticable { // DEPRECATED (newest deprecations at the bottom) - /// Specifies which overscroll indicator to use on [TargetPlatform.android]. - /// - /// When null, the default value of - /// [MaterialScrollBehavior.androidOverscrollIndicator] is - /// [AndroidOverscrollIndicator.glow]. - /// - /// This property is deprecated. Use the [useMaterial3] flag instead, or - /// override [ScrollBehavior.buildOverscrollIndicator]. - /// - /// See also: - /// - /// * [StretchingOverscrollIndicator], a Material Design edge effect - /// that transforms the contents of a scrollable when overscrolled. - /// * [GlowingOverscrollIndicator], an edge effect that paints a glow - /// over the contents of a scrollable when overscrolled. - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - final AndroidOverscrollIndicator? androidOverscrollIndicator; - /// Obsolete property that was used for input validation errors, e.g. in /// [TextField] fields. Use [ColorScheme.error] instead. @Deprecated( @@ -1584,7 +1491,6 @@ class ThemeData with Diagnosticable { TargetPlatform? platform, ScrollbarThemeData? scrollbarTheme, InteractiveInkFeatureFactory? splashFactory, - bool? useMaterial3, VisualDensity? visualDensity, // COLOR // [colorScheme] is the preferred way to configure colors. The other color @@ -1664,11 +1570,6 @@ class ThemeData with Diagnosticable { ToggleButtonsThemeData? toggleButtonsTheme, TooltipThemeData? tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - AndroidOverscrollIndicator? androidOverscrollIndicator, @Deprecated( 'No longer used by the framework, please remove any reference to it. ' 'For more information, consult the migration guide at ' @@ -1696,6 +1597,14 @@ class ThemeData with Diagnosticable { 'This feature was deprecated after v3.3.0-0.6.pre.', ) Color? bottomAppBarColor, + @Deprecated( + 'Use a ThemeData constructor (.from, .light, or .dark) instead. ' + 'These constructors all have a useMaterial3 argument, ' + 'and they set appropriate default values based on its value. ' + 'See the useMaterial3 API documentation for full details. ' + 'This feature was deprecated after v3.13.0-0.2.pre.', + ) + bool? useMaterial3, }) { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( @@ -1790,7 +1699,6 @@ class ThemeData with Diagnosticable { toggleButtonsTheme: toggleButtonsTheme ?? this.toggleButtonsTheme, tooltipTheme: tooltipTheme ?? this.tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator, toggleableActiveColor: toggleableActiveColor ?? _toggleableActiveColor, selectedRowColor: selectedRowColor ?? _selectedRowColor, errorColor: errorColor ?? _errorColor, @@ -1885,8 +1793,6 @@ class ThemeData with Diagnosticable { /// Linearly interpolate between two themes. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static ThemeData lerp(ThemeData a, ThemeData b, double t) { if (identical(a, b)) { @@ -1984,7 +1890,6 @@ class ThemeData with Diagnosticable { toggleButtonsTheme: ToggleButtonsThemeData.lerp(a.toggleButtonsTheme, b.toggleButtonsTheme, t)!, tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t)!, // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator, toggleableActiveColor: Color.lerp(a.toggleableActiveColor, b.toggleableActiveColor, t), selectedRowColor: Color.lerp(a.selectedRowColor, b.selectedRowColor, t), errorColor: Color.lerp(a.errorColor, b.errorColor, t), @@ -2090,7 +1995,6 @@ class ThemeData with Diagnosticable { other.toggleButtonsTheme == toggleButtonsTheme && other.tooltipTheme == tooltipTheme && // DEPRECATED (newest deprecations at the bottom) - other.androidOverscrollIndicator == androidOverscrollIndicator && other.toggleableActiveColor == toggleableActiveColor && other.selectedRowColor == selectedRowColor && other.errorColor == errorColor && @@ -2193,7 +2097,6 @@ class ThemeData with Diagnosticable { toggleButtonsTheme, tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator, toggleableActiveColor, selectedRowColor, errorColor, @@ -2298,7 +2201,6 @@ class ThemeData with Diagnosticable { properties.add(DiagnosticsProperty('toggleButtonsTheme', toggleButtonsTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('tooltipTheme', tooltipTheme, level: DiagnosticLevel.debug)); // DEPRECATED (newest deprecations at the bottom) - properties.add(EnumProperty('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, level: DiagnosticLevel.debug)); properties.add(ColorProperty('toggleableActiveColor', toggleableActiveColor, defaultValue: defaultData.toggleableActiveColor, level: DiagnosticLevel.debug)); properties.add(ColorProperty('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor, level: DiagnosticLevel.debug)); properties.add(ColorProperty('errorColor', errorColor, defaultValue: defaultData.errorColor, level: DiagnosticLevel.debug)); @@ -2339,8 +2241,6 @@ class ThemeData with Diagnosticable { class MaterialBasedCupertinoThemeData extends CupertinoThemeData { /// Create a [MaterialBasedCupertinoThemeData] based on a Material [ThemeData] /// and its `cupertinoOverrideTheme`. - /// - /// The [materialTheme] parameter must not be null. MaterialBasedCupertinoThemeData({ required ThemeData materialTheme, }) : this._( @@ -2467,8 +2367,6 @@ class _FifoCache { /// Returns the previously cached value for the given key, if available; /// if not, calls the given callback to obtain it first. - /// - /// The arguments must not be null. V putIfAbsent(K key, V Function() loader) { assert(key != null); final V? result = _cache[key]; @@ -2521,9 +2419,8 @@ class _FifoCache { class VisualDensity with Diagnosticable { /// A const constructor for [VisualDensity]. /// - /// All of the arguments must be non-null, and [horizontal] and [vertical] - /// must be in the interval between [minimumDensity] and [maximumDensity], - /// inclusive. + /// The [horizontal] and [vertical] arguments must be in the interval between + /// [minimumDensity] and [maximumDensity], inclusive. const VisualDensity({ this.horizontal = 0.0, this.vertical = 0.0, diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 479438aa15f27..52d176fa31358 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -348,7 +348,7 @@ class _HourMinuteControl extends StatelessWidget { child: Text( text, style: effectiveStyle, - textScaleFactor: 1, + textScaler: TextScaler.noScaling, ), ), ), @@ -479,7 +479,7 @@ class _StringFragment extends StatelessWidget { child: Text( _stringFragmentValue(timeOfDayFormat), style: effectiveStyle, - textScaleFactor: 1, + textScaler: TextScaler.noScaling, textAlign: TextAlign.center, ), ), @@ -696,7 +696,7 @@ class _AmPmButton extends StatelessWidget { final Color resolvedBackgroundColor = MaterialStateProperty.resolveAs(timePickerTheme.dayPeriodColor ?? defaultTheme.dayPeriodColor, states); final Color resolvedTextColor = MaterialStateProperty.resolveAs(timePickerTheme.dayPeriodTextColor ?? defaultTheme.dayPeriodTextColor, states); final TextStyle? resolvedTextStyle = MaterialStateProperty.resolveAs(timePickerTheme.dayPeriodTextStyle ?? defaultTheme.dayPeriodTextStyle, states)?.copyWith(color: resolvedTextColor); - final double buttonTextScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 2); + final TextScaler buttonTextScaler = MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 2.0); return Material( color: resolvedBackgroundColor, @@ -710,7 +710,7 @@ class _AmPmButton extends StatelessWidget { child: Text( label, style: resolvedTextStyle, - textScaleFactor: buttonTextScaleFactor, + textScaler: buttonTextScaler, ), ), ), @@ -1394,14 +1394,13 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { required String label, required VoidCallback onTap, }) { - final double labelScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 2); return _TappableLabel( value: value, inner: inner, painter: TextPainter( text: TextSpan(style: textStyle, text: label), textDirection: TextDirection.ltr, - textScaleFactor: labelScaleFactor, + textScaler: MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 2.0), )..layout(), onTap: onTap, ); @@ -2063,8 +2062,7 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with Restora return SizedBox.fromSize( size: alwaysUse24HourFormat ? defaultTheme.hourMinuteInputSize24Hour : defaultTheme.hourMinuteInputSize, - child: MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1), + child: MediaQuery.withNoTextScaling( child: UnmanagedRestorationScope( bucket: bucket, child: Semantics( @@ -2106,10 +2104,10 @@ typedef EntryModeChangeCallback = void Function(TimePickerEntryMode); /// selected [TimeOfDay] if the user taps the "OK" button, or null if the user /// taps the "CANCEL" button. The selected time is reported by calling /// [Navigator.pop]. +/// +/// Use [showTimePicker] to show a dialog already containing a [TimePickerDialog]. class TimePickerDialog extends StatefulWidget { /// Creates a Material Design time picker. - /// - /// [initialTime] must not be null. const TimePickerDialog({ super.key, required this.initialTime, @@ -2206,6 +2204,15 @@ class _TimePickerDialogState extends State with RestorationMix static const Size _kTimePickerMinLandscapeSize = Size(416, 248); static const Size _kTimePickerMinInputSize = Size(312, 196); + @override + void dispose() { + _selectedTime.dispose(); + _entryMode.dispose(); + _autovalidateMode.dispose(); + _orientation.dispose(); + super.dispose(); + } + @override String? get restorationId => widget.restorationId; @@ -2311,7 +2318,7 @@ class _TimePickerDialogState extends State with RestorationMix // Constrain the textScaleFactor to prevent layout issues. Since only some // parts of the time picker scale up with textScaleFactor, we cap the factor // to 1.1 as that provides enough space to reasonably fit all the content. - final double textScaleFactor = math.min(MediaQuery.textScaleFactorOf(context), 1.1); + final double textScaleFactor = MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.1).textScaleFactor; final Size timePickerSize; switch (_entryMode.value) { @@ -2384,6 +2391,7 @@ class _TimePickerDialogState extends State with RestorationMix overflowAlignment: OverflowBarAlignment.end, children: [ TextButton( + style: pickerTheme.cancelButtonStyle ?? defaultTheme.cancelButtonStyle, onPressed: _handleCancel, child: Text(widget.cancelText ?? (theme.useMaterial3 @@ -2391,6 +2399,7 @@ class _TimePickerDialogState extends State with RestorationMix : localizations.cancelButtonLabel.toUpperCase())), ), TextButton( + style: pickerTheme.confirmButtonStyle ?? defaultTheme.confirmButtonStyle, onPressed: _handleOk, child: Text(widget.confirmText ?? localizations.okButtonLabel), ), @@ -2584,6 +2593,13 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin { void dispose() { _vibrateTimer?.cancel(); _vibrateTimer = null; + _orientation.dispose(); + _selectedTime.dispose(); + _hourMinuteMode.dispose(); + _lastModeAnnounced.dispose(); + _autofocusHour.dispose(); + _autofocusMinute.dispose(); + _announcedInitialTime.dispose(); super.dispose(); } @@ -2876,8 +2892,9 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin { /// ``` /// {@end-tool} /// -/// The [context], [useRootNavigator] and [routeSettings] arguments are passed -/// to [showDialog], the documentation for which discusses how it is used. +/// The [context], [barrierDismissible], [barrierColor], [barrierLabel], +/// [useRootNavigator] and [routeSettings] arguments are passed to [showDialog], +/// the documentation for which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget to add /// inherited widgets like [Localizations.override], [Directionality], or @@ -2956,6 +2973,9 @@ Future showTimePicker({ required BuildContext context, required TimeOfDay initialTime, TransitionBuilder? builder, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, bool useRootNavigator = true, TimePickerEntryMode initialEntryMode = TimePickerEntryMode.dial, String? cancelText, @@ -2985,6 +3005,9 @@ Future showTimePicker({ ); return showDialog( context: context, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, useRootNavigator: useRootNavigator, builder: (BuildContext context) { return builder == null ? dialog : builder(context, dialog); @@ -3388,44 +3411,28 @@ class _TimePickerDefaultsM3 extends _TimePickerDefaults { @override Color get dayPeriodTextColor { return MaterialStateColor.resolveWith((Set states) { - return _dayPeriodForegroundColor.resolve(states); - }); - } - - MaterialStateProperty get _dayPeriodForegroundColor { - return MaterialStateProperty.resolveWith((Set states) { - Color? textColor; if (states.contains(MaterialState.selected)) { - if (states.contains(MaterialState.pressed)) { - textColor = _colors.onTertiaryContainer; - } else { - // not pressed - if (states.contains(MaterialState.hovered)) { - textColor = _colors.onTertiaryContainer; - } else { - // not hovered - if (states.contains(MaterialState.focused)) { - textColor = _colors.onTertiaryContainer; - } - } + if (states.contains(MaterialState.focused)) { + return _colors.onTertiaryContainer; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onTertiaryContainer; } - } else { - // unselected if (states.contains(MaterialState.pressed)) { - textColor = _colors.onSurfaceVariant; - } else { - // not pressed - if (states.contains(MaterialState.hovered)) { - textColor = _colors.onSurfaceVariant; - } else { - // not hovered - if (states.contains(MaterialState.focused)) { - textColor = _colors.onSurfaceVariant; - } - } + return _colors.onTertiaryContainer; } + return _colors.onTertiaryContainer; + } + if (states.contains(MaterialState.focused)) { + return _colors.onSurfaceVariant; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSurfaceVariant; + } + if (states.contains(MaterialState.pressed)) { + return _colors.onSurfaceVariant; } - return textColor ?? _colors.onTertiaryContainer; + return _colors.onSurfaceVariant; }); } @@ -3436,7 +3443,7 @@ class _TimePickerDefaultsM3 extends _TimePickerDefaults { @override Color get dialBackgroundColor { - return _colors.surfaceVariant.withOpacity(_colors.brightness == Brightness.dark ? 0.12 : 0.08); + return _colors.surfaceVariant; } @override @@ -3601,7 +3608,10 @@ class _TimePickerDefaultsM3 extends _TimePickerDefaults { @override TextStyle get hourMinuteTextStyle { return MaterialStateTextStyle.resolveWith((Set states) { - return _textTheme.displayLarge!.copyWith(color: _hourMinuteTextColor.resolve(states)); + // TODO(tahatesser): Update this when https://github.com/flutter/flutter/issues/131247 is fixed. + // This is using the correct text style from Material 3 spec. + // https://m3.material.io/components/time-pickers/specs#fd0b6939-edab-4058-82e1-93d163945215 + return _textTheme.displayMedium!.copyWith(color: _hourMinuteTextColor.resolve(states)); }); } diff --git a/packages/flutter/lib/src/material/time_picker_theme.dart b/packages/flutter/lib/src/material/time_picker_theme.dart index e340cf9edf402..30983f9ec14b4 100644 --- a/packages/flutter/lib/src/material/time_picker_theme.dart +++ b/packages/flutter/lib/src/material/time_picker_theme.dart @@ -72,7 +72,7 @@ class TimePickerThemeData with Diagnosticable { /// The style of the cancel button of a [TimePickerDialog]. final ButtonStyle? cancelButtonStyle; - /// The style of the conform (OK) button of a [TimePickerDialog]. + /// The style of the confirm (OK) button of a [TimePickerDialog]. final ButtonStyle? confirmButtonStyle; /// The color and weight of the day period's outline. @@ -136,8 +136,14 @@ class TimePickerThemeData with Diagnosticable { /// The background color of the time picker dial when the entry mode is /// [TimePickerEntryMode.dial] or [TimePickerEntryMode.dialOnly]. /// - /// If this is null, the time picker defaults to the overall theme's - /// [ColorScheme.primary]. + /// If this is null and [ThemeData.useMaterial3] is true, the time picker + /// dial background color defaults [ColorScheme.surfaceVariant] color. + /// + /// If this is null and [ThemeData.useMaterial3] is false, the time picker + /// dial background color defaults to [ColorScheme.onSurface] color with + /// an opacity of 0.08 when the overall theme's brightness is [Brightness.light] + /// and [ColorScheme.onSurface] color with an opacity of 0.12 when the overall + /// theme's brightness is [Brightness.dark]. final Color? dialBackgroundColor; /// The color of the time picker dial's hand when the entry mode is @@ -295,8 +301,6 @@ class TimePickerThemeData with Diagnosticable { /// Linearly interpolate between two time picker themes. /// - /// The argument `t` must not be null. - /// /// {@macro dart.ui.shadow.lerp} static TimePickerThemeData lerp(TimePickerThemeData? a, TimePickerThemeData? b, double t) { if (identical(a, b) && a != null) { diff --git a/packages/flutter/lib/src/material/toggle_buttons.dart b/packages/flutter/lib/src/material/toggle_buttons.dart index 4b786f3ca96da..731bf9781695e 100644 --- a/packages/flutter/lib/src/material/toggle_buttons.dart +++ b/packages/flutter/lib/src/material/toggle_buttons.dart @@ -205,9 +205,8 @@ class ToggleButtons extends StatelessWidget { /// /// Both [children] and [isSelected] properties arguments are required. /// - /// [isSelected] values must be non-null. [focusNodes] must be null or a - /// list of non-null nodes. [renderBorder] and [direction] must not be null. - /// If [direction] is [Axis.vertical], [verticalDirection] must not be null. + /// The [focusNodes] argument must be null or a list of nodes. If [direction] + /// is [Axis.vertical], [verticalDirection] must not be null. const ToggleButtons({ super.key, required this.children, diff --git a/packages/flutter/lib/src/material/toggle_buttons_theme.dart b/packages/flutter/lib/src/material/toggle_buttons_theme.dart index 199ec362928b7..24dbaa7aa1f53 100644 --- a/packages/flutter/lib/src/material/toggle_buttons_theme.dart +++ b/packages/flutter/lib/src/material/toggle_buttons_theme.dart @@ -246,8 +246,6 @@ class ToggleButtonsThemeData with Diagnosticable { class ToggleButtonsTheme extends InheritedTheme { /// Creates a toggle buttons theme that controls the color and border /// parameters for [ToggleButtons]. - /// - /// The data argument must not be null. const ToggleButtonsTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index daa9bdb7f3654..ecf19d37e4576 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -302,7 +302,7 @@ mixin ToggleableStateMixin on TickerProviderStateMixin /// build method - potentially after wrapping it in other widgets. Widget buildToggleable({ FocusNode? focusNode, - Function(bool)? onFocusChange, + ValueChanged? onFocusChange, bool autofocus = false, required MaterialStateProperty mouseCursor, required Size size, diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 2be81e1ba51cc..7db630ccb38b1 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -820,8 +820,6 @@ class TooltipState extends State with SingleTickerProviderStateMixin { /// below a target specified in the global coordinate system. class _TooltipPositionDelegate extends SingleChildLayoutDelegate { /// Creates a delegate for computing the layout of a tooltip. - /// - /// The arguments must not be null. _TooltipPositionDelegate({ required this.target, required this.verticalOffset, diff --git a/packages/flutter/lib/src/material/tooltip_theme.dart b/packages/flutter/lib/src/material/tooltip_theme.dart index 3f3c28dc539fd..a2542d6d085ff 100644 --- a/packages/flutter/lib/src/material/tooltip_theme.dart +++ b/packages/flutter/lib/src/material/tooltip_theme.dart @@ -262,8 +262,6 @@ class TooltipThemeData with Diagnosticable { class TooltipTheme extends InheritedTheme { /// Creates a tooltip theme that controls the configurations for /// [Tooltip]. - /// - /// The data argument must not be null. const TooltipTheme({ super.key, required this.data, diff --git a/packages/flutter/lib/src/material/tooltip_visibility.dart b/packages/flutter/lib/src/material/tooltip_visibility.dart index ff151324348a3..52642317a6007 100644 --- a/packages/flutter/lib/src/material/tooltip_visibility.dart +++ b/packages/flutter/lib/src/material/tooltip_visibility.dart @@ -26,8 +26,6 @@ class _TooltipVisibilityScope extends InheritedWidget { /// continues to provide any semantic information that is provided. class TooltipVisibility extends StatelessWidget { /// Creates a widget that configures the visibility of [Tooltip]. - /// - /// Both arguments must not be null. const TooltipVisibility({ super.key, required this.visible, diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart index 8105227deda09..a1fbb3f6e7a99 100644 --- a/packages/flutter/lib/src/painting/_network_image_io.dart +++ b/packages/flutter/lib/src/painting/_network_image_io.dart @@ -13,12 +13,13 @@ import 'debug.dart'; import 'image_provider.dart' as image_provider; import 'image_stream.dart'; +// Method signature for _loadAsync decode callbacks. +typedef _SimpleDecoderCallback = Future Function(ui.ImmutableBuffer buffer); + /// The dart:io implementation of [image_provider.NetworkImage]. @immutable class NetworkImage extends image_provider.ImageProvider implements image_provider.NetworkImage { /// Creates an object that fetches the image at the given URL. - /// - /// The arguments [url] and [scale] must not be null. const NetworkImage(this.url, { this.scale = 1.0, this.headers }); @override @@ -35,25 +36,6 @@ class NetworkImage extends image_provider.ImageProvider(this); } - @override - ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { - // Ownership of this controller is handed off to [_loadAsync]; it is that - // method's responsibility to close the controller's stream when the image - // has been loaded or an error is thrown. - final StreamController chunkEvents = StreamController(); - - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key as NetworkImage, chunkEvents, decodeDeprecated: decode), - chunkEvents: chunkEvents.stream, - scale: key.scale, - debugLabel: key.url, - informationCollector: () => [ - DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Image key', key), - ], - ); - } - @override ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) { // Ownership of this controller is handed off to [_loadAsync]; it is that @@ -62,7 +44,7 @@ class NetworkImage extends image_provider.ImageProvider chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key as NetworkImage, chunkEvents, decodeBufferDeprecated: decode), + codec: _loadAsync(key as NetworkImage, chunkEvents, decode: decode), chunkEvents: chunkEvents.stream, scale: key.scale, debugLabel: key.url, @@ -112,9 +94,7 @@ class NetworkImage extends image_provider.ImageProvider _loadAsync( NetworkImage key, StreamController chunkEvents, { - image_provider.ImageDecoderCallback? decode, - image_provider.DecoderBufferCallback? decodeBufferDeprecated, - image_provider.DecoderCallback? decodeDeprecated, + required _SimpleDecoderCallback decode, }) async { try { assert(key == this); @@ -148,16 +128,7 @@ class NetworkImage extends image_provider.ImageProvider Object.hash(url, scale); @override - String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)'; + String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: ${scale.toStringAsFixed(1)})'; } diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart index ceb012e65a80b..c3219046eb267 100644 --- a/packages/flutter/lib/src/painting/_network_image_web.dart +++ b/packages/flutter/lib/src/painting/_network_image_web.dart @@ -5,19 +5,23 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:ui' as ui; +import 'dart:ui_web' as ui_web; import 'package:flutter/foundation.dart'; +import 'package:web/web.dart' as web; -import '../services/dom.dart'; import 'image_provider.dart' as image_provider; import 'image_stream.dart'; /// Creates a type for an overridable factory function for testing purposes. -typedef HttpRequestFactory = DomXMLHttpRequest Function(); +typedef HttpRequestFactory = web.XMLHttpRequest Function(); + +// Method signature for _loadAsync decode callbacks. +typedef _SimpleDecoderCallback = Future Function(ui.ImmutableBuffer buffer); /// Default HTTP client. -DomXMLHttpRequest _httpClient() { - return DomXMLHttpRequest(); +web.XMLHttpRequest _httpClient() { + return web.XMLHttpRequest(); } /// Creates an overridable factory function. @@ -36,8 +40,6 @@ class NetworkImage extends image_provider.ImageProvider implements image_provider.NetworkImage { /// Creates an object that fetches the image at the given URL. - /// - /// The arguments [url] and [scale] must not be null. const NetworkImage(this.url, {this.scale = 1.0, this.headers}); @override @@ -54,23 +56,6 @@ class NetworkImage return SynchronousFuture(this); } - @override - ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { - // Ownership of this controller is handed off to [_loadAsync]; it is that - // method's responsibility to close the controller's stream when the image - // has been loaded or an error is thrown. - final StreamController chunkEvents = - StreamController(); - - return MultiFrameImageStreamCompleter( - chunkEvents: chunkEvents.stream, - codec: _loadAsync(key as NetworkImage, null, null, decode, chunkEvents), - scale: key.scale, - debugLabel: key.url, - informationCollector: _imageStreamInformationCollector(key), - ); - } - @override ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) { // Ownership of this controller is handed off to [_loadAsync]; it is that @@ -81,7 +66,7 @@ class NetworkImage return MultiFrameImageStreamCompleter( chunkEvents: chunkEvents.stream, - codec: _loadAsync(key as NetworkImage, null, decode, null, chunkEvents), + codec: _loadAsync(key as NetworkImage, decode, chunkEvents), scale: key.scale, debugLabel: key.url, informationCollector: _imageStreamInformationCollector(key), @@ -97,7 +82,7 @@ class NetworkImage return MultiFrameImageStreamCompleter( chunkEvents: chunkEvents.stream, - codec: _loadAsync(key as NetworkImage, decode, null, null, chunkEvents), + codec: _loadAsync(key as NetworkImage, decode, chunkEvents), scale: key.scale, debugLabel: key.url, informationCollector: _imageStreamInformationCollector(key), @@ -117,13 +102,11 @@ class NetworkImage } // Html renderer does not support decoding network images to a specified size. The decode parameter - // here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used - // directly in place of the typical `instantiateImageCodec` method. + // here is ignored and `ui_web.createImageCodecFromUrl` will be used directly + // in place of the typical `instantiateImageCodec` method. Future _loadAsync( NetworkImage key, - image_provider.ImageDecoderCallback? decode, - image_provider.DecoderBufferCallback? decodeBufferDeprecated, - image_provider.DecoderCallback? decodeDeprecated, + _SimpleDecoderCallback decode, StreamController chunkEvents, ) async { assert(key == this); @@ -133,11 +116,11 @@ class NetworkImage final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false; // We use a different method when headers are set because the - // `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers. + // `ui_web.createImageCodecFromUrl` method is not capable of handling headers. if (isCanvasKit || containsNetworkImageHeaders) { - final Completer completer = - Completer(); - final DomXMLHttpRequest request = httpRequestFactory(); + final Completer completer = + Completer(); + final web.XMLHttpRequest request = httpRequestFactory(); request.open('GET', key.url, true); request.responseType = 'arraybuffer'; @@ -147,9 +130,9 @@ class NetworkImage }); } - request.addEventListener('load', createDomEventListener((DomEvent e) { - final int? status = request.status; - final bool accepted = status! >= 200 && status < 300; + request.addEventListener('load', (web.Event e) { + final int status = request.status; + final bool accepted = status >= 200 && status < 300; final bool fileUri = status == 0; // file:// URIs have status of 0. final bool notModified = status == 304; final bool unknownRedirect = status > 307 && status < 400; @@ -161,12 +144,12 @@ class NetworkImage } else { completer.completeError(e); throw image_provider.NetworkImageLoadException( - statusCode: request.status ?? 400, uri: resolved); + statusCode: status, uri: resolved); } - })); + }.toJS); request.addEventListener('error', - createDomEventListener(completer.completeError)); + ((JSObject e) => completer.completeError(e)).toJS); request.send(); @@ -176,30 +159,17 @@ class NetworkImage if (bytes.lengthInBytes == 0) { throw image_provider.NetworkImageLoadException( - statusCode: request.status!, uri: resolved); - } - - if (decode != null) { - final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); - return decode(buffer); - } else if (decodeBufferDeprecated != null) { - final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); - return decodeBufferDeprecated(buffer); - } else { - assert(decodeDeprecated != null); - return decodeDeprecated!(bytes); + statusCode: request.status, uri: resolved); } + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); } else { - // This API only exists in the web engine implementation and is not - // contained in the analyzer summary for Flutter. - // ignore: undefined_function, avoid_dynamic_calls - return ui.webOnlyInstantiateImageCodecFromUrl( + return ui_web.createImageCodecFromUrl( resolved, chunkCallback: (int bytes, int total) { chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: bytes, expectedTotalBytes: total)); }, - ) as Future; + ); } } @@ -215,6 +185,5 @@ class NetworkImage int get hashCode => Object.hash(url, scale); @override - String toString() => - '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)'; + String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: ${scale.toStringAsFixed(1)})'; } diff --git a/packages/flutter/lib/src/painting/alignment.dart b/packages/flutter/lib/src/painting/alignment.dart index b323e9b1acef0..7509321d75b6f 100644 --- a/packages/flutter/lib/src/painting/alignment.dart +++ b/packages/flutter/lib/src/painting/alignment.dart @@ -185,8 +185,6 @@ abstract class AlignmentGeometry { /// whether the horizontal direction depends on the [TextDirection]. class Alignment extends AlignmentGeometry { /// Creates an alignment. - /// - /// The [x] and [y] arguments must not be null. const Alignment(this.x, this.y); /// The distance fraction in the horizontal direction. @@ -401,8 +399,6 @@ class Alignment extends AlignmentGeometry { /// whose horizontal component does not depend on the text direction). class AlignmentDirectional extends AlignmentGeometry { /// Creates a directional alignment. - /// - /// The [start] and [y] arguments must not be null. const AlignmentDirectional(this.start, this.y); /// The distance fraction in the horizontal direction. diff --git a/packages/flutter/lib/src/painting/beveled_rectangle_border.dart b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart index 0f5e4571e804b..3c900e808dd27 100644 --- a/packages/flutter/lib/src/painting/beveled_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart @@ -20,8 +20,6 @@ import 'borders.dart'; class BeveledRectangleBorder extends OutlinedBorder { /// Creates a border like a [RoundedRectangleBorder] except that the corners /// are joined by straight lines instead of arcs. - /// - /// The arguments must not be null. const BeveledRectangleBorder({ super.side, this.borderRadius = BorderRadius.zero, diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart index 880db6d33fa23..02efc9082ac7c 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart @@ -78,47 +78,6 @@ mixin PaintingBinding on BindingBase, ServicesBinding { @protected ImageCache createImageCache() => ImageCache(); - /// Calls through to [dart:ui.instantiateImageCodec] from [ImageCache]. - /// - /// This method is deprecated. use [instantiateImageCodecFromBuffer] with an - /// [ImmutableBuffer] instance instead of this method. - /// - /// The `cacheWidth` and `cacheHeight` parameters, when specified, indicate - /// the size to decode the image to. - /// - /// Both `cacheWidth` and `cacheHeight` must be positive values greater than - /// or equal to 1, or null. It is valid to specify only one of `cacheWidth` - /// and `cacheHeight` with the other remaining null, in which case the omitted - /// dimension will be scaled to maintain the aspect ratio of the original - /// dimensions. When both are null or omitted, the image will be decoded at - /// its native resolution. - /// - /// The `allowUpscaling` parameter determines whether the `cacheWidth` or - /// `cacheHeight` parameters are clamped to the intrinsic width and height of - /// the original image. By default, the dimensions are clamped to avoid - /// unnecessary memory usage for images. Callers that wish to display an image - /// above its native resolution should prefer scaling the canvas the image is - /// drawn into. - @Deprecated( - 'Use instantiateImageCodecWithSize with an ImmutableBuffer instance instead. ' - 'This feature was deprecated after v2.13.0-1.0.pre.', - ) - Future instantiateImageCodec( - Uint8List bytes, { - int? cacheWidth, - int? cacheHeight, - bool allowUpscaling = false, - }) { - assert(cacheWidth == null || cacheWidth > 0); - assert(cacheHeight == null || cacheHeight > 0); - return ui.instantiateImageCodec( - bytes, - targetWidth: cacheWidth, - targetHeight: cacheHeight, - allowUpscaling: allowUpscaling, - ); - } - /// Calls through to [dart:ui.instantiateImageCodecFromBuffer] from [ImageCache]. /// /// The [buffer] parameter should be an [ui.ImmutableBuffer] instance which can diff --git a/packages/flutter/lib/src/painting/borders.dart b/packages/flutter/lib/src/painting/borders.dart index b96713cdfcff8..63e7f3bf83748 100644 --- a/packages/flutter/lib/src/painting/borders.dart +++ b/packages/flutter/lib/src/painting/borders.dart @@ -77,8 +77,6 @@ class BorderSide with Diagnosticable { /// If one of the sides is zero-width with [BorderStyle.none], then the other /// side is return as-is. If both of the sides are zero-width with /// [BorderStyle.none], then [BorderSide.none] is returned. - /// - /// The arguments must not be null. static BorderSide merge(BorderSide a, BorderSide b) { assert(canMerge(a, b)); final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0; @@ -243,8 +241,6 @@ class BorderSide with Diagnosticable { /// /// Two sides can be merged if one or both are zero-width with /// [BorderStyle.none], or if they both have the same color and style. - /// - /// The arguments must not be null. static bool canMerge(BorderSide a, BorderSide b) { if ((a.style == BorderStyle.none && a.width == 0.0) || (b.style == BorderStyle.none && b.width == 0.0)) { @@ -256,8 +252,6 @@ class BorderSide with Diagnosticable { /// Linearly interpolate between two border sides. /// - /// The arguments must not be null. - /// /// {@macro dart.ui.shadow.lerp} static BorderSide lerp(BorderSide a, BorderSide b, double t) { if (identical(a, b)) { @@ -665,8 +659,6 @@ abstract class ShapeBorder { abstract class OutlinedBorder extends ShapeBorder { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. - /// - /// The value of [side] must not be null. const OutlinedBorder({ this.side = BorderSide.none }); @override @@ -840,7 +832,7 @@ class _CompoundBorder extends ShapeBorder { } @override - bool get preferPaintInterior => true; + bool get preferPaintInterior => borders.every((ShapeBorder border) => border.preferPaintInterior); @override void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) { @@ -884,8 +876,6 @@ class _CompoundBorder extends ShapeBorder { /// borders (where all the borders have the same configuration); to render a /// uniform border, consider using [Canvas.drawRect] directly. /// -/// The arguments must not be null. -/// /// See also: /// /// * [paintImage], which paints an image in a rectangle on a canvas. diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index dfc9a606a556b..7d2e5bab0c34d 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -242,16 +242,24 @@ abstract class BoxBorder extends ShapeBorder { } } - static void _paintNonUniformBorder( + /// Paints a Border with different widths, styles and strokeAligns, on any + /// borderRadius while using a single color. + /// + /// See also: + /// + /// * [paintBorder], which supports multiple colors but not borderRadius. + /// * [paint], which calls this method. + static void paintNonUniformBorder( Canvas canvas, Rect rect, { required BorderRadius? borderRadius, - required BoxShape shape, required TextDirection? textDirection, - required BorderSide left, - required BorderSide top, - required BorderSide right, - required BorderSide bottom, + BoxShape shape = BoxShape.rectangle, + BorderSide top = BorderSide.none, + BorderSide right = BorderSide.none, + BorderSide bottom = BorderSide.none, + BorderSide left = BorderSide.none, + required Color color, }) { final RRect borderRect; switch (shape) { @@ -266,7 +274,7 @@ abstract class BoxBorder extends ShapeBorder { Radius.circular(rect.width), ); } - final Paint paint = Paint()..color = top.color; + final Paint paint = Paint()..color = color; final RRect inner = _deflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset, right.strokeInset, bottom.strokeInset)); final RRect outer = _inflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeOutset, top.strokeOutset, right.strokeOutset, bottom.strokeOutset)); canvas.drawDRRect(outer, inner, paint); @@ -379,8 +387,6 @@ class Border extends BoxBorder { /// Creates a border. /// /// All the sides of the border default to [BorderSide.none]. - /// - /// The arguments must not be null. const Border({ this.top = BorderSide.none, this.right = BorderSide.none, @@ -389,8 +395,6 @@ class Border extends BoxBorder { }); /// Creates a border whose sides are all the same. - /// - /// The `side` argument must not be null. const Border.fromBorderSide(BorderSide side) : top = side, right = side, @@ -402,7 +406,7 @@ class Border extends BoxBorder { /// The `vertical` argument applies to the [left] and [right] sides, and the /// `horizontal` argument applies to the [top] and [bottom] sides. /// - /// All arguments default to [BorderSide.none] and must not be null. + /// All arguments default to [BorderSide.none]. const Border.symmetric({ BorderSide vertical = BorderSide.none, BorderSide horizontal = BorderSide.none, @@ -429,8 +433,6 @@ class Border extends BoxBorder { /// /// It is only valid to call this if [BorderSide.canMerge] returns true for /// the pairwise combination of each side on both [Border]s. - /// - /// The arguments must not be null. static Border merge(Border a, Border b) { assert(BorderSide.canMerge(a.top, b.top)); assert(BorderSide.canMerge(a.right, b.right)); @@ -489,6 +491,32 @@ class Border extends BoxBorder { && right.strokeAlign == topStrokeAlign; } + Set _distinctVisibleColors() { + final Set distinctVisibleColors = {}; + if (top.style != BorderStyle.none) { + distinctVisibleColors.add(top.color); + } + if (right.style != BorderStyle.none) { + distinctVisibleColors.add(right.color); + } + if (bottom.style != BorderStyle.none) { + distinctVisibleColors.add(bottom.color); + } + if (left.style != BorderStyle.none) { + distinctVisibleColors.add(left.color); + } + return distinctVisibleColors; + } + + // [BoxBorder.paintNonUniformBorder] is about 20% faster than [paintBorder], + // but [paintBorder] is able to draw hairline borders when width is zero + // and style is [BorderStyle.solid]. + bool get _hasHairlineBorder => + (top.style == BorderStyle.solid && top.width == 0.0) || + (right.style == BorderStyle.solid && right.width == 0.0) || + (bottom.style == BorderStyle.solid && bottom.width == 0.0) || + (left.style == BorderStyle.solid && left.width == 0.0); + @override Border? add(ShapeBorder other, { bool reversed = false }) { if (other is Border && @@ -603,31 +631,41 @@ class Border extends BoxBorder { } } - // Allow painting non-uniform borders if the color and style are uniform. - if (_colorIsUniform && _styleIsUniform) { - switch (top.style) { - case BorderStyle.none: - return; - case BorderStyle.solid: - BoxBorder._paintNonUniformBorder(canvas, rect, - shape: shape, - borderRadius: borderRadius, - textDirection: textDirection, - left: left, - top: top, - right: right, - bottom: bottom); - return; - } + if (_styleIsUniform && top.style == BorderStyle.none) { + return; } - assert(() { - if (borderRadius != null) { + // Allow painting non-uniform borders if the visible colors are uniform. + final Set visibleColors = _distinctVisibleColors(); + final bool hasHairlineBorder = _hasHairlineBorder; + // Paint a non uniform border if a single color is visible + // and (borderRadius is present) or (border is visible and width != 0.0). + if (visibleColors.length == 1 && + !hasHairlineBorder && + (shape == BoxShape.circle || + (borderRadius != null && borderRadius != BorderRadius.zero))) { + BoxBorder.paintNonUniformBorder(canvas, rect, + shape: shape, + borderRadius: borderRadius, + textDirection: textDirection, + top: top.style == BorderStyle.none ? BorderSide.none : top, + right: right.style == BorderStyle.none ? BorderSide.none : right, + bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom, + left: left.style == BorderStyle.none ? BorderSide.none : left, + color: visibleColors.first); + return; + } + + assert(() { + if (hasHairlineBorder) { + assert(borderRadius == null || borderRadius == BorderRadius.zero, + 'A hairline border like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.'); + } + if (borderRadius != null && borderRadius != BorderRadius.zero) { throw FlutterError.fromParts([ - ErrorSummary('A borderRadius can only be given on borders with uniform colors and styles.'), + ErrorSummary('A borderRadius can only be given on borders with uniform colors.'), ErrorDescription('The following is not uniform:'), if (!_colorIsUniform) ErrorDescription('BorderSide.color'), - if (!_styleIsUniform) ErrorDescription('BorderSide.style'), ]); } return true; @@ -635,10 +673,9 @@ class Border extends BoxBorder { assert(() { if (shape != BoxShape.rectangle) { throw FlutterError.fromParts([ - ErrorSummary('A Border can only be drawn as a circle on borders with uniform colors and styles.'), + ErrorSummary('A Border can only be drawn as a circle on borders with uniform colors.'), ErrorDescription('The following is not uniform:'), if (!_colorIsUniform) ErrorDescription('BorderSide.color'), - if (!_styleIsUniform) ErrorDescription('BorderSide.style'), ]); } return true; @@ -646,7 +683,7 @@ class Border extends BoxBorder { assert(() { if (!_strokeAlignIsUniform || top.strokeAlign != BorderSide.strokeAlignInside) { throw FlutterError.fromParts([ - ErrorSummary('A Border can only draw strokeAlign different than BorderSide.strokeAlignInside on borders with uniform colors and styles.'), + ErrorSummary('A Border can only draw strokeAlign different than BorderSide.strokeAlignInside on borders with uniform colors.'), ]); } return true; @@ -718,8 +755,6 @@ class BorderDirectional extends BoxBorder { /// the trailing edge. They are resolved during [paint]. /// /// All the sides of the border default to [BorderSide.none]. - /// - /// The arguments must not be null. const BorderDirectional({ this.top = BorderSide.none, this.start = BorderSide.none, @@ -732,8 +767,6 @@ class BorderDirectional extends BoxBorder { /// /// It is only valid to call this if [BorderSide.canMerge] returns true for /// the pairwise combination of each side on both [BorderDirectional]s. - /// - /// The arguments must not be null. static BorderDirectional merge(BorderDirectional a, BorderDirectional b) { assert(BorderSide.canMerge(a.top, b.top)); assert(BorderSide.canMerge(a.start, b.start)); @@ -806,6 +839,31 @@ class BorderDirectional extends BoxBorder { && end.strokeAlign == topStrokeAlign; } + Set _distinctVisibleColors() { + final Set distinctVisibleColors = {}; + if (top.style != BorderStyle.none) { + distinctVisibleColors.add(top.color); + } + if (end.style != BorderStyle.none) { + distinctVisibleColors.add(end.color); + } + if (bottom.style != BorderStyle.none) { + distinctVisibleColors.add(bottom.color); + } + if (start.style != BorderStyle.none) { + distinctVisibleColors.add(start.color); + } + + return distinctVisibleColors; + } + + + bool get _hasHairlineBorder => + (top.style == BorderStyle.solid && top.width == 0.0) || + (end.style == BorderStyle.solid && end.width == 0.0) || + (bottom.style == BorderStyle.solid && bottom.width == 0.0) || + (start.style == BorderStyle.solid && start.width == 0.0); + @override BoxBorder? add(ShapeBorder other, { bool reversed = false }) { if (other is BorderDirectional) { @@ -951,6 +1009,10 @@ class BorderDirectional extends BoxBorder { } } + if (_styleIsUniform && top.style == BorderStyle.none) { + return; + } + final BorderSide left, right; assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.'); switch (textDirection!) { @@ -962,27 +1024,31 @@ class BorderDirectional extends BoxBorder { right = end; } - // Allow painting non-uniform borders if the color and style are uniform. - if (_colorIsUniform && _styleIsUniform) { - switch (top.style) { - case BorderStyle.none: - return; - case BorderStyle.solid: - BoxBorder._paintNonUniformBorder(canvas, rect, - shape: shape, - borderRadius: borderRadius, - textDirection: textDirection, - left: left, - top: top, - right: right, - bottom: bottom); - return; - } + // Allow painting non-uniform borders if the visible colors are uniform. + final Set visibleColors = _distinctVisibleColors(); + final bool hasHairlineBorder = _hasHairlineBorder; + if (visibleColors.length == 1 && + !hasHairlineBorder && + (shape == BoxShape.circle || + (borderRadius != null && borderRadius != BorderRadius.zero))) { + BoxBorder.paintNonUniformBorder(canvas, rect, + shape: shape, + borderRadius: borderRadius, + textDirection: textDirection, + top: top.style == BorderStyle.none ? BorderSide.none : top, + right: right.style == BorderStyle.none ? BorderSide.none : right, + bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom, + left: left.style == BorderStyle.none ? BorderSide.none : left, + color: visibleColors.first); + return; } - assert(borderRadius == null, 'A borderRadius can only be given for borders with uniform colors and styles.'); - assert(shape == BoxShape.rectangle, 'A Border can only be drawn as a circle on borders with uniform colors and styles.'); - assert(_strokeAlignIsUniform && top.strokeAlign == BorderSide.strokeAlignInside, 'A Border can only draw strokeAlign different than strokeAlignInside on borders with uniform colors and styles.'); + if (hasHairlineBorder) { + assert(borderRadius == null || borderRadius == BorderRadius.zero, 'A side like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.'); + } + assert(borderRadius == null, 'A borderRadius can only be given for borders with uniform colors.'); + assert(shape == BoxShape.rectangle, 'A Border can only be drawn as a circle on borders with uniform colors.'); + assert(_strokeAlignIsUniform && top.strokeAlign == BorderSide.strokeAlignInside, 'A Border can only draw strokeAlign different than strokeAlignInside on borders with uniform colors.'); paintBorder(canvas, rect, top: top, left: left, bottom: bottom, right: right); } diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index 444eb40608130..26328607ccf3f 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -84,8 +84,6 @@ class BoxDecoration extends Decoration { /// * If [boxShadow] is null, this decoration does not paint a shadow. /// * If [gradient] is null, this decoration does not paint gradients. /// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver] - /// - /// The [shape] argument must not be null. const BoxDecoration({ this.color, this.image, @@ -232,7 +230,7 @@ class BoxDecoration extends Decoration { BoxDecoration scale(double factor) { return BoxDecoration( color: Color.lerp(null, color, factor), - image: image, // TODO(ianh): fade the image from transparent + image: DecorationImage.lerp(null, image, factor), border: BoxBorder.lerp(null, border, factor), borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor), boxShadow: BoxShadow.lerpList(null, boxShadow, factor), @@ -307,7 +305,7 @@ class BoxDecoration extends Decoration { } return BoxDecoration( color: Color.lerp(a.color, b.color, t), - image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image + image: DecorationImage.lerp(a.image, b.image, t), border: BoxBorder.lerp(a.border, b.border, t), borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t), boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t), diff --git a/packages/flutter/lib/src/painting/circle_border.dart b/packages/flutter/lib/src/painting/circle_border.dart index 69b5aeb63c370..afec6883efbcc 100644 --- a/packages/flutter/lib/src/painting/circle_border.dart +++ b/packages/flutter/lib/src/painting/circle_border.dart @@ -30,8 +30,6 @@ import 'borders.dart'; /// * [Border], which, when used with [BoxDecoration], can also describe a circle. class CircleBorder extends OutlinedBorder { /// Create a circle border. - /// - /// The [side] argument must not be null. const CircleBorder({ super.side, this.eccentricity = 0.0 }) : assert(eccentricity >= 0.0, 'The eccentricity argument $eccentricity is not greater than or equal to zero.'), assert(eccentricity <= 1.0, 'The eccentricity argument $eccentricity is not less than or equal to one.'); diff --git a/packages/flutter/lib/src/painting/colors.dart b/packages/flutter/lib/src/painting/colors.dart index beb614b25b8c6..f5c9e06bd8856 100644 --- a/packages/flutter/lib/src/painting/colors.dart +++ b/packages/flutter/lib/src/painting/colors.dart @@ -86,8 +86,8 @@ Color _colorFromHue( class HSVColor { /// Creates a color. /// - /// All the arguments must not be null and be in their respective ranges. See - /// the fields for each parameter for a description of their ranges. + /// All the arguments must be in their respective ranges. See the fields for + /// each parameter for a description of their ranges. const HSVColor.fromAHSV(this.alpha, this.hue, this.saturation, this.value) : assert(alpha >= 0.0), assert(alpha <= 1.0), @@ -254,8 +254,8 @@ class HSVColor { class HSLColor { /// Creates a color. /// - /// All the arguments must not be null and be in their respective ranges. See - /// the fields for each parameter for a description of their ranges. + /// All the arguments must be in their respective ranges. See the fields for + /// each parameter for a description of their ranges. const HSLColor.fromAHSL(this.alpha, this.hue, this.saturation, this.lightness) : assert(alpha >= 0.0), assert(alpha <= 1.0), @@ -499,8 +499,6 @@ class ColorSwatch extends Color { /// [DiagnosticsProperty] that has an [Color] as value. class ColorProperty extends DiagnosticsProperty { /// Create a diagnostics property for [Color]. - /// - /// The [showName], [style], and [level] arguments must not be null. ColorProperty( String super.name, super.value, { diff --git a/packages/flutter/lib/src/painting/continuous_rectangle_border.dart b/packages/flutter/lib/src/painting/continuous_rectangle_border.dart index 302203175b4f2..b7612e43cb490 100644 --- a/packages/flutter/lib/src/painting/continuous_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/continuous_rectangle_border.dart @@ -33,7 +33,7 @@ import 'edge_insets.dart'; /// radius in a step function instead of gradually like the /// [ContinuousRectangleBorder]. class ContinuousRectangleBorder extends OutlinedBorder { - /// The arguments must not be null. + /// Creates a [ContinuousRectangleBorder]. const ContinuousRectangleBorder({ super.side, this.borderRadius = BorderRadius.zero, diff --git a/packages/flutter/lib/src/painting/decoration_image.dart b/packages/flutter/lib/src/painting/decoration_image.dart index 87ed9e0eefeaf..62ea98b8cec9c 100644 --- a/packages/flutter/lib/src/painting/decoration_image.dart +++ b/packages/flutter/lib/src/painting/decoration_image.dart @@ -40,9 +40,6 @@ enum ImageRepeat { @immutable class DecorationImage { /// Creates an image to show in a [BoxDecoration]. - /// - /// The [image], [alignment], [repeat], and [matchTextDirection] arguments - /// must not be null. const DecorationImage({ required this.image, this.onError, @@ -173,11 +170,11 @@ class DecorationImage { /// Creates a [DecorationImagePainter] for this [DecorationImage]. /// - /// The `onChanged` argument must not be null. It will be called whenever the - /// image needs to be repainted, e.g. because it is loading incrementally or - /// because it is animated. + /// The `onChanged` argument will be called whenever the image needs to be + /// repainted, e.g. because it is loading incrementally or because it is + /// animated. DecorationImagePainter createPainter(VoidCallback onChanged) { - return DecorationImagePainter._(this, onChanged); + return _DecorationImagePainter._(this, onChanged); } @override @@ -236,8 +233,8 @@ class DecorationImage { '$repeat', if (matchTextDirection) 'match text direction', - 'scale $scale', - 'opacity $opacity', + 'scale ${scale.toStringAsFixed(1)}', + 'opacity ${opacity.toStringAsFixed(1)}', '$filterQuality', if (invertColors) 'invert colors', @@ -246,6 +243,28 @@ class DecorationImage { ]; return '${objectRuntimeType(this, 'DecorationImage')}(${properties.join(", ")})'; } + + /// Linearly interpolates between two [DecorationImage]s. + /// + /// The `t` argument represents position on the timeline, with 0.0 meaning + /// that the interpolation has not started, returning `a`, 1.0 meaning that + /// the interpolation has finished, returning `b`, and values in between + /// meaning that the interpolation is at the relevant point on the timeline + /// between `a` and `this`. The interpolation can be extrapolated beyond 0.0 + /// and 1.0, so negative values and values greater than 1.0 are valid (and can + /// easily be generated by curves such as [Curves.elasticInOut]). + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + static DecorationImage? lerp(DecorationImage? a, DecorationImage? b, double t) { + if (identical(a, b) || t == 0.0) { + return a; + } + if (t == 1.0) { + return b; + } + return _BlendedDecorationImage(a, b, t); + } } /// The painter for a [DecorationImage]. @@ -259,15 +278,7 @@ class DecorationImage { /// /// This object should be disposed using the [dispose] method when it is no /// longer needed. -class DecorationImagePainter { - DecorationImagePainter._(this._details, this._onChanged); - - final DecorationImage _details; - final VoidCallback _onChanged; - - ImageStream? _imageStream; - ImageInfo? _image; - +abstract interface class DecorationImagePainter { /// Draw the image onto the given canvas. /// /// The image is drawn at the position and size given by the `rect` argument. @@ -282,8 +293,34 @@ class DecorationImagePainter { /// because it had not yet been loaded the first time this method was called, /// then the `onChanged` callback passed to [DecorationImage.createPainter] /// will be called. - void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration) { + /// + /// The `blend` argument specifies the opacity that should be applied to the + /// image due to this image being blended with another. The `blendMode` + /// argument can be specified to override the [DecorationImagePainter]'s + /// default [BlendMode] behavior. It is usually set to [BlendMode.srcOver] if + /// this is the first or only image being blended, and [BlendMode.plus] if it + /// is being blended with an image below. + void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }); + + /// Releases the resources used by this painter. + /// + /// This should be called whenever the painter is no longer needed. + /// + /// After this method has been called, the object is no longer usable. + void dispose(); +} + +class _DecorationImagePainter implements DecorationImagePainter { + _DecorationImagePainter._(this._details, this._onChanged); + final DecorationImage _details; + final VoidCallback _onChanged; + + ImageStream? _imageStream; + ImageInfo? _image; + + @override + void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) { bool flipHorizontally = false; if (_details.matchTextDirection) { assert(() { @@ -338,10 +375,11 @@ class DecorationImagePainter { centerSlice: _details.centerSlice, repeat: _details.repeat, flipHorizontally: flipHorizontally, - opacity: _details.opacity, + opacity: _details.opacity * blend, filterQuality: _details.filterQuality, invertColors: _details.invertColors, isAntiAlias: _details.isAntiAlias, + blendMode: blendMode, ); if (clipPath != null) { @@ -364,12 +402,7 @@ class DecorationImagePainter { } } - /// Releases the resources used by this painter. - /// - /// This should be called whenever the painter is no longer needed. - /// - /// After this method has been called, the object is no longer usable. - @mustCallSuper + @override void dispose() { _imageStream?.removeListener(ImageStreamListener( _handleImage, @@ -444,7 +477,7 @@ void debugFlushLastFrameImageSizeInfo() { /// corners of the destination rectangle defined by applying `fit`. The /// remaining five regions are drawn by stretching them to fit such that they /// exactly cover the destination rectangle while maintaining their relative -/// positions. +/// positions. See also [Canvas.drawImageNine]. /// /// * `repeat`: If the image does not fill `rect`, whether and how the image /// should be repeated to fill `rect`. By default, the image is not repeated. @@ -466,9 +499,6 @@ void debugFlushLastFrameImageSizeInfo() { /// bilinear interpolation, rather than the default [FilterQuality.none] which corresponds /// to nearest-neighbor. /// -/// The `canvas`, `rect`, `image`, `scale`, `alignment`, `repeat`, `flipHorizontally` and `filterQuality` -/// arguments must not be null. -/// /// See also: /// /// * [paintBorder], which paints a border around a rectangle on a canvas. @@ -490,6 +520,7 @@ void paintImage({ bool invertColors = false, FilterQuality filterQuality = FilterQuality.low, bool isAntiAlias = false, + BlendMode blendMode = BlendMode.srcOver, }) { assert( image.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true, @@ -530,9 +561,10 @@ void paintImage({ if (colorFilter != null) { paint.colorFilter = colorFilter; } - paint.color = Color.fromRGBO(0, 0, 0, opacity); + paint.color = Color.fromRGBO(0, 0, 0, clampDouble(opacity, 0.0, 1.0)); paint.filterQuality = filterQuality; paint.invertColors = invertColors; + paint.blendMode = blendMode; final double halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; final double halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; final double dx = halfWidthDelta + (flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta; @@ -543,6 +575,12 @@ void paintImage({ // Set to true if we added a saveLayer to the canvas to invert/flip the image. bool invertedCanvas = false; // Output size and destination rect are fully calculated. + + // Implement debug-mode and profile-mode features: + // - cacheWidth/cacheHeight warning + // - debugInvertOversizedImages + // - debugOnPaintImage + // - Flutter.ImageSizesForFrame events in timeline if (!kReleaseMode) { // We can use the devicePixelRatio of the views directly here (instead of // going through a MediaQuery) because if it changes, whatever is aware of @@ -554,7 +592,6 @@ void paintImage({ 0.0, (double previousValue, ui.FlutterView view) => math.max(previousValue, view.devicePixelRatio), ); - final ImageSizeInfo sizeInfo = ImageSizeInfo( // Some ImageProvider implementations may not have given this. source: debugImageLabel ?? '', @@ -599,7 +636,7 @@ void paintImage({ return true; }()); // Avoid emitting events that are the same as those emitted in the last frame. - if (!kReleaseMode && !_lastFrameImageSizeInfo.contains(sizeInfo)) { + if (!_lastFrameImageSizeInfo.contains(sizeInfo)) { final ImageSizeInfo? existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source]; if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) { _pendingImageSizeInfo[sizeInfo.source!] = sizeInfo; @@ -691,3 +728,101 @@ Iterable _generateImageTileRects(Rect outputRect, Rect fundamentalRect, Im } Rect _scaleRect(Rect rect, double scale) => Rect.fromLTRB(rect.left * scale, rect.top * scale, rect.right * scale, rect.bottom * scale); + +// Implements DecorationImage.lerp when the image is different. +// +// This class just paints both decorations on top of each other, blended together. +// +// The Decoration properties are faked by just forwarded to the target image. +class _BlendedDecorationImage implements DecorationImage { + const _BlendedDecorationImage(this.a, this.b, this.t) : assert(a != null || b != null); + + final DecorationImage? a; + final DecorationImage? b; + final double t; + + @override + ImageProvider get image => b?.image ?? a!.image; + @override + ImageErrorListener? get onError => b?.onError ?? a!.onError; + @override + ColorFilter? get colorFilter => b?.colorFilter ?? a!.colorFilter; + @override + BoxFit? get fit => b?.fit ?? a!.fit; + @override + AlignmentGeometry get alignment => b?.alignment ?? a!.alignment; + @override + Rect? get centerSlice => b?.centerSlice ?? a!.centerSlice; + @override + ImageRepeat get repeat => b?.repeat ?? a!.repeat; + @override + bool get matchTextDirection => b?.matchTextDirection ?? a!.matchTextDirection; + @override + double get scale => b?.scale ?? a!.scale; + @override + double get opacity => b?.opacity ?? a!.opacity; + @override + FilterQuality get filterQuality => b?.filterQuality ?? a!.filterQuality; + @override + bool get invertColors => b?.invertColors ?? a!.invertColors; + @override + bool get isAntiAlias => b?.isAntiAlias ?? a!.isAntiAlias; + + @override + DecorationImagePainter createPainter(VoidCallback onChanged) { + return _BlendedDecorationImagePainter._( + a?.createPainter(onChanged), + b?.createPainter(onChanged), + t, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is _BlendedDecorationImage + && other.a == a + && other.b == b + && other.t == t; + } + + @override + int get hashCode => Object.hash(a, b, t); + + @override + String toString() { + return '${objectRuntimeType(this, '_BlendedDecorationImage')}($a, $b, $t)'; + } +} + +class _BlendedDecorationImagePainter implements DecorationImagePainter { + _BlendedDecorationImagePainter._(this.a, this.b, this.t); + + final DecorationImagePainter? a; + final DecorationImagePainter? b; + final double t; + + @override + void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) { + canvas.saveLayer(null, Paint()); + a?.paint(canvas, rect, clipPath, configuration, blend: blend * (1.0 - t), blendMode: blendMode); + b?.paint(canvas, rect, clipPath, configuration, blend: blend * t, blendMode: a != null ? BlendMode.plus : blendMode); + canvas.restore(); + } + + @override + void dispose() { + a?.dispose(); + b?.dispose(); + } + + @override + String toString() { + return '${objectRuntimeType(this, '_BlendedDecorationImagePainter')}($a, $b, $t)'; + } +} diff --git a/packages/flutter/lib/src/painting/flutter_logo.dart b/packages/flutter/lib/src/painting/flutter_logo.dart index 8bee6d9e60b91..81f08eeeb034a 100644 --- a/packages/flutter/lib/src/painting/flutter_logo.dart +++ b/packages/flutter/lib/src/painting/flutter_logo.dart @@ -38,8 +38,6 @@ class FlutterLogoDecoration extends Decoration { /// /// The [style] controls whether and where to draw the "Flutter" label. If one /// is shown, the [textColor] controls the color of the label. - /// - /// The [textColor], [style], and [margin] arguments must not be null. const FlutterLogoDecoration({ this.textColor = const Color(0xFF757575), this.style = FlutterLogoStyle.markOnly, diff --git a/packages/flutter/lib/src/painting/fractional_offset.dart b/packages/flutter/lib/src/painting/fractional_offset.dart index fffdcd36b1f20..174e3b522b5f5 100644 --- a/packages/flutter/lib/src/painting/fractional_offset.dart +++ b/packages/flutter/lib/src/painting/fractional_offset.dart @@ -53,8 +53,6 @@ import 'basic_types.dart'; @immutable class FractionalOffset extends Alignment { /// Creates a fractional offset. - /// - /// The [dx] and [dy] arguments must not be null. const FractionalOffset(double dx, double dy) : super(dx * 2.0 - 1.0, dy * 2.0 - 1.0); diff --git a/packages/flutter/lib/src/painting/geometry.dart b/packages/flutter/lib/src/painting/geometry.dart index e57d3cfad2a89..fb12ba1065f7c 100644 --- a/packages/flutter/lib/src/painting/geometry.dart +++ b/packages/flutter/lib/src/painting/geometry.dart @@ -35,8 +35,6 @@ import 'basic_types.dart'; /// container. /// /// Used by [Tooltip] to position a tooltip relative to its parent. -/// -/// The arguments must not be null. Offset positionDependentBox({ required Size size, required Size childSize, diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index f740eab9797d7..e1545edecdc72 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -149,8 +149,8 @@ class GradientRotation extends GradientTransform { abstract class Gradient { /// Initialize the gradient's colors and stops. /// - /// The [colors] argument must not be null, and must have at least two colors - /// (the length is not verified until the [createShader] method is called). + /// The [colors] argument must have at least two colors (the length is not + /// verified until the [createShader] method is called). /// /// If specified, the [stops] argument must have the same number of entries as /// [colors] (this is also not verified until the [createShader] method is @@ -374,8 +374,7 @@ abstract class Gradient { class LinearGradient extends Gradient { /// Creates a linear gradient. /// - /// The [colors] argument must not be null. If [stops] is non-null, it must - /// have the same length as [colors]. + /// If [stops] is non-null, it must have the same length as [colors]. const LinearGradient({ this.begin = Alignment.centerLeft, this.end = Alignment.centerRight, @@ -625,8 +624,7 @@ class LinearGradient extends Gradient { class RadialGradient extends Gradient { /// Creates a radial gradient. /// - /// The [colors] argument must not be null. If [stops] is non-null, it must - /// have the same length as [colors]. + /// If [stops] is non-null, it must have the same length as [colors]. const RadialGradient({ this.center = Alignment.center, this.radius = 0.5, @@ -707,7 +705,7 @@ class RadialGradient extends Gradient { radius * rect.shortestSide, colors, _impliedStops(), tileMode, _resolveTransform(rect, textDirection), - focal == null ? null : focal!.resolve(textDirection).withinRect(rect), + focal?.resolve(textDirection).withinRect(rect), focalRadius * rect.shortestSide, ); } @@ -921,8 +919,7 @@ class RadialGradient extends Gradient { class SweepGradient extends Gradient { /// Creates a sweep gradient. /// - /// The [colors] argument must not be null. If [stops] is non-null, it must - /// have the same length as [colors]. + /// If [stops] is non-null, it must have the same length as [colors]. const SweepGradient({ this.center = Alignment.center, this.startAngle = 0.0, diff --git a/packages/flutter/lib/src/painting/image_cache.dart b/packages/flutter/lib/src/painting/image_cache.dart index d14bab2ccd40b..f8ddad84f465b 100644 --- a/packages/flutter/lib/src/painting/image_cache.dart +++ b/packages/flutter/lib/src/painting/image_cache.dart @@ -103,9 +103,9 @@ class ImageCache { if (value == maximumSize) { return; } - TimelineTask? timelineTask; + TimelineTask? debugTimelineTask; if (!kReleaseMode) { - timelineTask = TimelineTask()..start( + debugTimelineTask = TimelineTask()..start( 'ImageCache.setMaximumSize', arguments: {'value': value}, ); @@ -114,10 +114,10 @@ class ImageCache { if (maximumSize == 0) { clear(); } else { - _checkCacheSize(timelineTask); + _checkCacheSize(debugTimelineTask); } if (!kReleaseMode) { - timelineTask!.finish(); + debugTimelineTask!.finish(); } } @@ -142,9 +142,9 @@ class ImageCache { if (value == _maximumSizeBytes) { return; } - TimelineTask? timelineTask; + TimelineTask? debugTimelineTask; if (!kReleaseMode) { - timelineTask = TimelineTask()..start( + debugTimelineTask = TimelineTask()..start( 'ImageCache.setMaximumSizeBytes', arguments: {'value': value}, ); @@ -153,10 +153,10 @@ class ImageCache { if (_maximumSizeBytes == 0) { clear(); } else { - _checkCacheSize(timelineTask); + _checkCacheSize(debugTimelineTask); } if (!kReleaseMode) { - timelineTask!.finish(); + debugTimelineTask!.finish(); } } @@ -231,8 +231,7 @@ class ImageCache { /// completely discarded by the cache. It should be set to false when calls /// to evict are trying to relieve memory pressure, since an image with a /// listener will not actually be evicted from memory, and subsequent attempts - /// to load it will end up allocating more memory for the image again. The - /// argument must not be null. + /// to load it will end up allocating more memory for the image again. /// /// See also: /// @@ -283,7 +282,6 @@ class ImageCache { /// Resizes the cache as appropriate to maintain the constraints of /// [maximumSize] and [maximumSizeBytes]. void _touch(Object key, _CachedImage image, TimelineTask? timelineTask) { - assert(timelineTask != null); if (image.sizeBytes != null && image.sizeBytes! <= maximumSizeBytes && maximumSize > 0) { _currentSizeBytes += image.sizeBytes!; _cache[key] = image; @@ -314,8 +312,6 @@ class ImageCache { /// if not, calls the given callback to obtain it first. In either case, the /// key is moved to the 'most recently used' position. /// - /// The arguments must not be null. The `loader` cannot return null. - /// /// In the event that the loader throws an exception, it will be caught only if /// `onError` is also provided. When an exception is caught resolving an image, /// no completers are cached and `null` is returned instead of a new @@ -324,9 +320,9 @@ class ImageCache { /// Images that are larger than [maximumSizeBytes] are not cached, and do not /// cause other images in the cache to be evicted. ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) { - TimelineTask? timelineTask; + TimelineTask? debugTimelineTask; if (!kReleaseMode) { - timelineTask = TimelineTask()..start( + debugTimelineTask = TimelineTask()..start( 'ImageCache.putIfAbsent', arguments: { 'key': key.toString(), @@ -337,7 +333,7 @@ class ImageCache { // Nothing needs to be done because the image hasn't loaded yet. if (result != null) { if (!kReleaseMode) { - timelineTask!.finish(arguments: {'result': 'pending'}); + debugTimelineTask!.finish(arguments: {'result': 'pending'}); } return result; } @@ -348,7 +344,7 @@ class ImageCache { final _CachedImage? image = _cache.remove(key); if (image != null) { if (!kReleaseMode) { - timelineTask!.finish(arguments: {'result': 'keepAlive'}); + debugTimelineTask!.finish(arguments: {'result': 'keepAlive'}); } // The image might have been keptAlive but had no listeners (so not live). // Make sure the cache starts tracking it as live again. @@ -369,10 +365,10 @@ class ImageCache { liveImage.completer, sizeBytes: liveImage.sizeBytes, ), - timelineTask, + debugTimelineTask, ); if (!kReleaseMode) { - timelineTask!.finish(arguments: {'result': 'keepAlive'}); + debugTimelineTask!.finish(arguments: {'result': 'keepAlive'}); } return liveImage.completer; } @@ -382,7 +378,7 @@ class ImageCache { _trackLiveImage(key, result, null); } catch (error, stackTrace) { if (!kReleaseMode) { - timelineTask!.finish(arguments: { + debugTimelineTask!.finish(arguments: { 'result': 'error', 'error': error.toString(), 'stackTrace': stackTrace.toString(), @@ -397,7 +393,7 @@ class ImageCache { } if (!kReleaseMode) { - timelineTask!.start('listener'); + debugTimelineTask!.start('listener'); } // A multi-frame provider may call the listener more than once. We need do make // sure that some cleanup works won't run multiple times, such as finishing the @@ -424,7 +420,7 @@ class ImageCache { // Only touch if the cache was enabled when resolve was initially called. if (trackPendingImage) { - _touch(key, image, timelineTask); + _touch(key, image, debugTimelineTask); } else { image.dispose(); } @@ -434,7 +430,7 @@ class ImageCache { pendingImage.removeListener(); } if (!kReleaseMode && !listenedOnce) { - timelineTask! + debugTimelineTask! ..finish(arguments: { 'syncCall': syncCall, 'sizeInBytes': sizeBytes, diff --git a/packages/flutter/lib/src/painting/image_decoder.dart b/packages/flutter/lib/src/painting/image_decoder.dart index 39d926ee341a1..328c840f7393f 100644 --- a/packages/flutter/lib/src/painting/image_decoder.dart +++ b/packages/flutter/lib/src/painting/image_decoder.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:typed_data'; -import 'dart:ui' as ui show Codec, FrameInfo, Image; +import 'dart:ui' as ui show Codec, FrameInfo, Image, ImmutableBuffer; import 'binding.dart'; @@ -17,10 +17,11 @@ import 'binding.dart'; /// [instantiateImageCodec] if support for animated images is necessary. /// /// This function differs from [ui.decodeImageFromList] in that it defers to -/// [PaintingBinding.instantiateImageCodec], and therefore can be mocked in -/// tests. +/// [PaintingBinding.instantiateImageCodecWithSize], and therefore can be mocked +/// in tests. Future decodeImageFromList(Uint8List bytes) async { - final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodec(bytes); + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; } diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index ebc583fb71eba..50df1a66e8db1 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -6,8 +6,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; -import 'dart:ui' show Locale, Size, TextDirection; - import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -54,7 +52,7 @@ class ImageConfiguration { ImageConfiguration copyWith({ AssetBundle? bundle, double? devicePixelRatio, - Locale? locale, + ui.Locale? locale, TextDirection? textDirection, Size? size, TargetPlatform? platform, @@ -77,7 +75,7 @@ class ImageConfiguration { final double? devicePixelRatio; /// The language and region for which to select the image. - final Locale? locale; + final ui.Locale? locale; /// The reading direction of the language for which to select the image. final TextDirection? textDirection; @@ -162,25 +160,6 @@ class ImageConfiguration { } } -/// Performs the decode process for use in [ImageProvider.load]. -/// -/// This typedef is deprecated. Use [DecoderBufferCallback] with -/// [ImageProvider.loadBuffer] instead. -/// -/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and -/// `allowUpscaling` parameters from implementations of [ImageProvider] that do -/// not expose them. -/// -/// See also: -/// -/// * [ResizeImage], which uses this to override the `cacheWidth`, -/// `cacheHeight`, and `allowUpscaling` parameters. -@Deprecated( - 'Use ImageDecoderCallback with ImageProvider.loadImage instead. ' - 'This feature was deprecated after v2.13.0-1.0.pre.', -) -typedef DecoderCallback = Future Function(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling}); - /// Performs the decode process for use in [ImageProvider.loadBuffer]. /// /// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and @@ -197,6 +176,9 @@ typedef DecoderCallback = Future Function(Uint8List buffer, {int? cach ) typedef DecoderBufferCallback = Future Function(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling}); +// Method signature for _loadAsync decode callbacks. +typedef _SimpleDecoderCallback = Future Function(ui.ImmutableBuffer buffer); + /// Performs the decode process for use in [ImageProvider.loadImage]. /// /// This callback allows decoupling of the `getTargetSize` parameter from @@ -249,16 +231,16 @@ typedef ImageDecoderCallback = Future Function( /// using that key. This is handled by [resolveStreamForKey]. That method /// may fizzle if it determines the image is no longer necessary, use the /// provided [ImageErrorListener] to report an error, set the completer -/// from the cache if possible, or call [loadBuffer] to fetch the encoded image +/// from the cache if possible, or call [loadImage] to fetch the encoded image /// bytes and schedule decoding. -/// 4. The [loadBuffer] method is responsible for both fetching the encoded bytes -/// and decoding them using the provided [DecoderCallback]. It is called +/// 4. The [loadImage] method is responsible for both fetching the encoded bytes +/// and decoding them using the provided [ImageDecoderCallback]. It is called /// in a context that uses the [ImageErrorListener] to report errors back. /// -/// Subclasses normally only have to implement the [loadBuffer] and [obtainKey] +/// Subclasses normally only have to implement the [loadImage] and [obtainKey] /// methods. A subclass that needs finer grained control over the [ImageStream] /// type must override [createStream]. A subclass that needs finer grained -/// control over the resolution, such as delaying calling [loadBuffer], must override +/// control over the resolution, such as delaying calling [loadImage], must override /// [resolveStreamForKey]. /// /// The [resolve] method is marked as [nonVirtual] so that [ImageProvider]s can @@ -346,6 +328,16 @@ typedef ImageDecoderCallback = Future Function( /// } /// ``` /// {@end-tool} +/// +/// ## Creating an [ImageProvider] +/// +/// {@tool dartpad} +/// In this example, a variant of [NetworkImage] is created that passes all the +/// [ImageConfiguration] information (locale, platform, size, etc) to the server +/// using query arguments in the image URL. +/// +/// ** See code in examples/api/lib/painting/image_provider/image_provider.0.dart ** +/// {@end-tool} @optionalTypeArgs abstract class ImageProvider { /// Abstract const constructor. This constructor enables subclasses to provide @@ -357,10 +349,10 @@ abstract class ImageProvider { /// /// This is the public entry-point of the [ImageProvider] class hierarchy. /// - /// Subclasses should implement [obtainKey] and [load], which are used by this - /// method. If they need to change the implementation of [ImageStream] used, - /// they should override [createStream]. If they need to manage the actual - /// resolution of the image, they should override [resolveStreamForKey]. + /// Subclasses should implement [obtainKey] and [loadImage], which are used by + /// this method. If they need to change the implementation of [ImageStream] + /// used, they should override [createStream]. If they need to manage the + /// actual resolution of the image, they should override [resolveStreamForKey]. /// /// See the Lifecycle documentation on [ImageProvider] for more information. @nonVirtual @@ -414,8 +406,7 @@ abstract class ImageProvider { /// The location may be [ImageCacheStatus.untracked], indicating that this /// image provider's key is not available in the [ImageCache]. /// - /// The `cache` and `configuration` parameters must not be null. If the - /// `handleError` parameter is null, errors will be reported to + /// If the `handleError` parameter is null, errors will be reported to /// [FlutterError.onError], and the method will return null. /// /// A completed return value of null indicates that an error has occurred. @@ -534,10 +525,6 @@ abstract class ImageProvider { // of type `_AbstractImageStreamCompleter`. if (result is _AbstractImageStreamCompleter) { result = loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer); - if (result is _AbstractImageStreamCompleter) { - // Same fallback as above but for the deprecated `load()` method. - result = load(key, PaintingBinding.instance.instantiateImageCodec); - } } return result; }, @@ -598,46 +585,25 @@ abstract class ImageProvider { return cache.evict(key); } - /// Converts an ImageProvider's settings plus an ImageConfiguration to a key + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. /// /// The type of the key is determined by the subclass. It is a value that - /// unambiguously identifies the image (_including its scale_) that the [load] - /// method will fetch. Different [ImageProvider]s given the same constructor - /// arguments and [ImageConfiguration] objects should return keys that are - /// '==' to each other (possibly by using a class for the key that itself - /// implements [==]). + /// unambiguously identifies the image (_including its scale_) that the + /// [loadImage] method will fetch. Different [ImageProvider]s given the same + /// constructor arguments and [ImageConfiguration] objects should return keys + /// that are '==' to each other (possibly by using a class for the key that + /// itself implements [==]). + /// + /// If the result can be determined synchronously, this function should return + /// a [SynchronousFuture]. This allows image resolution to progress + /// synchronously during a frame rather than delaying image loading. Future obtainKey(ImageConfiguration configuration); /// Converts a key into an [ImageStreamCompleter], and begins fetching the /// image. /// - /// This method is deprecated. Implement [loadBuffer] for faster image - /// loading. Only one of [load] and [loadBuffer] must be implemented, and - /// [loadBuffer] is preferred. - /// - /// The [decode] callback provides the logic to obtain the codec for the - /// image. - /// - /// See also: - /// - /// * [ResizeImage], for modifying the key to account for cache dimensions. - @protected - @Deprecated( - 'Implement loadImage for faster image loading. ' - 'This feature was deprecated after v2.13.0-1.0.pre.', - ) - ImageStreamCompleter load(T key, DecoderCallback decode) { - throw UnsupportedError('Implement loadBuffer for faster image loading'); - } - - /// Converts a key into an [ImageStreamCompleter], and begins fetching the - /// image. - /// - /// For backwards-compatibility the default implementation of this method returns - /// an object that will cause [resolveStreamForKey] to consult [load]. However, - /// implementors of this interface should only override this method and not - /// [load], which is deprecated. + /// This method is deprecated. Implement [loadImage] instead. /// /// The [decode] callback provides the logic to obtain the codec for the /// image. @@ -688,8 +654,6 @@ class _AbstractImageStreamCompleter extends ImageStreamCompleter {} @immutable class AssetBundleImageKey { /// Creates the key for an [AssetImage] or [AssetBundleImageProvider]. - /// - /// The arguments must not be null. const AssetBundleImageKey({ required this.bundle, required this.name, @@ -767,25 +731,7 @@ abstract class AssetBundleImageProvider extends ImageProvider [ - DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Image key', key), - ]; - return true; - }()); - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decodeDeprecated: decode), + codec: _loadAsync(key, decode: decode), scale: key.scale, debugLabel: key.name, informationCollector: collector, @@ -795,48 +741,22 @@ abstract class AssetBundleImageProvider extends ImageProvider _loadAsync( AssetBundleImageKey key, { - ImageDecoderCallback? decode, - DecoderBufferCallback? decodeBufferDeprecated, - DecoderCallback? decodeDeprecated, + required _SimpleDecoderCallback decode, }) async { - if (decode != null) { - ui.ImmutableBuffer buffer; - // Hot reload/restart could change whether an asset bundle or key in a - // bundle are available, or if it is a network backed bundle. - try { - buffer = await key.bundle.loadBuffer(key.name); - } on FlutterError { - PaintingBinding.instance.imageCache.evict(key); - rethrow; - } - return decode(buffer); - } - if (decodeBufferDeprecated != null) { - ui.ImmutableBuffer buffer; - // Hot reload/restart could change whether an asset bundle or key in a - // bundle are available, or if it is a network backed bundle. - try { - buffer = await key.bundle.loadBuffer(key.name); - } on FlutterError { - PaintingBinding.instance.imageCache.evict(key); - rethrow; - } - return decodeBufferDeprecated(buffer); - } - ByteData data; + final ui.ImmutableBuffer buffer; // Hot reload/restart could change whether an asset bundle or key in a // bundle are available, or if it is a network backed bundle. try { - data = await key.bundle.load(key.name); + buffer = await key.bundle.loadBuffer(key.name); } on FlutterError { PaintingBinding.instance.imageCache.evict(key); rethrow; } - return decodeDeprecated!(data.buffer.asUint8List()); + return decode(buffer); } } @@ -1328,27 +1248,6 @@ class ResizeImage extends ImageProvider { return provider; } - @override - @Deprecated( - 'Implement loadImage for faster image loading. ' - 'This feature was deprecated after v2.13.0-1.0.pre.', - ) - ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) { - Future decodeResize(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) { - assert( - cacheWidth == null && cacheHeight == null && allowUpscaling == null, - 'ResizeImage cannot be composed with another ImageProvider that applies ' - 'cacheWidth, cacheHeight, or allowUpscaling.', - ); - return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling); - } - final ImageStreamCompleter completer = imageProvider.load(key._providerCacheKey, decodeResize); - if (!kReleaseMode) { - completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})'; - } - return completer; - } - @override @Deprecated( 'Implement loadImage for image loading. ' @@ -1368,6 +1267,7 @@ class ResizeImage extends ImageProvider { if (!kReleaseMode) { completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})'; } + _configureErrorListener(completer, key); return completer; } @@ -1437,9 +1337,22 @@ class ResizeImage extends ImageProvider { if (!kReleaseMode) { completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})'; } + _configureErrorListener(completer, key); return completer; } + void _configureErrorListener(ImageStreamCompleter completer, ResizeImageKey key) { + completer.addEphemeralErrorListener((Object exception, StackTrace? stackTrace) { + // The microtask is scheduled because of the same reason as NetworkImage: + // Depending on where the exception was thrown, the image cache may not + // have had a chance to track the key in the cache at all. + // Schedule a microtask to give the cache a chance to add the key. + scheduleMicrotask(() { + PaintingBinding.instance.imageCache.evict(key); + }); + }); + } + @override Future obtainKey(ImageConfiguration configuration) { Completer? completer; @@ -1470,22 +1383,22 @@ class ResizeImage extends ImageProvider { /// /// The image will be cached regardless of cache headers from the server. /// -/// When a network image is used on the Web platform, the `cacheWidth` and -/// `cacheHeight` parameters of the [DecoderCallback] are only supported when the -/// application is running with the CanvasKit renderer. When the application is using -/// the HTML renderer, the web engine delegates image decoding of network images to the Web, -/// which does not support custom decode sizes. +/// When a network image is used on the Web platform, the `getTargetSize` +/// parameter of the [ImageDecoderCallback] is only supported when the +/// application is running with the CanvasKit renderer. When the application is +/// using the HTML renderer, the web engine delegates image decoding of network +/// images to the Web, which does not support custom decode sizes. /// /// See also: /// /// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage]. +/// * The example at [ImageProvider], which shows a custom variant of this class +/// that applies different logic for fetching the image. // TODO(ianh): Find some way to honor cache headers to the extent that when the // last reference to an image is released, we proactively evict the image from // our cache if the headers describe the image as having expired at that point. abstract class NetworkImage extends ImageProvider { /// Creates an object that fetches the image at the given URL. - /// - /// The arguments [url] and [scale] must not be null. const factory NetworkImage(String url, { double scale, Map? headers }) = network_image.NetworkImage; /// The URL from which the image will be fetched. @@ -1496,12 +1409,9 @@ abstract class NetworkImage extends ImageProvider { /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. /// - /// When running flutter on the web, headers are not used. + /// When running Flutter on the web, headers are not used. Map? get headers; - @override - ImageStreamCompleter load(NetworkImage key, DecoderCallback decode); - @override ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode); @@ -1521,8 +1431,6 @@ abstract class NetworkImage extends ImageProvider { @immutable class FileImage extends ImageProvider { /// Creates an object that decodes a [File] as an image. - /// - /// The arguments must not be null. const FileImage(this.file, { this.scale = 1.0 }); /// The file to decode into an image. @@ -1536,22 +1444,10 @@ class FileImage extends ImageProvider { return SynchronousFuture(this); } - @override - ImageStreamCompleter load(FileImage key, DecoderCallback decode) { - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decodeDeprecated: decode), - scale: key.scale, - debugLabel: key.file.path, - informationCollector: () => [ - ErrorDescription('Path: ${file.path}'), - ], - ); - } - @override ImageStreamCompleter loadBuffer(FileImage key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decodeBufferDeprecated: decode), + codec: _loadAsync(key, decode: decode), scale: key.scale, debugLabel: key.file.path, informationCollector: () => [ @@ -1575,12 +1471,9 @@ class FileImage extends ImageProvider { Future _loadAsync( FileImage key, { - ImageDecoderCallback? decode, - DecoderBufferCallback? decodeBufferDeprecated, - DecoderCallback? decodeDeprecated, + required _SimpleDecoderCallback decode, }) async { assert(key == this); - // TODO(jonahwilliams): making this sync caused test failures that seem to // indicate that we can fail to call evict unless at least one await has // occurred in the test. @@ -1591,19 +1484,9 @@ class FileImage extends ImageProvider { PaintingBinding.instance.imageCache.evict(key); throw StateError('$file is empty and cannot be loaded as an image.'); } - if (decode != null) { - if (file.runtimeType == File) { - return decode(await ui.ImmutableBuffer.fromFilePath(file.path)); - } - return decode(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes())); - } - if (decodeBufferDeprecated != null) { - if (file.runtimeType == File) { - return decodeBufferDeprecated(await ui.ImmutableBuffer.fromFilePath(file.path)); - } - return decodeBufferDeprecated(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes())); - } - return decodeDeprecated!(await file.readAsBytes()); + return (file.runtimeType == File) + ? decode(await ui.ImmutableBuffer.fromFilePath(file.path)) + : decode(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes())); } @override @@ -1620,7 +1503,7 @@ class FileImage extends ImageProvider { int get hashCode => Object.hash(file.path, scale); @override - String toString() => '${objectRuntimeType(this, 'FileImage')}("${file.path}", scale: $scale)'; + String toString() => '${objectRuntimeType(this, 'FileImage')}("${file.path}", scale: ${scale.toStringAsFixed(1)})'; } /// Decodes the given [Uint8List] buffer as an image, associating it with the @@ -1629,8 +1512,8 @@ class FileImage extends ImageProvider { /// The provided [bytes] buffer should not be changed after it is provided /// to a [MemoryImage]. To provide an [ImageStream] that represents an image /// that changes over time, consider creating a new subclass of [ImageProvider] -/// whose [load] method returns a subclass of [ImageStreamCompleter] that can -/// handle providing multiple images. +/// whose [loadImage] method returns a subclass of [ImageStreamCompleter] that +/// can handle providing multiple images. /// /// See also: /// @@ -1638,8 +1521,6 @@ class FileImage extends ImageProvider { @immutable class MemoryImage extends ImageProvider { /// Creates an object that decodes a [Uint8List] buffer as an image. - /// - /// The arguments must not be null. const MemoryImage(this.bytes, { this.scale = 1.0 }); /// The bytes to decode into an image. @@ -1649,7 +1530,7 @@ class MemoryImage extends ImageProvider { /// /// See also: /// - /// * [PaintingBinding.instantiateImageCodec] + /// * [PaintingBinding.instantiateImageCodecWithSize] final Uint8List bytes; /// The scale to place in the [ImageInfo] object of the image. @@ -1665,19 +1546,11 @@ class MemoryImage extends ImageProvider { return SynchronousFuture(this); } - @override - ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decodeDeprecated: decode), - scale: key.scale, - debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})', - ); - } - @override ImageStreamCompleter loadBuffer(MemoryImage key, DecoderBufferCallback decode) { + assert(key == this); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decodeBufferDeprecated: decode), + codec: _loadAsync(key, decode: decode), scale: key.scale, debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})', ); @@ -1694,20 +1567,10 @@ class MemoryImage extends ImageProvider { Future _loadAsync( MemoryImage key, { - ImageDecoderCallback? decode, - DecoderBufferCallback? decodeBufferDeprecated, - DecoderCallback? decodeDeprecated, + required _SimpleDecoderCallback decode, }) async { assert(key == this); - if (decode != null) { - final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); - return decode(buffer); - } - if (decodeBufferDeprecated != null) { - final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); - return decodeBufferDeprecated(buffer); - } - return decodeDeprecated!(bytes); + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); } @override @@ -1724,7 +1587,7 @@ class MemoryImage extends ImageProvider { int get hashCode => Object.hash(bytes.hashCode, scale); @override - String toString() => '${objectRuntimeType(this, 'MemoryImage')}(${describeIdentity(bytes)}, scale: $scale)'; + String toString() => '${objectRuntimeType(this, 'MemoryImage')}(${describeIdentity(bytes)}, scale: ${scale.toStringAsFixed(1)})'; } /// Fetches an image from an [AssetBundle], associating it with the given scale. @@ -1802,10 +1665,9 @@ class MemoryImage extends ImageProvider { class ExactAssetImage extends AssetBundleImageProvider { /// Creates an object that fetches the given image from an asset bundle. /// - /// The [assetName] and [scale] arguments must not be null. The [scale] arguments - /// defaults to 1.0. The [bundle] argument may be null, in which case the - /// bundle provided in the [ImageConfiguration] passed to the [resolve] call - /// will be used instead. + /// The [scale] argument defaults to 1. The [bundle] argument may be null, in + /// which case the bundle provided in the [ImageConfiguration] passed to the + /// [resolve] call will be used instead. /// /// The [package] argument must be non-null when fetching an asset that is /// included in a package. See the documentation for the [ExactAssetImage] class @@ -1865,7 +1727,7 @@ class ExactAssetImage extends AssetBundleImageProvider { int get hashCode => Object.hash(keyName, scale, bundle); @override - String toString() => '${objectRuntimeType(this, 'ExactAssetImage')}(name: "$keyName", scale: $scale, bundle: $bundle)'; + String toString() => '${objectRuntimeType(this, 'ExactAssetImage')}(name: "$keyName", scale: ${scale.toStringAsFixed(1)}, bundle: $bundle)'; } // A completer used when resolving an image fails sync. diff --git a/packages/flutter/lib/src/painting/image_resolution.dart b/packages/flutter/lib/src/painting/image_resolution.dart index 2644a05f72583..f3fe4ef975fa8 100644 --- a/packages/flutter/lib/src/painting/image_resolution.dart +++ b/packages/flutter/lib/src/painting/image_resolution.dart @@ -233,10 +233,10 @@ const double _kLowDprLimit = 2.0; class AssetImage extends AssetBundleImageProvider { /// Creates an object that fetches an image from an asset bundle. /// - /// The [assetName] argument must not be null. It should name the main asset - /// from the set of images to choose from. The [package] argument must be - /// non-null when fetching an asset that is included in package. See the - /// documentation for the [AssetImage] class itself for details. + /// The [assetName] argument should name the main asset from the set of images + /// to choose from. The [package] argument must be non-null when fetching an + /// asset that is included in package. See the documentation for the + /// [AssetImage] class itself for details. const AssetImage( this.assetName, { this.bundle, diff --git a/packages/flutter/lib/src/painting/image_stream.dart b/packages/flutter/lib/src/painting/image_stream.dart index 65baf777ebf1c..2f5537a353701 100644 --- a/packages/flutter/lib/src/painting/image_stream.dart +++ b/packages/flutter/lib/src/painting/image_stream.dart @@ -20,8 +20,6 @@ import 'package:flutter/scheduler.dart'; class ImageInfo { /// Creates an [ImageInfo] object for the given [image] and [scale]. /// - /// Both the [image] and the [scale] must not be null. - /// /// The [debugLabel] may be used to identify the source of this image. const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel }); @@ -159,8 +157,6 @@ class ImageInfo { @immutable class ImageStreamListener { /// Creates a new [ImageStreamListener]. - /// - /// The [onImage] parameter must not be null. const ImageStreamListener( this.onImage, { this.onChunk, @@ -468,6 +464,7 @@ class ImageStreamCompleterHandle { /// configure it with the right [ImageStreamCompleter] when possible. abstract class ImageStreamCompleter with Diagnosticable { final List _listeners = []; + final List _ephemeralErrorListeners = []; ImageInfo? _currentImage; FlutterErrorDetails? _currentError; @@ -489,6 +486,9 @@ abstract class ImageStreamCompleter with Diagnosticable { /// and similarly, by overriding [removeListener], checking if [hasListeners] /// is false after calling `super.removeListener()`, and if so, stopping that /// same work. + /// + /// The ephemeral error listeners (added through [addEphemeralErrorListener]) + /// will not be taken into consideration in this property. @protected @visibleForTesting bool get hasListeners => _listeners.isNotEmpty; @@ -515,6 +515,11 @@ abstract class ImageStreamCompleter with Diagnosticable { /// this listener's [ImageStreamListener.onImage] will fire multiple times. /// /// {@macro flutter.painting.imageStream.addListener} + /// + /// See also: + /// + /// * [addEphemeralErrorListener], which adds an error listener that is + /// automatically removed after first image load or error. void addListener(ImageStreamListener listener) { _checkDisposed(); _hadAtLeastOneListener = true; @@ -548,6 +553,58 @@ abstract class ImageStreamCompleter with Diagnosticable { } } + /// Adds an error listener callback that is called when the first error is reported. + /// + /// The callback will be removed automatically after the first successful + /// image load or the first error - that is why it is called "ephemeral". + /// + /// If a concrete image is already available, the listener will be discarded + /// synchronously. If an error has been already reported, the listener + /// will be notified synchronously. + /// + /// The presence of a listener will affect neither the lifecycle of this object + /// nor what [hasListeners] reports. + /// + /// It is different from [addListener] in a few points: Firstly, this one only + /// listens to errors, while [addListener] listens to all kinds of events. + /// Secondly, this listener will be automatically removed according to the + /// rules mentioned above, while [addListener] will need manual removal. + /// Thirdly, this listener will not affect how this object is disposed, while + /// any non-removed listener added via [addListener] will forbid this object + /// from disposal. + /// + /// When you want to know full information and full control, use [addListener]. + /// When you only want to get notified for an error ephemerally, use this function. + /// + /// See also: + /// + /// * [addListener], which adds a full-featured listener and needs manual + /// removal. + void addEphemeralErrorListener(ImageErrorListener listener) { + _checkDisposed(); + if (_currentError != null) { + // immediately fire the listener, and no need to add to _ephemeralErrorListeners + try { + listener(_currentError!.exception, _currentError!.stack); + } catch (newException, newStack) { + if (newException != _currentError!.exception) { + FlutterError.reportError( + FlutterErrorDetails( + exception: newException, + library: 'image resource service', + context: ErrorDescription('by a synchronously-called image error listener'), + stack: newStack, + ), + ); + } + } + } else if (_currentImage == null) { + // add to _ephemeralErrorListeners to wait for the error, + // only if no image has been loaded + _ephemeralErrorListeners.add(listener); + } + } + int _keepAliveHandles = 0; /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from /// being disposed at least until the handle is disposed. @@ -595,6 +652,7 @@ abstract class ImageStreamCompleter with Diagnosticable { return; } + _ephemeralErrorListeners.clear(); _currentImage?.dispose(); _currentImage = null; _disposed = true; @@ -640,6 +698,8 @@ abstract class ImageStreamCompleter with Diagnosticable { _currentImage?.dispose(); _currentImage = image; + _ephemeralErrorListeners.clear(); + if (_listeners.isEmpty) { return; } @@ -707,10 +767,14 @@ abstract class ImageStreamCompleter with Diagnosticable { ); // Make a copy to allow for concurrent modification. - final List localErrorListeners = _listeners - .map((ImageStreamListener listener) => listener.onError) - .whereType() - .toList(); + final List localErrorListeners = [ + ..._listeners + .map((ImageStreamListener listener) => listener.onError) + .whereType(), + ..._ephemeralErrorListeners, + ]; + + _ephemeralErrorListeners.clear(); bool handled = false; for (final ImageErrorListener errorListener in localErrorListeners) { @@ -764,6 +828,11 @@ abstract class ImageStreamCompleter with Diagnosticable { _listeners, ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }', )); + description.add(ObjectFlagProperty>( + 'ephemeralErrorListeners', + _ephemeralErrorListeners, + ifPresent: '${_ephemeralErrorListeners.length} ephemeralErrorListener${_ephemeralErrorListeners.length == 1 ? "" : "s" }', + )); description.add(FlagProperty('disposed', value: _disposed, ifTrue: '')); } } diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart index 9037b99c09a04..97bb83f2c6b2c 100644 --- a/packages/flutter/lib/src/painting/inline_span.dart +++ b/packages/flutter/lib/src/painting/inline_span.dart @@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; import 'basic_types.dart'; import 'text_painter.dart'; +import 'text_scaler.dart'; import 'text_span.dart'; import 'text_style.dart'; @@ -50,8 +51,6 @@ class InlineSpanSemanticsInformation { /// Constructs an object that holds the text and semantics label values of an /// [InlineSpan]. /// - /// The text parameter must not be null. - /// /// Use [InlineSpanSemanticsInformation.placeholder] instead of directly setting /// [isPlaceholder]. const InlineSpanSemanticsInformation( @@ -209,7 +208,7 @@ abstract class InlineSpan extends DiagnosticableTree { /// Apply the properties of this object to the given [ParagraphBuilder], from /// which a [Paragraph] can be obtained. /// - /// The `textScaleFactor` parameter specifies a scale that the text and + /// The `textScaler` parameter specifies a [TextScaler] that the text and /// placeholders will be scaled by. The scaling is performed before layout, /// so the text will be laid out with the scaled glyphs and placeholders. /// @@ -218,7 +217,10 @@ abstract class InlineSpan extends DiagnosticableTree { /// in the same order as defined in the [InlineSpan] tree. /// /// [Paragraph] objects can be drawn on [Canvas] objects. - void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List? dimensions }); + void build(ui.ParagraphBuilder builder, { + TextScaler textScaler = TextScaler.noScaling, + List? dimensions, + }); /// Walks this [InlineSpan] and any descendants in pre-order and calls `visitor` /// for each span that has content. diff --git a/packages/flutter/lib/src/painting/linear_border.dart b/packages/flutter/lib/src/painting/linear_border.dart index 76dc6709a59e5..eaa51eec1145e 100644 --- a/packages/flutter/lib/src/painting/linear_border.dart +++ b/packages/flutter/lib/src/painting/linear_border.dart @@ -132,6 +132,12 @@ class LinearBorderEdge { /// /// Convenience constructors are included for the common case where just one edge is specified: /// [LinearBorder.start], [LinearBorder.end], [LinearBorder.top], [LinearBorder.bottom]. +/// +/// {@tool dartpad} +/// This example shows how to draw different kinds of [LinearBorder]s. +/// +/// ** See code in examples/api/lib/painting/linear_border/linear_border.0.dart ** +/// {@end-tool} class LinearBorder extends OutlinedBorder { /// Creates a rectangular box border that's rendered as zero to four lines. const LinearBorder({ diff --git a/packages/flutter/lib/src/painting/matrix_utils.dart b/packages/flutter/lib/src/painting/matrix_utils.dart index 7bc30f076b12d..b2d0018de6207 100644 --- a/packages/flutter/lib/src/painting/matrix_utils.dart +++ b/packages/flutter/lib/src/painting/matrix_utils.dart @@ -541,8 +541,6 @@ List debugDescribeTransform(Matrix4? transform) { /// Property which handles [Matrix4] that represent transforms. class TransformProperty extends DiagnosticsProperty { /// Create a diagnostics property for [Matrix4] objects. - /// - /// The [showName] and [level] arguments must not be null. TransformProperty( String super.name, super.value, { diff --git a/packages/flutter/lib/src/painting/notched_shapes.dart b/packages/flutter/lib/src/painting/notched_shapes.dart index 8f7a83042d494..2e19b315c6658 100644 --- a/packages/flutter/lib/src/painting/notched_shapes.dart +++ b/packages/flutter/lib/src/painting/notched_shapes.dart @@ -130,8 +130,6 @@ class CircularNotchedRectangle extends NotchedShape { class AutomaticNotchedShape extends NotchedShape { /// Creates a [NotchedShape] that is defined by two [ShapeBorder]s. /// - /// The [host] must not be null. - /// /// The [guest] may be null, in which case no notch is created even /// if a guest rectangle is provided to [getOuterPath]. const AutomaticNotchedShape(this.host, [ this.guest ]); diff --git a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart index c4320ebdf841d..27109396c2a0c 100644 --- a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart @@ -25,8 +25,6 @@ import 'circle_border.dart'; /// describe a rounded rectangle. class RoundedRectangleBorder extends OutlinedBorder { /// Creates a rounded rectangle border. - /// - /// The arguments must not be null. const RoundedRectangleBorder({ super.side, this.borderRadius = BorderRadius.zero, diff --git a/packages/flutter/lib/src/painting/shader_warm_up.dart b/packages/flutter/lib/src/painting/shader_warm_up.dart index 29bc0ecbc1cc9..48a5bf0edfdfc 100644 --- a/packages/flutter/lib/src/painting/shader_warm_up.dart +++ b/packages/flutter/lib/src/painting/shader_warm_up.dart @@ -88,14 +88,18 @@ abstract class ShaderWarmUp { final ui.Picture picture = recorder.endRecording(); assert(debugCaptureShaderWarmUpPicture(picture)); if (!kIsWeb || isCanvasKit) { // Picture.toImage is not yet implemented on the web. - final TimelineTask shaderWarmUpTask = TimelineTask(); - shaderWarmUpTask.start('Warm-up shader'); + TimelineTask? debugShaderWarmUpTask; + if (!kReleaseMode) { + debugShaderWarmUpTask = TimelineTask()..start('Warm-up shader'); + } try { final ui.Image image = await picture.toImage(size.width.ceil(), size.height.ceil()); assert(debugCaptureShaderWarmUpImage(image)); image.dispose(); } finally { - shaderWarmUpTask.finish(); + if (!kReleaseMode) { + debugShaderWarmUpTask!.finish(); + } } } picture.dispose(); diff --git a/packages/flutter/lib/src/painting/shape_decoration.dart b/packages/flutter/lib/src/painting/shape_decoration.dart index 20785bced2150..fbe3c60ad5c3d 100644 --- a/packages/flutter/lib/src/painting/shape_decoration.dart +++ b/packages/flutter/lib/src/painting/shape_decoration.dart @@ -68,8 +68,6 @@ class ShapeDecoration extends Decoration { /// /// The [color] and [gradient] properties are mutually exclusive, one (or /// both) of them must be null. - /// - /// The [shape] must not be null. const ShapeDecoration({ this.color, this.image, @@ -157,8 +155,7 @@ class ShapeDecoration extends Decoration { /// Shapes can be stacked (using the `+` operator). The color, gradient, and /// image are drawn into the inner-most shape specified. /// - /// The [shape] property specifies the outline (border) of the decoration. The - /// shape must not be null. + /// The [shape] property specifies the outline (border) of the decoration. /// /// ## Directionality-dependent shapes /// @@ -237,7 +234,7 @@ class ShapeDecoration extends Decoration { return ShapeDecoration( color: Color.lerp(a?.color, b?.color, t), gradient: Gradient.lerp(a?.gradient, b?.gradient, t), - image: t < 0.5 ? a?.image : b?.image, // TODO(ianh): cross-fade the image + image: DecorationImage.lerp(a?.image, b?.image, t), shadows: BoxShadow.lerpList(a?.shadows, b?.shadows, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t)!, ); diff --git a/packages/flutter/lib/src/painting/stadium_border.dart b/packages/flutter/lib/src/painting/stadium_border.dart index 1576e93acaed2..e9e57dfcda24e 100644 --- a/packages/flutter/lib/src/painting/stadium_border.dart +++ b/packages/flutter/lib/src/painting/stadium_border.dart @@ -25,8 +25,6 @@ import 'rounded_rectangle_border.dart'; /// * [BorderSide], which is used to describe the border of the stadium. class StadiumBorder extends OutlinedBorder { /// Create a stadium border. - /// - /// The [side] argument must not be null. const StadiumBorder({ super.side }); @override diff --git a/packages/flutter/lib/src/painting/strut_style.dart b/packages/flutter/lib/src/painting/strut_style.dart index 4899affb03f07..432a835950161 100644 --- a/packages/flutter/lib/src/painting/strut_style.dart +++ b/packages/flutter/lib/src/painting/strut_style.dart @@ -318,8 +318,6 @@ class StrutStyle with Diagnosticable { /// Builds a StrutStyle that contains values of the equivalent properties in /// the provided [textStyle]. /// - /// The [textStyle] parameter must not be null. - /// /// The named parameters override the [textStyle]'s argument's properties. /// Since TextStyle does not contain [leading] or [forceStrutHeight], these /// values will take on default values (null and false) unless otherwise @@ -405,7 +403,7 @@ class StrutStyle with Diagnosticable { /// constructor. List? get fontFamilyFallback { if (_package != null && _fontFamilyFallback != null) { - return _fontFamilyFallback!.map((String family) => 'packages/$_package/$family').toList(); + return _fontFamilyFallback.map((String family) => 'packages/$_package/$family').toList(); } return _fontFamilyFallback; } diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 08ed58c612960..955e716ed0f5b 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -22,6 +22,7 @@ import 'basic_types.dart'; import 'inline_span.dart'; import 'placeholder_span.dart'; import 'strut_style.dart'; +import 'text_scaler.dart'; import 'text_span.dart'; export 'package:flutter/services.dart' show TextRange, TextSelection; @@ -55,8 +56,6 @@ enum TextOverflow { /// Placeholders specify an empty space in the text layout, which is used /// to later render arbitrary inline widgets into defined by a [WidgetSpan]. /// -/// The [size] and [alignment] properties are required and cannot be null. -/// /// See also: /// /// * [WidgetSpan], a subclass of [InlineSpan] and [PlaceholderSpan] that @@ -278,14 +277,6 @@ class _TextLayout { // object when it's no logner needed. ui.Paragraph _paragraph; - /// Whether to enable the rounding in _applyFloatingPointHack and SkParagraph. - static const bool _shouldApplyFloatingPointHack = !bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK'); - - // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/31707 - // remove this hack as well as the flooring in `layout`. - @pragma('vm:prefer-inline') - static double _applyFloatingPointHack(double layoutValue) => _shouldApplyFloatingPointHack ? layoutValue.ceilToDouble() : layoutValue; - /// Whether this layout has been invalidated and disposed. /// /// Only for use when asserts are enabled. @@ -295,23 +286,23 @@ class _TextLayout { /// /// If a line ends with trailing spaces, the trailing spaces may extend /// outside of the horizontal paint bounds defined by [width]. - double get width => _applyFloatingPointHack(_paragraph.width); + double get width => _paragraph.width; /// The vertical space required to paint this text. - double get height => _applyFloatingPointHack(_paragraph.height); + double get height => _paragraph.height; /// The width at which decreasing the width of the text would prevent it from /// painting itself completely within its bounds. - double get minIntrinsicLineExtent => _applyFloatingPointHack(_paragraph.minIntrinsicWidth); + double get minIntrinsicLineExtent => _paragraph.minIntrinsicWidth; /// The width at which increasing the width of the text no longer decreases the height. /// /// Includes trailing spaces if any. - double get maxIntrinsicLineExtent => _applyFloatingPointHack(_paragraph.maxIntrinsicWidth); + double get maxIntrinsicLineExtent => _paragraph.maxIntrinsicWidth; /// The distance from the left edge of the leftmost glyph to the right edge of /// the rightmost glyph in the paragraph. - double get longestLine => _applyFloatingPointHack(_paragraph.longestLine); + double get longestLine => _paragraph.longestLine; /// Returns the distance from the top of the text to the first baseline of the /// given type. @@ -360,12 +351,6 @@ class _TextPainterLayoutCacheWithOffset { ui.Paragraph get paragraph => layout._paragraph; static double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis, _TextLayout layout) { - // TODO(LongCatIsLooong): remove the rounding when _applyFloatingPointHack - // is removed. - if (_TextLayout._shouldApplyFloatingPointHack) { - minWidth = minWidth.floorToDouble(); - maxWidth = maxWidth.floorToDouble(); - } return switch (widthBasis) { TextWidthBasis.longestLine => clampDouble(layout.longestLine, minWidth, maxWidth), TextWidthBasis.parent => clampDouble(layout.maxIntrinsicLineExtent, minWidth, maxWidth), @@ -483,14 +468,18 @@ class TextPainter { /// The `text` and `textDirection` arguments are optional but [text] and /// [textDirection] must be non-null before calling [layout]. /// - /// The [textAlign] property must not be null. - /// /// The [maxLines] property, if non-null, must be greater than zero. TextPainter({ InlineSpan? text, TextAlign textAlign = TextAlign.start, TextDirection? textDirection, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, int? maxLines, String? ellipsis, Locale? locale, @@ -499,10 +488,11 @@ class TextPainter { ui.TextHeightBehavior? textHeightBehavior, }) : assert(text == null || text.debugAssertIsValid()), assert(maxLines == null || maxLines > 0), + assert(textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), 'Use textScaler instead.'), _text = text, _textAlign = textAlign, _textDirection = textDirection, - _textScaleFactor = textScaleFactor, + _textScaler = textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, _maxLines = maxLines, _ellipsis = ellipsis, _locale = locale, @@ -522,7 +512,13 @@ class TextPainter { required InlineSpan text, required TextDirection textDirection, TextAlign textAlign = TextAlign.start, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, int? maxLines, String? ellipsis, Locale? locale, @@ -532,11 +528,15 @@ class TextPainter { double minWidth = 0.0, double maxWidth = double.infinity, }) { + assert( + textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), + 'Use textScaler instead.', + ); final TextPainter painter = TextPainter( text: text, textAlign: textAlign, textDirection: textDirection, - textScaleFactor: textScaleFactor, + textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, maxLines: maxLines, ellipsis: ellipsis, locale: locale, @@ -564,7 +564,13 @@ class TextPainter { required InlineSpan text, required TextDirection textDirection, TextAlign textAlign = TextAlign.start, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, int? maxLines, String? ellipsis, Locale? locale, @@ -574,11 +580,15 @@ class TextPainter { double minWidth = 0.0, double maxWidth = double.infinity, }) { + assert( + textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), + 'Use textScaler instead.', + ); final TextPainter painter = TextPainter( text: text, textAlign: textAlign, textDirection: textDirection, - textScaleFactor: textScaleFactor, + textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, maxLines: maxLines, ellipsis: ellipsis, locale: locale, @@ -693,7 +703,7 @@ class TextPainter { /// /// After this is set, you must call [layout] before the next call to [paint]. /// - /// The [textAlign] property must not be null. It defaults to [TextAlign.start]. + /// The [textAlign] property defaults to [TextAlign.start]. TextAlign get textAlign => _textAlign; TextAlign _textAlign; set textAlign(TextAlign value) { @@ -731,19 +741,48 @@ class TextPainter { _layoutTemplate = null; // Shouldn't really matter, but for strict correctness... } + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. /// /// After this is set, you must call [layout] before the next call to [paint]. - double get textScaleFactor => _textScaleFactor; - double _textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double get textScaleFactor => textScaler.textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) set textScaleFactor(double value) { - if (_textScaleFactor == value) { + textScaler = TextScaler.linear(value); + } + + /// {@template flutter.painting.textPainter.textScaler} + /// The font scaling strategy to use when laying out and rendering the text. + /// + /// The value usually comes from [MediaQuery.textScalerOf], which typically + /// reflects the user-specified text scaling value in the platform's + /// accessibility settings. The [TextStyle.fontSize] of the text will be + /// adjusted by the [TextScaler] before the text is laid out and rendered. + /// {@endtemplate} + /// + /// The [layout] method must be called after [textScaler] changes as it + /// affects the text layout. + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + set textScaler(TextScaler value) { + if (value == _textScaler) { return; } - _textScaleFactor = value; + _textScaler = value; markNeedsLayout(); _layoutTemplate?.dispose(); _layoutTemplate = null; @@ -908,7 +947,7 @@ class TextPainter { return _text!.style?.getParagraphStyle( textAlign: textAlign, textDirection: textDirection ?? defaultTextDirection, - textScaleFactor: textScaleFactor, + textScaler: textScaler, maxLines: _maxLines, textHeightBehavior: _textHeightBehavior, ellipsis: _ellipsis, @@ -919,8 +958,8 @@ class TextPainter { textDirection: textDirection ?? defaultTextDirection, // Use the default font size to multiply by as RichText does not // perform inheriting [TextStyle]s and would otherwise - // fail to apply textScaleFactor. - fontSize: _kDefaultFontSize * textScaleFactor, + // fail to apply textScaler. + fontSize: textScaler.scale(_kDefaultFontSize), maxLines: maxLines, textHeightBehavior: _textHeightBehavior, ellipsis: ellipsis, @@ -933,7 +972,7 @@ class TextPainter { final ui.ParagraphBuilder builder = ui.ParagraphBuilder( _createParagraphStyle(TextDirection.rtl), ); // direction doesn't matter, text is just a space - final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaleFactor: textScaleFactor); + final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler); if (textStyle != null) { builder.pushStyle(textStyle); } @@ -1028,7 +1067,7 @@ class TextPainter { // assign it to _paragraph. ui.Paragraph _createParagraph(InlineSpan text) { final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle()); - text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions); + text.build(builder, textScaler: textScaler, dimensions: _placeholderDimensions); assert(() { _debugMarkNeedsLayoutCallStack = null; return true; diff --git a/packages/flutter/lib/src/painting/text_scaler.dart b/packages/flutter/lib/src/painting/text_scaler.dart new file mode 100644 index 0000000000000..554cfb892fa5a --- /dev/null +++ b/packages/flutter/lib/src/painting/text_scaler.dart @@ -0,0 +1,151 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' show max, min; + +import 'package:flutter/foundation.dart'; + +/// A class that describes how textual contents should be scaled for better +/// readability. +/// +/// The [scale] function computes the scaled font size given the original +/// unscaled font size specified by app developers. +/// +/// The [==] operator defines the equality of 2 [TextScaler]s, which the +/// framework uses to determine whether text widgets should rebuild when their +/// [TextScaler] changes. Consider overridding the [==] operator if applicable +/// to avoid unnecessary rebuilds. +@immutable +abstract class TextScaler { + /// Creates a TextScaler. + const TextScaler(); + + /// Creates a proportional [TextScaler] that scales the incoming font size by + /// multiplying it with the given `textScaleFactor`. + const factory TextScaler.linear(double textScaleFactor) = _LinearTextScaler; + + /// A [TextScaler] that doesn't scale the input font size. + /// + /// This is equivalent to `TextScaler.linear(1.0)`, the [TextScaler.scale] + /// implementation always returns the input font size as-is. + static const TextScaler noScaling = _LinearTextScaler(1.0); + + /// Computes the scaled font size (in logical pixels) with the given unscaled + /// `fontSize` (in logical pixels). + /// + /// The input `fontSize` must be finite and non-negative. + /// + /// When given the same `fontSize` input, this method returns the same value. + /// The output of a larger input `fontSize` is typically larger than that of a + /// smaller input, but on unusual occasions they may produce the same output. + /// For example, some platforms use single-precision floats to represent font + /// sizes, as a result of truncation two different unscaled font sizes can be + /// scaled to the same value. + double scale(double fontSize); + + /// The estimated number of font pixels for each logical pixel. This property + /// exists only for backward compatibility purposes, and will be removed in + /// a future version of Flutter. + /// + /// The value of this property is only an estimate, so it may not reflect the + /// exact text scaling strategy this [TextScaler] represents, especially when + /// this [TextScaler] is not linear. Consider using [TextScaler.scale] instead. + @Deprecated( + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double get textScaleFactor; + + /// Returns a new [TextScaler] that restricts the scaled font size to within + /// the range `[minScaleFactor * fontSize, maxScaleFactor * fontSize]`. + TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) { + assert(maxScaleFactor >= minScaleFactor); + assert(!maxScaleFactor.isNaN); + assert(minScaleFactor.isFinite); + assert(minScaleFactor >= 0); + + return minScaleFactor == maxScaleFactor + ? TextScaler.linear(minScaleFactor) + : _ClampedTextScaler(this, minScaleFactor, maxScaleFactor); + } +} + +final class _LinearTextScaler implements TextScaler { + const _LinearTextScaler(this.textScaleFactor) : assert(textScaleFactor >= 0); + + @override + final double textScaleFactor; + + @override + double scale(double fontSize) { + assert(fontSize >= 0); + assert(fontSize.isFinite); + return fontSize * textScaleFactor; + } + + @override + TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) { + assert(maxScaleFactor >= minScaleFactor); + assert(!maxScaleFactor.isNaN); + assert(minScaleFactor.isFinite); + assert(minScaleFactor >= 0); + + final double newScaleFactor = clampDouble(textScaleFactor, minScaleFactor, maxScaleFactor); + return newScaleFactor == textScaleFactor ? this : _LinearTextScaler(newScaleFactor); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _LinearTextScaler && other.textScaleFactor == textScaleFactor; + } + + @override + int get hashCode => textScaleFactor.hashCode; + + @override + String toString() => textScaleFactor == 1.0 ? 'no scaling' : 'linear (${textScaleFactor}x)'; +} + +final class _ClampedTextScaler implements TextScaler { + const _ClampedTextScaler(this.scaler, this.minScale, this.maxScale) : assert(maxScale > minScale); + final TextScaler scaler; + final double minScale; + final double maxScale; + + @override + double get textScaleFactor => clampDouble(scaler.textScaleFactor, minScale, maxScale); + + @override + double scale(double fontSize) { + assert(fontSize >= 0); + assert(fontSize.isFinite); + return minScale == maxScale + ? minScale * fontSize + : clampDouble(scaler.scale(fontSize), minScale * fontSize, maxScale * fontSize); + } + + @override + TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) { + return minScaleFactor == maxScaleFactor + ? _LinearTextScaler(minScaleFactor) + : _ClampedTextScaler(scaler, max(minScaleFactor, minScale), min(maxScaleFactor, maxScale)); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _ClampedTextScaler + && minScale == other.minScale + && maxScale == other.maxScale + && (minScale == maxScale || scaler == other.scaler); + } + + @override + int get hashCode => minScale == maxScale ? minScale.hashCode : Object.hash(scaler, minScale, maxScale); +} diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index 34168be205e91..c3f37310332bc 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'basic_types.dart'; import 'inline_span.dart'; import 'text_painter.dart'; +import 'text_scaler.dart'; // Examples can assume: // late TextSpan myTextSpan; @@ -267,13 +268,13 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati @override void build( ui.ParagraphBuilder builder, { - double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, List? dimensions, }) { assert(debugAssertIsValid()); final bool hasStyle = style != null; if (hasStyle) { - builder.pushStyle(style!.getTextStyle(textScaleFactor: textScaleFactor)); + builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); } if (text != null) { try { @@ -294,7 +295,7 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati for (final InlineSpan child in children) { child.build( builder, - textScaleFactor: textScaleFactor, + textScaler: textScaler, dimensions: dimensions, ); } diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index b00e0191d0184..3fd51313a7ba7 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -19,6 +19,7 @@ import 'basic_types.dart'; import 'colors.dart'; import 'strut_style.dart'; import 'text_painter.dart'; +import 'text_scaler.dart'; const String _kDefaultDebugLabel = 'unknown'; @@ -928,8 +929,6 @@ class TextStyle with Diagnosticable { /// applied to a `style` whose [fontWeight] is [FontWeight.w500] will return a /// [TextStyle] with a [FontWeight.w300]. /// - /// The numeric arguments must not be null. - /// /// If the underlying values are null, then the corresponding factors and/or /// deltas must not be specified. /// @@ -1271,7 +1270,24 @@ class TextStyle with Diagnosticable { } /// The style information for text runs, encoded for use by `dart:ui`. - ui.TextStyle getTextStyle({ double textScaleFactor = 1.0 }) { + ui.TextStyle getTextStyle({ + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, + }) { + assert( + identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ); + final double? fontSize = switch (this.fontSize) { + null => null, + final double size when textScaler == TextScaler.noScaling => size * textScaleFactor, + final double size => textScaler.scale(size), + }; return ui.TextStyle( color: color, decoration: decoration, @@ -1284,16 +1300,17 @@ class TextStyle with Diagnosticable { leadingDistribution: leadingDistribution, fontFamily: fontFamily, fontFamilyFallback: fontFamilyFallback, - fontSize: fontSize == null ? null : fontSize! * textScaleFactor, + fontSize: fontSize, letterSpacing: letterSpacing, wordSpacing: wordSpacing, height: height, locale: locale, foreground: foreground, - background: background ?? (backgroundColor != null - ? (Paint()..color = backgroundColor!) - : null - ), + background: switch ((background, backgroundColor)) { + (final Paint paint, _) => paint, + (_, final Color color) => Paint()..color = color, + _ => null, + }, shadows: shadows, fontFeatures: fontFeatures, fontVariations: fontVariations, @@ -1302,16 +1319,16 @@ class TextStyle with Diagnosticable { /// The style information for paragraphs, encoded for use by `dart:ui`. /// - /// The `textScaleFactor` argument must not be null. If omitted, it defaults - /// to 1.0. The other arguments may be null. The `maxLines` argument, if - /// specified and non-null, must be greater than zero. + /// If the `textScaleFactor` argument is omitted, it defaults to one. The + /// other arguments may be null. The `maxLines` argument, if specified and + /// non-null, must be greater than zero. /// /// If the font size on this style isn't set, it will default to 14 logical /// pixels. ui.ParagraphStyle getParagraphStyle({ TextAlign? textAlign, TextDirection? textDirection, - double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, String? ellipsis, int? maxLines, ui.TextHeightBehavior? textHeightBehavior, @@ -1327,6 +1344,7 @@ class TextStyle with Diagnosticable { final ui.TextLeadingDistribution? leadingDistribution = this.leadingDistribution; final ui.TextHeightBehavior? effectiveTextHeightBehavior = textHeightBehavior ?? (leadingDistribution == null ? null : ui.TextHeightBehavior(leadingDistribution: leadingDistribution)); + return ui.ParagraphStyle( textAlign: textAlign, textDirection: textDirection, @@ -1335,13 +1353,16 @@ class TextStyle with Diagnosticable { fontWeight: fontWeight ?? this.fontWeight, fontStyle: fontStyle ?? this.fontStyle, fontFamily: fontFamily ?? this.fontFamily, - fontSize: (fontSize ?? this.fontSize ?? _kDefaultFontSize) * textScaleFactor, + fontSize: textScaler.scale(fontSize ?? this.fontSize ?? _kDefaultFontSize), height: height ?? this.height, textHeightBehavior: effectiveTextHeightBehavior, strutStyle: strutStyle == null ? null : ui.StrutStyle( fontFamily: strutStyle.fontFamily, fontFamilyFallback: strutStyle.fontFamilyFallback, - fontSize: strutStyle.fontSize == null ? null : strutStyle.fontSize! * textScaleFactor, + fontSize: switch (strutStyle.fontSize) { + null => null, + final double unscaled => textScaler.scale(unscaled), + }, height: strutStyle.height, leading: strutStyle.leading, fontWeight: strutStyle.fontWeight, diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index 845d60896aff6..cbce56c38c319 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -99,8 +99,42 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { ); } + /// When asserts are enabled, returns the animation controller that is used + /// to drive the resizing. + /// + /// Otherwise, returns null. + /// + /// This getter is intended for use in framework unit tests. Applications must + /// not depend on its value. + @visibleForTesting + AnimationController? get debugController { + AnimationController? controller; + assert(() { + controller = _controller; + return true; + }()); + return controller; + } + + /// When asserts are enabled, returns the animation that drives the resizing. + /// + /// Otherwise, returns null. + /// + /// This getter is intended for use in framework unit tests. Applications must + /// not depend on its value. + @visibleForTesting + CurvedAnimation? get debugAnimation { + CurvedAnimation? animation; + assert(() { + animation = _animation; + return true; + }()); + return animation; + } + late final AnimationController _controller; late final CurvedAnimation _animation; + final SizeTween _sizeTween = SizeTween(); late bool _hasVisualOverflow; double? _lastValue; @@ -141,7 +175,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -351,6 +385,8 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { @override void dispose() { _clipRectLayer.layer = null; + _controller.dispose(); + _animation.dispose(); super.dispose(); } } diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index a90e062c42114..5d1552ec3cb54 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -10,7 +10,6 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; -import 'box.dart'; import 'debug.dart'; import 'mouse_tracker.dart'; import 'object.dart'; @@ -22,28 +21,34 @@ export 'package:flutter/gestures.dart' show HitTestResult; // Examples can assume: // late BuildContext context; -/// The glue between the render tree and the Flutter engine. +/// The glue between the render trees and the Flutter engine. +/// +/// The [RendererBinding] manages multiple independent render trees. Each render +/// tree is rooted in a [RenderView] that must be added to the binding via +/// [addRenderView] to be considered during frame production, hit testing, etc. +/// Furthermore, the render tree must be managed by a [PipelineOwner] that is +/// part of the pipeline owner tree rooted at [rootPipelineOwner]. +/// +/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way +/// described above is left as a responsibility for a higher level abstraction. +/// The widgets library, for example, introduces the [View] widget, which +/// registers its [RenderView] and [PipelineOwner] with this binding. mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable { @override void initInstances() { super.initInstances(); _instance = this; - _pipelineOwner = PipelineOwner( - onSemanticsOwnerCreated: _handleSemanticsOwnerCreated, - onSemanticsUpdate: _handleSemanticsUpdate, - onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed, - ); + _rootPipelineOwner = createRootPipelineOwner(); platformDispatcher ..onMetricsChanged = handleMetricsChanged ..onTextScaleFactorChanged = handleTextScaleFactorChanged ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged; - initRenderView(); addPersistentFrameCallback(_handlePersistentFrameCallback); initMouseTracker(); if (kIsWeb) { addPostFrameCallback(_handleWebFirstFrame); } - _pipelineOwner.attach(_manifold); + rootPipelineOwner.attach(_manifold); } /// The current [RendererBinding], if one has been created. @@ -108,9 +113,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture registerServiceExtension( name: RenderingServiceExtensions.debugDumpLayerTree.name, callback: (Map parameters) async { - final String data = RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable.'; return { - 'data': data, + 'data': _debugCollectLayerTrees(), }; }, ); @@ -155,9 +159,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture registerServiceExtension( name: RenderingServiceExtensions.debugDumpRenderTree.name, callback: (Map parameters) async { - final String data = RendererBinding.instance.renderView.toStringDeep(); return { - 'data': data, + 'data': _debugCollectRenderTrees(), }; }, ); @@ -165,7 +168,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, callback: (Map parameters) async { return { - 'data': _generateSemanticsTree(DebugSemanticsDumpOrder.traversalOrder), + 'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.traversalOrder), }; }, ); @@ -173,7 +176,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, callback: (Map parameters) async { return { - 'data': _generateSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest), + 'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.inverseHitTest), }; }, ); @@ -200,38 +203,156 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture late final PipelineManifold _manifold = _BindingPipelineManifold(this); - /// Creates a [RenderView] object to be the root of the - /// [RenderObject] rendering tree, and initializes it so that it - /// will be rendered when the next frame is requested. - /// - /// Called automatically when the binding is created. - void initRenderView() { - assert(!_debugIsRenderViewInitialized); - assert(() { - _debugIsRenderViewInitialized = true; - return true; - }()); - renderView = RenderView(configuration: createViewConfiguration(), view: platformDispatcher.implicitView!); - renderView.prepareInitialFrame(); - } - bool _debugIsRenderViewInitialized = false; - /// The object that manages state about currently connected mice, for hover /// notification. MouseTracker get mouseTracker => _mouseTracker!; MouseTracker? _mouseTracker; - /// The render tree's owner, which maintains dirty state for layout, - /// composite, paint, and accessibility semantics. - PipelineOwner get pipelineOwner => _pipelineOwner; - late PipelineOwner _pipelineOwner; + /// Deprecated. Will be removed in a future version of Flutter. + /// + /// This is typically the owner of the render tree bootstrapped by [runApp] + /// and rooted in [renderView]. It maintains dirty state for layout, + /// composite, paint, and accessibility semantics for that tree. + /// + /// However, by default, the [pipelineOwner] does not participate in frame + /// production because it is not automatically attached to the + /// [rootPipelineOwner] or any of its descendants. It is also not + /// automatically associated with the [renderView]. This is left as a + /// responsibility for a higher level abstraction. The [WidgetsBinding], for + /// example, wires this up in [WidgetsBinding.wrapWithDefaultView], which is + /// called indirectly from [runApp]. + /// + /// Apps, that don't use the [WidgetsBinding] or don't call [runApp] (or + /// [WidgetsBinding.wrapWithDefaultView]) must manually add this pipeline owner + /// to the pipeline owner tree rooted at [rootPipelineOwner] and assign a + /// [RenderView] to it if the they want to use this deprecated property. + /// + /// Instead of accessing this deprecated property, consider interacting with + /// the root of the [PipelineOwner] tree (exposed in [rootPipelineOwner]) or + /// instead of accessing the [SemanticsOwner] of any [PipelineOwner] consider + /// interacting with the [SemanticsBinding] (exposed via + /// [SemanticsBinding.instance]) directly. + @Deprecated( + 'Interact with the pipelineOwner tree rooted at RendererBinding.rootPipelineOwner instead. ' + 'Or instead of accessing the SemanticsOwner of any PipelineOwner interact with the SemanticsBinding directly. ' + 'This feature was deprecated after v3.10.0-12.0.pre.' + ) + late final PipelineOwner pipelineOwner = PipelineOwner( + onSemanticsOwnerCreated: () { + (pipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (ui.SemanticsUpdate update) { + (pipelineOwner.rootNode as RenderView?)?.updateSemantics(update); + }, + onSemanticsOwnerDisposed: () { + (pipelineOwner.rootNode as RenderView?)?.clearSemantics(); + } + ); + + /// Deprecated. Will be removed in a future version of Flutter. + /// + /// This is typically the root of the render tree bootstrapped by [runApp]. + /// + /// However, by default this render view is not associated with any + /// [PipelineOwner] and therefore isn't considered during frame production. + /// It is also not registered with this binding via [addRenderView]. + /// Wiring this up is left as a responsibility for a higher level. The + /// [WidgetsBinding], for example, sets this up in + /// [WidgetsBinding.wrapWithDefaultView], which is called indirectly from + /// [runApp]. + /// + /// Apps that don't use the [WidgetsBinding] or don't call [runApp] (or + /// [WidgetsBinding.wrapWithDefaultView]) must manually assign a + /// [PipelineOwner] to this [RenderView], make sure the pipeline owner is part + /// of the pipeline owner tree rooted at [rootPipelineOwner], and call + /// [addRenderView] if they want to use this deprecated property. + /// + /// Instead of interacting with this deprecated property, consider using + /// [renderViews] instead, which contains all [RenderView]s managed by the + /// binding. + @Deprecated( + 'Consider using RendererBinding.renderViews instead as the binding may manage multiple RenderViews. ' + 'This feature was deprecated after v3.10.0-12.0.pre.' + ) + // TODO(goderbauer): When this deprecated property is removed also delete the _ReusableRenderView class. + late final RenderView renderView = _ReusableRenderView( + view: platformDispatcher.implicitView!, + ); + + /// Creates the [PipelineOwner] that serves as the root of the pipeline owner + /// tree ([rootPipelineOwner]). + /// + /// {@template flutter.rendering.createRootPipelineOwner} + /// By default, the root pipeline owner is not setup to manage a render tree + /// and its [PipelineOwner.rootNode] must not be assigned. If necessary, + /// [createRootPipelineOwner] may be overridden to create a root pipeline + /// owner configured to manage its own render tree. + /// + /// In typical use, child pipeline owners are added to the root pipeline owner + /// (via [PipelineOwner.adoptChild]). Those children typically do each manage + /// their own [RenderView] and produce distinct render trees which render + /// their content into the [FlutterView] associated with that [RenderView]. + /// {@endtemplate} + PipelineOwner createRootPipelineOwner() { + return _DefaultRootPipelineOwner(); + } - /// The render tree that's attached to the output surface. - RenderView get renderView => _pipelineOwner.rootNode! as RenderView; - /// Sets the given [RenderView] object (which must not be null), and its tree, to - /// be the new render tree to display. The previous tree, if any, is detached. - set renderView(RenderView value) { - _pipelineOwner.rootNode = value; + /// The [PipelineOwner] that is the root of the PipelineOwner tree. + /// + /// {@macro flutter.rendering.createRootPipelineOwner} + PipelineOwner get rootPipelineOwner => _rootPipelineOwner; + late PipelineOwner _rootPipelineOwner; + + /// The [RenderView]s managed by this binding. + /// + /// A [RenderView] is added by [addRenderView] and removed by [removeRenderView]. + Iterable get renderViews => _viewIdToRenderView.values; + final Map _viewIdToRenderView = {}; + + /// Adds a [RenderView] to this binding. + /// + /// The binding will interact with the [RenderView] in the following ways: + /// + /// * setting and updating [RenderView.configuration], + /// * calling [RenderView.compositeFrame] when it is time to produce a new + /// frame, and + /// * forwarding relevant pointer events to the [RenderView] for hit testing. + /// + /// To remove a [RenderView] from the binding, call [removeRenderView]. + void addRenderView(RenderView view) { + final Object viewId = view.flutterView.viewId; + assert(!_viewIdToRenderView.containsValue(view)); + assert(!_viewIdToRenderView.containsKey(viewId)); + _viewIdToRenderView[viewId] = view; + view.configuration = createViewConfigurationFor(view); + } + + /// Removes a [RenderView] previously added with [addRenderView] from the + /// binding. + void removeRenderView(RenderView view) { + final Object viewId = view.flutterView.viewId; + assert(_viewIdToRenderView[viewId] == view); + _viewIdToRenderView.remove(viewId); + } + + /// Returns a [ViewConfiguration] configured for the provided [RenderView] + /// based on the current environment. + /// + /// This is called during [addRenderView] and also in response to changes to + /// the system metrics to update all [renderViews] added to the binding. + /// + /// Bindings can override this method to change what size or device pixel + /// ratio the [RenderView] will use. For example, the testing framework uses + /// this to force the display into 800x600 when a test is run on the device + /// using `flutter run`. + @protected + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + final FlutterView view = renderView.flutterView; + final double devicePixelRatio = view.devicePixelRatio; + return ViewConfiguration( + size: view.physicalSize / devicePixelRatio, + devicePixelRatio: devicePixelRatio, + ); } /// Called when the system metrics change. @@ -240,8 +361,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @protected @visibleForTesting void handleMetricsChanged() { - renderView.configuration = createViewConfiguration(); - if (renderView.child != null) { + bool forceFrame = false; + for (final RenderView view in renderViews) { + forceFrame = forceFrame || view.child != null; + view.configuration = createViewConfigurationFor(view); + } + if (forceFrame) { scheduleForcedFrame(); } } @@ -288,25 +413,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @protected void handlePlatformBrightnessChanged() { } - /// Returns a [ViewConfiguration] configured for the [RenderView] based on the - /// current environment. - /// - /// This is called during construction and also in response to changes to the - /// system metrics. - /// - /// Bindings can override this method to change what size or device pixel - /// ratio the [RenderView] will use. For example, the testing framework uses - /// this to force the display into 800x600 when a test is run on the device - /// using `flutter run`. - ViewConfiguration createViewConfiguration() { - final FlutterView view = platformDispatcher.implicitView!; - final double devicePixelRatio = view.devicePixelRatio; - return ViewConfiguration( - size: view.physicalSize / devicePixelRatio, - devicePixelRatio: devicePixelRatio, - ); - } - /// Creates a [MouseTracker] which manages state about currently connected /// mice, for hover notification. /// @@ -335,19 +441,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @override void performSemanticsAction(SemanticsActionEvent action) { - _pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments); - } - - void _handleSemanticsOwnerCreated() { - renderView.scheduleInitialSemantics(); - } - - void _handleSemanticsUpdate(ui.SemanticsUpdate update) { - renderView.updateSemantics(update); - } - - void _handleSemanticsOwnerDisposed() { - renderView.clearSemantics(); + // Due to the asynchronicity in some screen readers (they may not have + // processed the latest semantics update yet) this code is more forgiving + // and actions for views/nodes that no longer exist are gracefully ignored. + _viewIdToRenderView[action.viewId]?.owner?.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments); } void _handleWebFirstFrame(Duration _) { @@ -491,12 +588,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture // When editing the above, also update widgets/binding.dart's copy. @protected void drawFrame() { - pipelineOwner.flushLayout(); - pipelineOwner.flushCompositingBits(); - pipelineOwner.flushPaint(); + rootPipelineOwner.flushLayout(); + rootPipelineOwner.flushCompositingBits(); + rootPipelineOwner.flushPaint(); if (sendFramesToEngine) { - renderView.compositeFrame(); // this sends the bits to the GPU - pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. + for (final RenderView renderView in renderViews) { + renderView.compositeFrame(); // this sends the bits to the GPU + } + rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS. _firstFrameSent = true; } } @@ -504,34 +603,25 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @override Future performReassemble() async { await super.performReassemble(); - if (BindingBase.debugReassembleConfig?.widgetName == null) { - if (!kReleaseMode) { - FlutterTimeline.startSync('Preparing Hot Reload (layout)'); - } - try { + if (!kReleaseMode) { + FlutterTimeline.startSync('Preparing Hot Reload (layout)'); + } + try { + for (final RenderView renderView in renderViews) { renderView.reassemble(); - } finally { - if (!kReleaseMode) { - FlutterTimeline.finishSync(); - } + } + } finally { + if (!kReleaseMode) { + FlutterTimeline.finishSync(); } } scheduleWarmUpFrame(); await endOfFrame; } - late final int _implicitViewId = platformDispatcher.implicitView!.viewId; - @override void hitTestInView(HitTestResult result, Offset position, int viewId) { - // Currently Flutter only supports one view, the implicit view `renderView`. - // TODO(dkwingsmt): After Flutter supports multi-view, look up the correct - // render view for the ID. - // https://github.com/flutter/flutter/issues/121573 - assert(viewId == _implicitViewId, - 'Unexpected view ID $viewId (expecting implicit view ID $_implicitViewId)'); - assert(viewId == renderView.flutterView.viewId); - renderView.hitTest(result, position: position); + _viewIdToRenderView[viewId]?.hitTest(result, position: position); super.hitTestInView(result, position, viewId); } @@ -541,40 +631,93 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture child.markNeedsPaint(); child.visitChildren(visitor); }; - instance.renderView.visitChildren(visitor); + for (final RenderView renderView in renderViews) { + renderView.visitChildren(visitor); + } return endOfFrame; } } -/// Prints a textual representation of the entire render tree. +String _debugCollectRenderTrees() { + if (RendererBinding.instance.renderViews.isEmpty) { + return 'No render tree root was added to the binding.'; + } + return [ + for (final RenderView renderView in RendererBinding.instance.renderViews) + renderView.toStringDeep(), + ].join('\n\n'); +} + +/// Prints a textual representation of the render trees. +/// +/// {@template flutter.rendering.debugDumpRenderTree} +/// It prints the trees associated with every [RenderView] in +/// [RendererBinding.renderView], separated by two blank lines. +/// {@endtemplate} void debugDumpRenderTree() { - debugPrint(RendererBinding.instance.renderView.toStringDeep()); + debugPrint(_debugCollectRenderTrees()); } -/// Prints a textual representation of the entire layer tree. +String _debugCollectLayerTrees() { + if (RendererBinding.instance.renderViews.isEmpty) { + return 'No render tree root was added to the binding.'; + } + return [ + for (final RenderView renderView in RendererBinding.instance.renderViews) + renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable for $renderView.', + ].join('\n\n'); +} + +/// Prints a textual representation of the layer trees. +/// +/// {@macro flutter.rendering.debugDumpRenderTree} void debugDumpLayerTree() { - debugPrint(RendererBinding.instance.renderView.debugLayer?.toStringDeep()); + debugPrint(_debugCollectLayerTrees()); } -/// Prints a textual representation of the entire semantics tree. -/// This will only work if there is a semantics client attached. -/// Otherwise, a notice that no semantics are available will be printed. +String _debugCollectSemanticsTrees(DebugSemanticsDumpOrder childOrder) { + if (RendererBinding.instance.renderViews.isEmpty) { + return 'No render tree root was added to the binding.'; + } + const String explanation = 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' + 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' + 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'; + final List trees = []; + bool printedExplanation = false; + for (final RenderView renderView in RendererBinding.instance.renderViews) { + final String? tree = renderView.debugSemantics?.toStringDeep(childOrder: childOrder); + if (tree != null) { + trees.add(tree); + } else { + String message = 'Semantics not generated for $renderView.'; + if (!printedExplanation) { + printedExplanation = true; + message = '$message\n$explanation'; + } + trees.add(message); + } + } + return trees.join('\n\n'); +} + +/// Prints a textual representation of the semantics trees. +/// +/// {@macro flutter.rendering.debugDumpRenderTree} +/// +/// Semantics trees are only constructed when semantics are enabled (see +/// [SemanticsBinding.semanticsEnabled]). If a semantics tree is not available, +/// a notice about the missing semantics tree is printed instead. /// /// The order in which the children of a [SemanticsNode] will be printed is /// controlled by the [childOrder] parameter. void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) { - debugPrint(_generateSemanticsTree(childOrder)); + debugPrint(_debugCollectSemanticsTrees(childOrder)); } -String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) { - final String? tree = RendererBinding.instance.renderView.debugSemantics?.toStringDeep(childOrder: childOrder); - if (tree != null) { - return tree; - } - return 'Semantics not generated.\n' - 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' - 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' - 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'; +/// Prints a textual representation of the [PipelineOwner] tree rooted at +/// [RendererBinding.rootPipelineOwner]. +void debugDumpPipelineOwnerTree() { + debugPrint(RendererBinding.instance.rootPipelineOwner.toStringDeep()); } /// A concrete binding for applications that use the Rendering framework @@ -595,18 +738,17 @@ String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) { /// rendering layer directly. If you are writing to a higher-level /// library, such as the Flutter Widgets library, then you would use /// that layer's binding (see [WidgetsFlutterBinding]). +/// +/// The [RenderingFlutterBinding] can manage multiple render trees. Each render +/// tree is rooted in a [RenderView] that must be added to the binding via +/// [addRenderView] to be consider during frame production, hit testing, etc. +/// Furthermore, the render tree must be managed by a [PipelineOwner] that is +/// part of the pipeline owner tree rooted at [rootPipelineOwner]. +/// +/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way +/// described above is left as a responsibility for a higher level abstraction. +/// The binding does not own any [RenderView]s directly. class RenderingFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, SemanticsBinding, PaintingBinding, RendererBinding { - /// Creates a binding for the rendering layer. - /// - /// The `root` render box is attached directly to the [renderView] and is - /// given constraints that require it to fill the window. - /// - /// This binding does not automatically schedule any frames. Callers are - /// responsible for deciding when to first call [scheduleFrame]. - RenderingFlutterBinding({ RenderBox? root }) { - renderView.child = root; - } - /// Returns an instance of the binding that implements /// [RendererBinding]. If no binding has yet been initialized, the /// [RenderingFlutterBinding] class is used to create and initialize @@ -645,3 +787,82 @@ class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifol super.dispose(); } } + +// A [PipelineOwner] that cannot have a root node. +class _DefaultRootPipelineOwner extends PipelineOwner { + _DefaultRootPipelineOwner() : super(onSemanticsUpdate: _onSemanticsUpdate); + + @override + set rootNode(RenderObject? _) { + assert(() { + throw FlutterError.fromParts([ + ErrorSummary( + 'Cannot set a rootNode on the default root pipeline owner.', + ), + ErrorDescription( + 'By default, the RendererBinding.rootPipelineOwner is not configured ' + 'to manage a root node because this pipeline owner does not define a ' + 'proper onSemanticsUpdate callback to handle semantics for that node.', + ), + ErrorHint( + 'Typically, the root pipeline owner does not manage a root node. ' + 'Instead, properly configured child pipeline owners (which do manage ' + 'root nodes) are added to it. Alternatively, if you do want to set a ' + 'root node for the root pipeline owner, override ' + 'RendererBinding.createRootPipelineOwner to create a ' + 'pipeline owner that is configured to properly handle semantics for ' + 'the provided root node.' + ), + ]); + }()); + } + + static void _onSemanticsUpdate(ui.SemanticsUpdate _) { + // Neve called because we don't have a root node. + assert(false); + } +} + +// Prior to multi view support, the [RendererBinding] would own a long-lived +// [RenderView], that was never disposed (see [RendererBinding.renderView]). +// With multi view support, the [RendererBinding] no longer owns a [RenderView] +// and instead higher level abstractions (like the [View] widget) can add/remove +// multiple [RenderView]s to the binding as needed. When the [View] widget is no +// longer needed, it expects to dispose its [RenderView]. +// +// This special version of a [RenderView] now exists as a bridge between those +// worlds to continue supporting the [RendererBinding.renderView] property +// through its deprecation period. Per the property's contract, it is supposed +// to be long-lived, but it is also managed by a [View] widget (introduced by +// [WidgetsBinding.wrapWithDefaultView]), that expects to dispose its render +// object at the end of the widget's life time. This special version now +// implements logic to reset the [RenderView] when it is "disposed" so it can be +// reused by another [View] widget. +// +// Once the deprecated [RendererBinding.renderView] property is removed, this +// class is no longer necessary. +class _ReusableRenderView extends RenderView { + _ReusableRenderView({required super.view}); + + bool _initialFramePrepared = false; + + @override + void prepareInitialFrame() { + if (_initialFramePrepared) { + return; + } + super.prepareInitialFrame(); + _initialFramePrepared = true; + } + + @override + void scheduleInitialSemantics() { + clearSemantics(); + super.scheduleInitialSemantics(); + } + + @override + void dispose() { // ignore: must_call_super + child = null; + } +} diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 6a9a00791ef15..cedd6362b9954 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -899,8 +899,6 @@ class BoxHitTestResult extends HitTestResult { /// A hit test entry used by [RenderBox]. class BoxHitTestEntry extends HitTestEntry { /// Creates a box hit test entry. - /// - /// The [localPosition] argument must not be null. BoxHitTestEntry(super.target, this.localPosition); /// The position of the hit test in the local coordinates of [target]. @@ -911,6 +909,15 @@ class BoxHitTestEntry extends HitTestEntry { } /// Parent data used by [RenderBox] and its subclasses. +/// +/// {@tool dartpad} +/// Parent data is used to communicate to a render object about its +/// children. In this example, there are two render objects that perform +/// text layout. They use parent data to identify the kind of child they +/// are laying out, and space the children accordingly. +/// +/// ** See code in examples/api/lib/rendering/box/parent_data.0.dart ** +/// {@end-tool} class BoxParentData extends ParentData { /// The offset at which to paint the child in the parent's coordinate system. Offset offset = Offset.zero; diff --git a/packages/flutter/lib/src/rendering/custom_layout.dart b/packages/flutter/lib/src/rendering/custom_layout.dart index e5ddee86281c4..7c28feca822cd 100644 --- a/packages/flutter/lib/src/rendering/custom_layout.dart +++ b/packages/flutter/lib/src/rendering/custom_layout.dart @@ -303,8 +303,6 @@ class RenderCustomMultiChildLayoutBox extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { /// Creates a render object that customizes the layout of multiple children. - /// - /// The [delegate] argument must not be null. RenderCustomMultiChildLayoutBox({ List? children, required MultiChildLayoutDelegate delegate, diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index e59d51ad99439..e9e176ec6dc14 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -119,6 +119,23 @@ typedef SemanticsBuilderCallback = List Function(Size si /// ``` /// {@end-tool} /// +/// ## Composition and the sharing of canvases +/// +/// Widgets (or rather, render objects) are composited together using a minimum +/// number of [Canvas]es, for performance reasons. As a result, a +/// [CustomPainter]'s [Canvas] may be the same as that used by other widgets +/// (including other [CustomPaint] widgets). +/// +/// This is mostly unnoticeable, except when using unusual [BlendMode]s. For +/// example, trying to use [BlendMode.dstOut] to "punch a hole" through a +/// previously-drawn image may erase more than was intended, because previous +/// widgets will have been painted onto the same canvas. +/// +/// To avoid this issue, consider using [Canvas.saveLayer] and +/// [Canvas.restore] when using such blend modes. Creating new layers is +/// relatively expensive, however, and should be done sparingly to avoid +/// introducing jank. +/// /// See also: /// /// * [Canvas], the class that a custom painter uses to paint. @@ -286,8 +303,6 @@ abstract class CustomPainter extends Listenable { @immutable class CustomPainterSemantics { /// Creates semantics information describing a rectangle on a canvas. - /// - /// Arguments `rect` and `properties` must not be null. const CustomPainterSemantics({ this.key, required this.rect, @@ -883,6 +898,9 @@ class RenderCustomPaint extends RenderProxyBox { if (properties.button != null) { config.isButton = properties.button!; } + if (properties.expanded != null) { + config.isExpanded = properties.expanded; + } if (properties.link != null) { config.isLink = properties.link!; } diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index a357bde585572..721e0aa45cf1f 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -36,8 +36,6 @@ const Radius _kFloatingCursorRadius = Radius.circular(1.0); @immutable class TextSelectionPoint { /// Creates a description of a point in a text selection. - /// - /// The [point] argument must not be null. const TextSelectionPoint(this.point, this.direction); /// Coordinates of the lower left or lower right corner of the selection, @@ -261,9 +259,7 @@ class VerticalCaretMovementRun implements Iterator { class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin, RenderInlineChildrenContainerDefaults implements TextLayoutMetrics { /// Creates a render object that implements the visual aspects of a text field. /// - /// The [textAlign] argument must not be null. It defaults to [TextAlign.start]. - /// - /// The [textDirection] argument must not be null. + /// The [textAlign] argument defaults to [TextAlign.start]. /// /// If [showCursor] is not specified, then it defaults to hiding the cursor. /// @@ -271,8 +267,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// the number of lines. By default, it is 1, meaning this is a single-line /// text field. If it is not null, it must be greater than zero. /// - /// The [offset] is required and must not be null. You can use [ - /// ViewportOffset.zero] if you have no need for scrolling. + /// Use [ViewportOffset.zero] for the [offset] if there is no need for + /// scrolling. RenderEditable({ InlineSpan? text, required TextDirection textDirection, @@ -288,7 +284,13 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, bool expands = false, StrutStyle? strutStyle, Color? selectionColor, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, TextSelection? selection, required ViewportOffset offset, this.ignorePointer = false, @@ -326,6 +328,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, !expands || (maxLines == null && minLines == null), 'minLines and maxLines must be null when expands is true.', ), + assert( + identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ), assert(obscuringCharacter.characters.length == 1), assert(cursorWidth >= 0.0), assert(cursorHeight == null || cursorHeight >= 0.0), @@ -333,7 +339,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, text: text, textAlign: textAlign, textDirection: textDirection, - textScaleFactor: textScaleFactor, + textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler, locale: locale, maxLines: maxLines == 1 ? 1 : null, strutStyle: strutStyle, @@ -554,7 +560,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// Character used for obscuring text if [obscureText] is true. /// - /// Cannot be null, and must have a length of exactly one. + /// Must have a length of exactly one. String get obscuringCharacter => _obscuringCharacter; String _obscuringCharacter; set obscuringCharacter(String value) { @@ -597,8 +603,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// The object that controls the text selection, used by this render object /// for implementing cut, copy, and paste keyboard shortcuts. /// - /// It must not be null. It will make cut, copy and paste functionality work - /// with the most recently set [TextSelectionDelegate]. + /// It will make cut, copy and paste functionality work with the most recently + /// set [TextSelectionDelegate]. TextSelectionDelegate textSelectionDelegate; /// Track whether position of the start of the selected text is within the viewport. @@ -792,8 +798,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, } /// How the text should be aligned horizontally. - /// - /// This must not be null. TextAlign get textAlign => _textPainter.textAlign; set textAlign(TextAlign value) { if (_textPainter.textAlign == value) { @@ -814,8 +818,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. - /// - /// This must not be null. // TextPainter.textDirection is nullable, but it is set to a // non-null value in the RenderEditable constructor and we refuse to // set it to null here, so _textPainter.textDirection cannot be null. @@ -868,7 +870,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// The color to use when painting the cursor aligned to the text while /// rendering the floating cursor. /// - /// The default is light grey. + /// Typically this would be set to [CupertinoColors.inactiveGray]. + /// + /// If this is null, the background cursor is not painted. Color? get backgroundCursorColor => _caretPainter.backgroundCursorColor; set backgroundCursorColor(Color? value) { _caretPainter.backgroundCursorColor = value; @@ -985,16 +989,35 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, _selectionPainter.highlightColor = value; } + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double get textScaleFactor => _textPainter.textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) set textScaleFactor(double value) { - if (_textPainter.textScaleFactor == value) { + textScaler = TextScaler.linear(value); + } + + /// {@macro flutter.painting.textPainter.textScaler} + TextScaler get textScaler => _textPainter.textScaler; + set textScaler(TextScaler value) { + if (_textPainter.textScaler == value) { return; } - _textPainter.textScaleFactor = value; + _textPainter.textScaler = value; markNeedsTextLayout(); } @@ -1226,7 +1249,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -1887,17 +1910,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, @override @protected bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + final Offset effectivePosition = position - _paintOffset; final InlineSpan? textSpan = _textPainter.text; - if (textSpan != null) { - final Offset effectivePosition = position - _paintOffset; - final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition); - final Object? span = textSpan.getSpanForPosition(textPosition); - if (span is HitTestTarget) { + switch (textSpan?.getSpanForPosition(_textPainter.getPositionForOffset(effectivePosition))) { + case final HitTestTarget span: result.add(HitTestEntry(span)); return true; - } + case _: + return hitTestInlineChildren(result, effectivePosition); } - return hitTestInlineChildren(result, position); } late TapGestureRecognizer _tap; @@ -2528,7 +2549,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, properties.add(IntProperty('minLines', minLines)); properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); properties.add(ColorProperty('selectionColor', selectionColor)); - properties.add(DoubleProperty('textScaleFactor', textScaleFactor)); + properties.add(DiagnosticsProperty('textScaler', textScaler, defaultValue: TextScaler.noScaling)); properties.add(DiagnosticsProperty('locale', locale, defaultValue: null)); properties.add(DiagnosticsProperty('selection', selection)); properties.add(DiagnosticsProperty('offset', offset)); @@ -2618,8 +2639,8 @@ class _RenderEditableCustomPaint extends RenderBox { /// when only auxiliary content changes (e.g. a blinking cursor) are present. It /// will be scheduled to repaint when: /// -/// * It's assigned to a new [RenderEditable] and the [shouldRepaint] method -/// returns true. +/// * It's assigned to a new [RenderEditable] (replacing a prior +/// [RenderEditablePainter]) and the [shouldRepaint] method returns true. /// * Any of the [RenderEditable]s it is attached to repaints. /// * The [notifyListeners] method is called, which typically happens when the /// painter's attributes change. @@ -2630,9 +2651,8 @@ class _RenderEditableCustomPaint extends RenderBox { /// and sets it as the foreground painter of the [RenderEditable]. /// * [RenderEditable.painter], which takes a [RenderEditablePainter] /// and sets it as the background painter of the [RenderEditable]. -/// * [CustomPainter] a similar class which paints within a [RenderCustomPaint]. +/// * [CustomPainter], a similar class which paints within a [RenderCustomPaint]. abstract class RenderEditablePainter extends ChangeNotifier { - /// Determines whether repaint is needed when a new [RenderEditablePainter] /// is provided to a [RenderEditable]. /// @@ -2768,6 +2788,11 @@ class _CaretPainter extends RenderEditablePainter { notifyListeners(); } + // This is directly manipulated by the RenderEditable during + // setFloatingCursor. + // + // When changing this value, the caller is responsible for ensuring that + // listeners are notified. bool showRegularCaret = false; final Paint caretPaint = Paint(); diff --git a/packages/flutter/lib/src/rendering/error.dart b/packages/flutter/lib/src/rendering/error.dart index 41fa359da602d..ee7806faee6fa 100644 --- a/packages/flutter/lib/src/rendering/error.dart +++ b/packages/flutter/lib/src/rendering/error.dart @@ -157,11 +157,11 @@ class RenderErrorBox extends RenderBox { width -= padding.left + padding.right; left += padding.left; } - _paragraph!.layout(ui.ParagraphConstraints(width: width)); - if (size.height > padding.top + _paragraph!.height + padding.bottom) { + _paragraph.layout(ui.ParagraphConstraints(width: width)); + if (size.height > padding.top + _paragraph.height + padding.bottom) { top += padding.top; } - context.canvas.drawParagraph(_paragraph!, offset + Offset(left, top)); + context.canvas.drawParagraph(_paragraph, offset + Offset(left, top)); } } catch (error) { // If an error happens here we're in a terrible state, so we really should diff --git a/packages/flutter/lib/src/rendering/flex.dart b/packages/flutter/lib/src/rendering/flex.dart index 6d10cf38c1af4..da12a9cd94437 100644 --- a/packages/flutter/lib/src/rendering/flex.dart +++ b/packages/flutter/lib/src/rendering/flex.dart @@ -472,7 +472,7 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin _clipBehavior; Clip _clipBehavior = Clip.none; set clipBehavior(Clip value) { @@ -730,7 +730,7 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { diff --git a/packages/flutter/lib/src/rendering/image.dart b/packages/flutter/lib/src/rendering/image.dart index 0180a26ad190c..593bc97c6a552 100644 --- a/packages/flutter/lib/src/rendering/image.dart +++ b/packages/flutter/lib/src/rendering/image.dart @@ -23,9 +23,8 @@ export 'package:flutter/painting.dart' show class RenderImage extends RenderBox { /// Creates a render box that displays an image. /// - /// The [scale], [alignment], [repeat], [matchTextDirection] and [filterQuality] arguments - /// must not be null. The [textDirection] argument must not be null if - /// [alignment] will need resolving or if [matchTextDirection] is true. + /// The [textDirection] argument must not be null if [alignment] will need + /// resolving or if [matchTextDirection] is true. RenderImage({ ui.Image? image, this.debugImageLabel, diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 779574f3f872c..f2bd274fd5212 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -172,6 +172,9 @@ abstract class Layer with DiagnosticableTreeMixin { } void _fireCompositionCallbacks({required bool includeChildren}) { + if (_callbacks.isEmpty) { + return; + } for (final VoidCallback callback in List.of(_callbacks.values)) { callback(); } @@ -495,46 +498,43 @@ abstract class Layer with DiagnosticableTreeMixin { _needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene; } - /// The owner for this node (null if unattached). + /// The owner for this layer (null if unattached). + /// + /// The entire layer tree that this layer belongs to will have the same owner. /// - /// The entire subtree that this node belongs to will have the same owner. + /// Typically the owner is a [RenderView]. Object? get owner => _owner; Object? _owner; - /// Whether this node is in a tree whose root is attached to something. + /// Whether the layer tree containing this layer is attached to an owner. /// /// This becomes true during the call to [attach]. /// /// This becomes false during the call to [detach]. bool get attached => _owner != null; - /// Mark this node as attached to the given owner. + /// Mark this layer as attached to the given owner. /// /// Typically called only from the [parent]'s [attach] method, and by the /// [owner] to mark the root of a tree as attached. /// - /// Subclasses with children should override this method to first call their - /// inherited [attach] method, and then [attach] all their children to the - /// same [owner]. - /// - /// Implementations of this method should start with a call to the inherited - /// method, as in `super.attach(owner)`. + /// Subclasses with children should override this method to + /// [attach] all their children to the same [owner] + /// after calling the inherited method, as in `super.attach(owner)`. @mustCallSuper void attach(covariant Object owner) { assert(_owner == null); _owner = owner; } - /// Mark this node as detached. + /// Mark this layer as detached from its owner. /// /// Typically called only from the [parent]'s [detach], and by the [owner] to /// mark the root of a tree as detached. /// - /// Subclasses with children should override this method to first call their - /// inherited [detach] method, and then [detach] all their children. - /// - /// Implementations of this method should end with a call to the inherited - /// method, as in `super.detach()`. + /// Subclasses with children should override this method to + /// [detach] all their children after calling the inherited method, + /// as in `super.detach()`. @mustCallSuper void detach() { assert(_owner != null); @@ -542,10 +542,12 @@ abstract class Layer with DiagnosticableTreeMixin { assert(parent == null || attached == parent!.attached); } - /// The depth of this node in the tree. + /// The depth of this layer in the layer tree. /// /// The depth of nodes in a tree monotonically increases as you traverse down - /// the tree. + /// the tree. There's no guarantee regarding depth between siblings. + /// + /// The depth is used to ensure that nodes are processed in depth order. int get depth => _depth; int _depth = 0; @@ -917,9 +919,9 @@ class PictureLayer extends Layer { /// /// See also: /// -/// * +/// * [TextureRegistry](/javadoc/io/flutter/view/TextureRegistry.html) /// for how to create and manage backend textures on Android. -/// * +/// * [TextureRegistry Protocol](/ios-embedder/protocol_flutter_texture_registry-p.html) /// for how to create and manage backend textures on iOS. class TextureLayer extends Layer { /// Creates a texture layer bounded by [rect] and with backend texture @@ -972,8 +974,6 @@ class TextureLayer extends Layer { /// on iOS. class PlatformViewLayer extends Layer { /// Creates a platform view layer. - /// - /// The `rect` and `viewId` parameters must not be null. PlatformViewLayer({ required this.rect, required this.viewId, @@ -1409,8 +1409,6 @@ class ContainerLayer extends Layer { /// may explicitly allow null as a value, for example if they know that they /// transform all their children identically. /// - /// The `transform` argument must not be null. - /// /// Used by [FollowerLayer] to transform its child to a [LeaderLayer]'s /// position. void applyTransform(Layer? child, Matrix4 transform) { @@ -1608,7 +1606,7 @@ class ClipRectLayer extends ContainerLayer { /// The [clipRect] argument must not be null before the compositing phase of /// the pipeline. /// - /// The [clipBehavior] argument must not be null, and must not be [Clip.none]. + /// The [clipBehavior] argument must not be [Clip.none]. ClipRectLayer({ Rect? clipRect, Clip clipBehavior = Clip.hardEdge, @@ -2366,9 +2364,8 @@ class LayerLink { class LeaderLayer extends ContainerLayer { /// Creates a leader layer. /// - /// The [link] property must not be null, and must not have been provided to - /// any other [LeaderLayer] layers that are [attached] to the layer tree at - /// the same time. + /// The [link] property must not have been provided to any other [LeaderLayer] + /// layers that are [attached] to the layer tree at the same time. /// /// The [offset] property must be non-null before the compositing phase of the /// pipeline. @@ -2478,8 +2475,6 @@ class LeaderLayer extends ContainerLayer { class FollowerLayer extends ContainerLayer { /// Creates a follower layer. /// - /// The [link] property must not be null. - /// /// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties /// must be non-null before the compositing phase of the pipeline. FollowerLayer({ @@ -2803,8 +2798,6 @@ class FollowerLayer extends ContainerLayer { /// if [opaque] is true and the layer's annotation is added. class AnnotatedRegionLayer extends ContainerLayer { /// Creates a new layer that annotates its children with [value]. - /// - /// The [value] provided cannot be null. AnnotatedRegionLayer( this.value, { this.size, diff --git a/packages/flutter/lib/src/rendering/list_wheel_viewport.dart b/packages/flutter/lib/src/rendering/list_wheel_viewport.dart index 48bc762367999..c66e97e3e55a4 100644 --- a/packages/flutter/lib/src/rendering/list_wheel_viewport.dart +++ b/packages/flutter/lib/src/rendering/list_wheel_viewport.dart @@ -146,7 +146,7 @@ class RenderListWheelViewport implements RenderAbstractViewport { /// Creates a [RenderListWheelViewport] which renders children on a wheel. /// - /// All arguments must not be null. Optional arguments have reasonable defaults. + /// Optional arguments have reasonable defaults. RenderListWheelViewport({ required this.childManager, required ViewportOffset offset, @@ -220,8 +220,6 @@ class RenderListWheelViewport /// viewport uses to select which part of its content to display. As the user /// scrolls the viewport, this value changes, which changes the content that /// is displayed. - /// - /// Must not be null. ViewportOffset get offset => _offset; ViewportOffset _offset; set offset(ViewportOffset value) { @@ -264,7 +262,7 @@ class RenderListWheelViewport /// /// Defaults to an arbitrary but aesthetically reasonable number of 2.0. /// - /// Must not be null and must be positive. + /// Must be a positive number. /// {@endtemplate} double get diameterRatio => _diameterRatio; double _diameterRatio; @@ -293,7 +291,7 @@ class RenderListWheelViewport /// A larger number brings the vanishing point closer and a smaller number /// pushes the vanishing point further. /// - /// Must not be null and must be positive. + /// Must be a positive number. /// {@endtemplate} double get perspective => _perspective; double _perspective; @@ -402,7 +400,7 @@ class RenderListWheelViewport /// The size of the children along the main axis. Children [RenderBox]es will /// be given the [BoxConstraints] of this exact size. /// - /// Must not be null and must be positive. + /// Must be a positive number. /// {@endtemplate} double get itemExtent => _itemExtent; double _itemExtent; @@ -432,7 +430,7 @@ class RenderListWheelViewport /// Changing this value will change the number of children built and shown /// inside the wheel. /// - /// Must not be null and must be positive. + /// Must be a positive number. /// {@endtemplate} /// /// Defaults to 1. @@ -454,9 +452,9 @@ class RenderListWheelViewport /// If false, every child will be painted. However the [Scrollable] is still /// the size of the viewport and detects gestures inside only. /// - /// Defaults to false. Must not be null. Cannot be true if [clipBehavior] - /// is not [Clip.none] since children outside the viewport will be clipped, and - /// therefore cannot render children outside the viewport. + /// Defaults to false. Cannot be true if [clipBehavior] is not [Clip.none] + /// since children outside the viewport will be clipped, and therefore cannot + /// render children outside the viewport. /// {@endtemplate} bool get renderChildrenOutsideViewport => _renderChildrenOutsideViewport; bool _renderChildrenOutsideViewport; @@ -475,7 +473,7 @@ class RenderListWheelViewport /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -1123,11 +1121,15 @@ class RenderListWheelViewport } @override - RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { + RevealedOffset getOffsetToReveal( + RenderObject target, + double alignment, { + Rect? rect, + Axis? axis, // Unused, only Axis.vertical supported by this viewport. + }) { // `target` is only fully revealed when in the selected/center position. Therefore, // this method always returns the offset that shows `target` in the center position, // which is the same offset for all `alignment` values. - rect ??= target.paintBounds; // `child` will be the last RenderObject before the viewport when walking up from `target`. diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 3e42a22ca2127..059e0a169c021 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:ui' as ui show PictureRecorder; -import 'dart:ui'; import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; @@ -14,7 +13,6 @@ import 'package:flutter/semantics.dart'; import 'debug.dart'; import 'layer.dart'; -import 'proxy_box.dart'; export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder, @@ -399,8 +397,16 @@ class PaintingContext extends ClipContext { /// If this hint is not set, the compositor will apply its own heuristics to /// decide whether the current layer is complex enough to benefit from /// caching. + /// + /// Calling this ensures a [Canvas] is available. Only draw calls on the + /// current canvas will be hinted; the hint is not propagated to new canvases + /// created after a new layer is added to the painting context (e.g. with + /// [addLayer] or [pushLayer]). void setIsComplexHint() { - _currentLayer?.isComplexHint = true; + if (_currentLayer == null) { + _startRecording(); + } + _currentLayer!.isComplexHint = true; } /// Hints that the painting in the current layer is likely to change next frame. @@ -409,8 +415,16 @@ class PaintingContext extends ClipContext { /// cache will not be used in the future. If this hint is not set, the /// compositor will apply its own heuristics to decide whether the current /// layer is likely to be reused in the future. + /// + /// Calling this ensures a [Canvas] is available. Only draw calls on the + /// current canvas will be hinted; the hint is not propagated to new canvases + /// created after a new layer is added to the painting context (e.g. with + /// [addLayer] or [pushLayer]). void setWillChangeHint() { - _currentLayer?.willChangeHint = true; + if (_currentLayer == null) { + _startRecording(); + } + _currentLayer!.willChangeHint = true; } /// Adds a composited leaf layer to the recording. @@ -798,8 +812,6 @@ abstract class Constraints { /// Signature for a function that is called for each [RenderObject]. /// /// Used by [RenderObject.visitChildren] and [RenderObject.visitChildrenForSemantics]. -/// -/// The `child` argument must not be null. typedef RenderObjectVisitor = void Function(RenderObject child); /// Signature for a function that is called during layout. @@ -872,7 +884,7 @@ class _LocalSemanticsHandle implements SemanticsHandle { /// without tying it to a specific binding implementation. All [PipelineOwner]s /// in a given tree must be attached to the same [PipelineManifold]. This /// happens automatically during [adoptChild]. -class PipelineOwner { +class PipelineOwner with DiagnosticableTreeMixin { /// Creates a pipeline owner. /// /// Typically created by the binding (e.g., [RendererBinding]), but can be @@ -986,7 +998,7 @@ class PipelineOwner { return true; }()); FlutterTimeline.startSync( - 'LAYOUT', + 'LAYOUT$_debugRootSuffixForTimelineEventNames', arguments: debugTimelineArguments, ); } @@ -1073,7 +1085,7 @@ class PipelineOwner { /// [flushPaint]. void flushCompositingBits() { if (!kReleaseMode) { - FlutterTimeline.startSync('UPDATING COMPOSITING BITS'); + FlutterTimeline.startSync('UPDATING COMPOSITING BITS$_debugRootSuffixForTimelineEventNames'); } _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) { @@ -1122,7 +1134,7 @@ class PipelineOwner { return true; }()); FlutterTimeline.startSync( - 'PAINT', + 'PAINT$_debugRootSuffixForTimelineEventNames', arguments: debugTimelineArguments, ); } @@ -1249,7 +1261,7 @@ class PipelineOwner { return; } if (!kReleaseMode) { - FlutterTimeline.startSync('SEMANTICS'); + FlutterTimeline.startSync('SEMANTICS$_debugRootSuffixForTimelineEventNames'); } assert(_semanticsOwner != null); assert(() { @@ -1281,6 +1293,20 @@ class PipelineOwner { } } + @override + List debugDescribeChildren() { + return [ + for (final PipelineOwner child in _children) + child.toDiagnosticsNode(), + ]; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('rootNode', rootNode, defaultValue: null)); + } + // TREE MANAGEMENT final Set _children = {}; @@ -1292,6 +1318,8 @@ class PipelineOwner { return true; } + String get _debugRootSuffixForTimelineEventNames => _debugParent == null ? ' (root)' : ''; + /// Mark this [PipelineOwner] as attached to the given [PipelineManifold]. /// /// Typically, this is only called directly on the root [PipelineOwner]. @@ -1317,7 +1345,9 @@ class PipelineOwner { assert(_manifold != null); _manifold!.removeListener(_updateSemanticsOwner); _manifold = null; - _updateSemanticsOwner(); + // Not updating the semantics owner here to not disrupt any of its clients + // in case we get re-attached. If necessary, semantics owner will be updated + // in "attach", or disposed in "dispose", if not reattached. for (final PipelineOwner child in _children) { child.detach(); @@ -1353,7 +1383,9 @@ class PipelineOwner { assert(!_children.contains(child)); assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); _children.add(child); - assert(_debugSetParent(child, this)); + if (!kReleaseMode) { + _debugSetParent(child, this); + } if (_manifold != null) { child.attach(_manifold!); } @@ -1371,7 +1403,9 @@ class PipelineOwner { assert(_children.contains(child)); assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); _children.remove(child); - assert(_debugSetParent(child, null)); + if (!kReleaseMode) { + _debugSetParent(child, null); + } if (_manifold != null) { child.detach(); } @@ -1386,6 +1420,26 @@ class PipelineOwner { void visitChildren(PipelineOwnerVisitor visitor) { _children.forEach(visitor); } + + /// Release any resources held by this pipeline owner. + /// + /// Prior to calling this method the pipeline owner must be removed from the + /// pipeline owner tree, i.e. it must have neither a parent nor any children + /// (see [dropChild]). It also must be [detach]ed from any [PipelineManifold]. + /// + /// The object is no longer usable after calling dispose. + void dispose() { + assert(_children.isEmpty); + assert(rootNode == null); + assert(_manifold == null); + assert(_debugParent == null); + _semanticsOwner?.dispose(); + _semanticsOwner = null; + _nodesNeedingLayout.clear(); + _nodesNeedingCompositingBitsUpdate.clear(); + _nodesNeedingPaint.clear(); + _nodesNeedingSemantics.clear(); + } } /// Signature for the callback to [PipelineOwner.visitChildren]. @@ -1687,21 +1741,22 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge } } - /// The depth of this node in the tree. + /// The depth of this render object in the render tree. /// /// The depth of nodes in a tree monotonically increases as you traverse down - /// the tree. - /// - /// Nodes always have a [depth] greater than their ancestors'. There's no - /// guarantee regarding depth between siblings. The depth of a node is used to - /// ensure that nodes are processed in depth order. The [depth] of a child can - /// be more than one greater than the [depth] of the parent, because the [depth] - /// values are never decreased: all that matters is that it's greater than the - /// parent. Consider a tree with a root node A, a child B, and a grandchild C. - /// Initially, A will have [depth] 0, B [depth] 1, and C [depth] 2. If C is - /// moved to be a child of A, sibling of B, then the numbers won't change. C's - /// [depth] will still be 2. The [depth] is automatically maintained by the - /// [adoptChild] and [dropChild] methods. + /// the tree: a node always has a [depth] greater than its ancestors. + /// There's no guarantee regarding depth between siblings. + /// + /// The [depth] of a child can be more than one greater than the [depth] of + /// the parent, because the [depth] values are never decreased: all that + /// matters is that it's greater than the parent. Consider a tree with a root + /// node A, a child B, and a grandchild C. Initially, A will have [depth] 0, + /// B [depth] 1, and C [depth] 2. If C is moved to be a child of A, + /// sibling of B, then the numbers won't change. C's [depth] will still be 2. + /// + /// The depth of a node is used to ensure that nodes are processed in + /// depth order. The [depth] is automatically maintained by the [adoptChild] + /// and [dropChild] methods. int get depth => _depth; int _depth = 0; @@ -1725,7 +1780,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge @protected void redepthChildren() { } - /// The parent of this node in the tree. + /// The parent of this render object in the render tree. + /// + /// The [parent] of the root node in the render tree is null. RenderObject? get parent => _parent; RenderObject? _parent; @@ -1901,7 +1958,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge final bool mutationsToDirtySubtreesAllowed = activeLayoutRoot.owner?._debugAllowMutationsToDirtySubtrees ?? false; final bool doingLayoutWithCallback = activeLayoutRoot._doingThisLayoutWithCallback; // Mutations on this subtree is allowed when: - // - the subtree is being mutated in a layout callback. + // - the "activeLayoutRoot" subtree is being mutated in a layout callback. // - a different part of the render tree is doing a layout callback, // and this subtree is being reparented to that subtree, as a result // of global key reparenting. @@ -2012,30 +2069,28 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge return layoutParent; } - /// The owner for this node (null if unattached). + /// The owner for this render object (null if unattached). /// - /// The entire subtree that this node belongs to will have the same owner. + /// The entire render tree that this render object belongs to + /// will have the same owner. PipelineOwner? get owner => _owner; PipelineOwner? _owner; - /// Whether this node is in a tree whose root is attached to something. + /// Whether the render tree this render object belongs to is attached to a [PipelineOwner]. /// /// This becomes true during the call to [attach]. /// /// This becomes false during the call to [detach]. bool get attached => _owner != null; - /// Mark this node as attached to the given owner. + /// Mark this render object as attached to the given owner. /// /// Typically called only from the [parent]'s [attach] method, and by the /// [owner] to mark the root of a tree as attached. /// - /// Subclasses with children should override this method to first call their - /// inherited [attach] method, and then [attach] all their children to the - /// same [owner]. - /// - /// Implementations of this method should start with a call to the inherited - /// method, as in `super.attach(owner)`. + /// Subclasses with children should override this method to + /// [attach] all their children to the same [owner] + /// after calling the inherited method, as in `super.attach(owner)`. @mustCallSuper void attach(PipelineOwner owner) { assert(!_debugDisposed); @@ -2067,16 +2122,14 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge } } - /// Mark this node as detached. + /// Mark this render object as detached from its [PipelineOwner]. /// /// Typically called only from the [parent]'s [detach], and by the [owner] to /// mark the root of a tree as detached. /// - /// Subclasses with children should override this method to first call their - /// inherited [detach] method, and then [detach] all their children. - /// - /// Implementations of this method should end with a call to the inherited - /// method, as in `super.detach()`. + /// Subclasses with children should override this method to + /// [detach] all their children after calling the inherited method, + /// as in `super.detach()`. @mustCallSuper void detach() { assert(_owner != null); @@ -3921,7 +3974,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge /// This mixin is typically used to implement render objects created /// in a [SingleChildRenderObjectWidget]. mixin RenderObjectWithChildMixin on RenderObject { - /// Checks whether the given render object has the correct [runtimeType] to be /// a child of this render object. /// diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 066f6137e715a..9ac2d9a499b51 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -18,6 +18,9 @@ import 'layout_helper.dart'; import 'object.dart'; import 'selection.dart'; +/// The start and end positions for a word. +typedef _WordBoundaryRecord = ({TextPosition wordStart, TextPosition wordEnd}); + const String _kEllipsis = '\u2026'; /// Used by the [RenderParagraph] to map its rendering children to their @@ -71,7 +74,7 @@ class TextParentData extends ParentData with ContainerParentDataMixin } @override - String toString() =>'widget: $span, ${offset == null ? "not laid out" : "offset: $offset"}'; + String toString() => 'widget: $span, ${offset == null ? "not laid out" : "offset: $offset"}'; } /// A mixin that provides useful default behaviors for text [RenderBox]es @@ -129,7 +132,7 @@ mixin RenderInlineChildrenContainerDefaults on RenderBox, ContainerRenderObjectM ui.PlaceholderAlignment.belowBaseline || ui.PlaceholderAlignment.bottom || ui.PlaceholderAlignment.middle || - ui.PlaceholderAlignment.top => null, + ui.PlaceholderAlignment.top => null, ui.PlaceholderAlignment.baseline => child.getDistanceToBaseline(span.baseline!), }, ); @@ -252,9 +255,6 @@ mixin RenderInlineChildrenContainerDefaults on RenderBox, ContainerRenderObjectM class RenderParagraph extends RenderBox with ContainerRenderObjectMixin, RenderInlineChildrenContainerDefaults, RelayoutWhenSystemFontsChangeMixin { /// Creates a paragraph render object. /// - /// The [text], [textAlign], [textDirection], [overflow], [softWrap], and - /// [textScaleFactor] arguments must not be null. - /// /// The [maxLines] property may be null (and indeed defaults to null), but if /// it is not null, it must be greater than zero. RenderParagraph(InlineSpan text, { @@ -262,7 +262,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin 0), + assert( + identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ), _softWrap = softWrap, _overflow = overflow, _selectionColor = selectionColor, @@ -280,7 +290,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin results = []; for (final _SelectableFragment fragment in _lastSelectableFragments!) { if (fragment._textSelectionStart != null && - fragment._textSelectionEnd != null && - fragment._textSelectionStart!.offset != fragment._textSelectionEnd!.offset) { + fragment._textSelectionEnd != null) { results.add( TextSelection( baseOffset: fragment._textSelectionStart!.offset, @@ -378,6 +387,9 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin _lastSelectableFragments?.isNotEmpty ?? false; + @override void markNeedsLayout() { _lastSelectableFragments?.forEach((_SelectableFragment element) => element.didChangeParagraphLayout()); @@ -424,9 +439,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin _textPainter.textDirection!; set textDirection(TextDirection value) { if (_textPainter.textDirection == value) { @@ -492,16 +503,35 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin _textPainter.textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) set textScaleFactor(double value) { - if (_textPainter.textScaleFactor == value) { + textScaler = TextScaler.linear(value); + } + + /// {@macro flutter.painting.textPainter.textScaler} + TextScaler get textScaler => _textPainter.textScaler; + set textScaler(TextScaler value) { + if (_textPainter.textScaler == value) { return; } - _textPainter.textScaleFactor = value; + _textPainter.textScaler = value; _overflowShader = null; markNeedsLayout(); } @@ -696,12 +726,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin('overflow', overflow)); properties.add( - DoubleProperty( - 'textScaleFactor', - textScaleFactor, - defaultValue: 1.0, - ), + DiagnosticsProperty('textScaler', textScaler, defaultValue: TextScaler.noScaling), ); properties.add( DiagnosticsProperty( @@ -1282,9 +1308,9 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin= range.start && wordBoundary.wordEnd.offset <= range.end); + if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) { + final bool isSamePosition = position.offset == existingSelectionEnd.offset; + final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; + final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset)); + if (shouldSwapEdges) { + if (position.offset < existingSelectionEnd.offset) { + targetPosition = wordBoundary.wordStart; + } else { + targetPosition = wordBoundary.wordEnd; + } + // When the selection is inverted by the new position it is necessary to + // swap the start edge (moving edge) with the end edge (static edge) to + // maintain the origin word within the selection. + final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionEnd); + assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end); + _setSelectionPosition(existingSelectionEnd.offset == localWordBoundary.wordStart.offset ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: true); + } else { + if (position.offset < existingSelectionEnd.offset) { + targetPosition = wordBoundary.wordStart; + } else if (position.offset > existingSelectionEnd.offset) { + targetPosition = wordBoundary.wordEnd; + } else { + // Keep the origin word in bounds when position is at the static edge. + targetPosition = existingSelectionStart; + } + } + } else { + if (existingSelectionEnd != null) { + // If the end edge exists and the start edge is being moved, then the + // start edge is moved to encompass the entire word at the new position. + if (position.offset < existingSelectionEnd.offset) { + targetPosition = wordBoundary.wordStart; + } else { + targetPosition = wordBoundary.wordEnd; + } + } else { + // Move the start edge to the closest word boundary. + targetPosition = _closestWordBoundary(wordBoundary, position); + } + } + } else { + // The position is not contained within the current rect. The targetPosition + // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] + // for a more in depth explanation on this adjustment. + if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) { + // When the selection is inverted by the new position it is necessary to + // swap the start edge (moving edge) with the end edge (static edge) to + // maintain the origin word within the selection. + final bool isSamePosition = position.offset == existingSelectionEnd.offset; + final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; + final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset)); + + if (shouldSwapEdges) { + final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionEnd); + assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end); + _setSelectionPosition(isSelectionInverted ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: true); + } + } + } + return targetPosition ?? position; + } + + TextPosition _updateSelectionEndEdgeByWord( + _WordBoundaryRecord? wordBoundary, + TextPosition position, + TextPosition? existingSelectionStart, + TextPosition? existingSelectionEnd, + ) { + TextPosition? targetPosition; + if (wordBoundary != null) { + assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end); + if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) { + final bool isSamePosition = position.offset == existingSelectionStart.offset; + final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; + final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset < existingSelectionStart.offset)); + if (shouldSwapEdges) { + if (position.offset < existingSelectionStart.offset) { + targetPosition = wordBoundary.wordStart; + } else { + targetPosition = wordBoundary.wordEnd; + } + // When the selection is inverted by the new position it is necessary to + // swap the end edge (moving edge) with the start edge (static edge) to + // maintain the origin word within the selection. + final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionStart); + assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end); + _setSelectionPosition(existingSelectionStart.offset == localWordBoundary.wordStart.offset ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: false); + } else { + if (position.offset < existingSelectionStart.offset) { + targetPosition = wordBoundary.wordStart; + } else if (position.offset > existingSelectionStart.offset) { + targetPosition = wordBoundary.wordEnd; + } else { + // Keep the origin word in bounds when position is at the static edge. + targetPosition = existingSelectionEnd; + } + } + } else { + if (existingSelectionStart != null) { + // If the start edge exists and the end edge is being moved, then the + // end edge is moved to encompass the entire word at the new position. + if (position.offset < existingSelectionStart.offset) { + targetPosition = wordBoundary.wordStart; + } else { + targetPosition = wordBoundary.wordEnd; + } + } else { + // Move the end edge to the closest word boundary. + targetPosition = _closestWordBoundary(wordBoundary, position); + } + } + } else { + // The position is not contained within the current rect. The targetPosition + // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] + // for a more in depth explanation on this adjustment. + if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) { + // When the selection is inverted by the new position it is necessary to + // swap the end edge (moving edge) with the start edge (static edge) to + // maintain the origin word within the selection. + final bool isSamePosition = position.offset == existingSelectionStart.offset; + final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset; + final bool shouldSwapEdges = isSelectionInverted != (position.offset < existingSelectionStart.offset) || isSamePosition; + if (shouldSwapEdges) { + final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionStart); + assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end); + _setSelectionPosition(isSelectionInverted ? localWordBoundary.wordStart : localWordBoundary.wordEnd, isEnd: false); + } + } + } + return targetPosition ?? position; + } + + SelectionResult _updateSelectionEdgeByWord(Offset globalPosition, {required bool isEnd}) { + // When the start/end edges are swapped, i.e. the start is after the end, and + // the scrollable synthesizes an event for the opposite edge, this will potentially + // move the opposite edge outside of the origin word boundary and we are unable to recover. + final TextPosition? existingSelectionStart = _textSelectionStart; + final TextPosition? existingSelectionEnd = _textSelectionEnd; + + _setSelectionPosition(null, isEnd: isEnd); + final Matrix4 transform = paragraph.getTransformTo(null); + transform.invert(); + final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition); + if (_rect.isEmpty) { + return SelectionUtils.getResultBasedOnRect(_rect, localPosition); + } + final Offset adjustedOffset = SelectionUtils.adjustDragOffset( + _rect, + localPosition, + direction: paragraph.textDirection, + ); + + final TextPosition position = paragraph.getPositionForOffset(adjustedOffset); + // Check if the original local position is within the rect, if it is not then + // we do not need to look up the word boundary for that position. This is to + // maintain a selectables selection collapsed at 0 when the local position is + // not located inside its rect. + final _WordBoundaryRecord? wordBoundary = !_rect.contains(localPosition) ? null : _getWordBoundaryAtPosition(position); + final TextPosition targetPosition = _clampTextPosition(isEnd ? _updateSelectionEndEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd) : _updateSelectionStartEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd)); + + _setSelectionPosition(targetPosition, isEnd: isEnd); + if (targetPosition.offset == range.end) { + return SelectionResult.next; + } + + if (targetPosition.offset == range.start) { + return SelectionResult.previous; + } + // TODO(chunhtai): The geometry information should not be used to determine + // selection result. This is a workaround to RenderParagraph, where it does + // not have a way to get accurate text length if its text is truncated due to + // layout constraint. + return SelectionUtils.getResultBasedOnRect(_rect, localPosition); + } + TextPosition _clampTextPosition(TextPosition position) { // Affinity of range.end is upstream. if (position.offset > range.end || @@ -1471,6 +1705,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM SelectionResult _handleClearSelection() { _textSelectionStart = null; _textSelectionEnd = null; + _selectableContainsOriginWord = false; return SelectionResult.none; } @@ -1481,20 +1716,29 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM } SelectionResult _handleSelectWord(Offset globalPosition) { + _selectableContainsOriginWord = true; + final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition)); - if (_positionIsWithinCurrentSelection(position)) { + if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) { return SelectionResult.end; } - final TextRange word = paragraph.getWordBoundary(position); - assert(word.isNormalized); - if (word.start < range.start && word.end < range.start) { + final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position); + if (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset < range.start) { return SelectionResult.previous; - } else if (word.start > range.end && word.end > range.end) { + } else if (wordBoundary.wordStart.offset > range.end && wordBoundary.wordEnd.offset > range.end) { return SelectionResult.next; } // Fragments are separated by placeholder span, the word boundary shouldn't // expand across fragments. - assert(word.start >= range.start && word.end <= range.end); + assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end); + _textSelectionStart = wordBoundary.wordStart; + _textSelectionEnd = wordBoundary.wordEnd; + return SelectionResult.end; + } + + _WordBoundaryRecord _getWordBoundaryAtPosition(TextPosition position) { + final TextRange word = paragraph.getWordBoundary(position); + assert(word.isNormalized); late TextPosition start; late TextPosition end; if (position.offset > word.end) { @@ -1503,9 +1747,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM start = TextPosition(offset: word.start); end = TextPosition(offset: word.end, affinity: TextAffinity.upstream); } - _textSelectionStart = start; - _textSelectionEnd = end; - return SelectionResult.end; + return (wordStart: start, wordEnd: end); } SelectionResult _handleDirectionallyExtendSelection(double horizontalBaseline, bool isExtent, SelectionExtendDirection movement) { diff --git a/packages/flutter/lib/src/rendering/performance_overlay.dart b/packages/flutter/lib/src/rendering/performance_overlay.dart index d48e21317c788..fbd4de8ada7eb 100644 --- a/packages/flutter/lib/src/rendering/performance_overlay.dart +++ b/packages/flutter/lib/src/rendering/performance_overlay.dart @@ -59,9 +59,6 @@ enum PerformanceOverlayOption { /// to true. class RenderPerformanceOverlay extends RenderBox { /// Creates a performance overlay render object. - /// - /// The [optionsMask], [rasterizerThreshold], [checkerboardRasterCacheImages], - /// and [checkerboardOffscreenLayers] arguments must not be null. RenderPerformanceOverlay({ int optionsMask = 0, int rasterizerThreshold = 0, diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index ca605597669b2..a04fbd8c6fccc 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -122,7 +122,7 @@ class RenderAndroidView extends PlatformViewRenderBox { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -269,41 +269,26 @@ class RenderAndroidView extends PlatformViewRenderBox { } } -/// A render object for an iOS UIKit UIView. -/// -/// [RenderUiKitView] is responsible for sizing and displaying an iOS -/// [UIView](https://developer.apple.com/documentation/uikit/uiview). -/// -/// UIViews are added as sub views of the FlutterView and are composited by Quartz. -/// -/// {@macro flutter.rendering.RenderAndroidView.layout} -/// -/// {@macro flutter.rendering.RenderAndroidView.gestures} +/// Common render-layer functionality for iOS and macOS platform views. /// -/// See also: -/// -/// * [UiKitView] which is a widget that is used to show a UIView. -/// * [PlatformViewsService] which is a service for controlling platform views. -class RenderUiKitView extends RenderBox { - /// Creates a render object for an iOS UIView. - /// - /// The `viewId`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null. - RenderUiKitView({ - required UiKitViewController viewController, +/// Provides the basic rendering logic for iOS and macOS platformviews. +/// Subclasses shall override handleEvent in order to execute custom event logic. +/// T represents the class of the view controller for the corresponding widget. +abstract class RenderDarwinPlatformView extends RenderBox { + /// Creates a render object for a platform view. + RenderDarwinPlatformView({ + required T viewController, required this.hitTestBehavior, - required Set> gestureRecognizers, + required Set> gestureRecognizers, }) : _viewController = viewController { updateGestureRecognizers(gestureRecognizers); } - /// The unique identifier of the UIView controlled by this controller. - /// - /// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView - /// must have been created by calling [PlatformViewsService.initUiKitView]. - UiKitViewController get viewController => _viewController; - UiKitViewController _viewController; - set viewController(UiKitViewController value) { + /// The unique identifier of the platform view controlled by this controller. + T get viewController => _viewController; + T _viewController; + set viewController(T value) { if (_viewController == value) { return; } @@ -320,20 +305,6 @@ class RenderUiKitView extends RenderBox { // any newly arriving events there's nothing we need to invalidate. PlatformViewHitTestBehavior hitTestBehavior; - /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} - void updateGestureRecognizers(Set> gestureRecognizers) { - assert( - _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, - 'There were multiple gesture recognizer factories for the same type, there must only be a single ' - 'gesture recognizer factory for each gesture recognizer type.', - ); - if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { - return; - } - _gestureRecognizer?.dispose(); - _gestureRecognizer = _UiKitViewGestureRecognizer(viewController, gestureRecognizers); - } - @override bool get sizedByParent => true; @@ -343,10 +314,10 @@ class RenderUiKitView extends RenderBox { @override bool get isRepaintBoundary => true; - _UiKitViewGestureRecognizer? _gestureRecognizer; - PointerEvent? _lastPointerDownEvent; + _UiKitViewGestureRecognizer? _gestureRecognizer; + @override Size computeDryLayout(BoxConstraints constraints) { return constraints.biggest; @@ -372,15 +343,6 @@ class RenderUiKitView extends RenderBox { @override bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; - @override - void handleEvent(PointerEvent event, HitTestEntry entry) { - if (event is! PointerDownEvent) { - return; - } - _gestureRecognizer!.addPointer(event); - _lastPointerDownEvent = event.original ?? event; - } - // This is registered as a global PointerRoute while the render object is attached. void _handleGlobalPointerEvent(PointerEvent event) { if (event is! PointerDownEvent) { @@ -415,11 +377,88 @@ class RenderUiKitView extends RenderBox { @override void detach() { GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent); + super.detach(); + } + + /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} + void updateGestureRecognizers(Set> gestureRecognizers); +} + +/// A render object for an iOS UIKit UIView. +/// +/// [RenderUiKitView] is responsible for sizing and displaying an iOS +/// [UIView](https://developer.apple.com/documentation/uikit/uiview). +/// +/// UIViews are added as subviews of the FlutterView and are composited by Quartz. +/// +/// The viewController is typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView +/// must have been created by calling [PlatformViewsService.initUiKitView]. +/// +/// {@macro flutter.rendering.RenderAndroidView.layout} +/// +/// {@macro flutter.rendering.RenderAndroidView.gestures} +/// +/// See also: +/// +/// * [UiKitView], which is a widget that is used to show a UIView. +/// * [PlatformViewsService], which is a service for controlling platform views. +class RenderUiKitView extends RenderDarwinPlatformView { + /// Creates a render object for an iOS UIView. + RenderUiKitView({ + required super.viewController, + required super.hitTestBehavior, + required super.gestureRecognizers, + }); + + /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} + @override + void updateGestureRecognizers(Set> gestureRecognizers) { + assert( + _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, + 'There were multiple gesture recognizer factories for the same type, there must only be a single ' + 'gesture recognizer factory for each gesture recognizer type.', + ); + if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { + return; + } + _gestureRecognizer?.dispose(); + _gestureRecognizer = _UiKitViewGestureRecognizer(viewController, gestureRecognizers); + } + + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + if (event is! PointerDownEvent) { + return; + } + _gestureRecognizer!.addPointer(event); + _lastPointerDownEvent = event.original ?? event; + } + + @override + void detach() { _gestureRecognizer!.reset(); super.detach(); } } +/// A render object for a macOS platform view. +class RenderAppKitView extends RenderDarwinPlatformView { + /// Creates a render object for a macOS AppKitView. + RenderAppKitView({ + required super.viewController, + required super.hitTestBehavior, + required super.gestureRecognizers, + }); + + // TODO(schectman): Add gesture functionality to macOS platform view when implemented. + // https://github.com/flutter/flutter/issues/128519 + // This method will need to behave the same as the same-named method for RenderUiKitView, + // but use a _AppKitViewGestureRecognizer or equivalent, whose constructor shall accept an + // AppKitViewController. + @override + void updateGestureRecognizers(Set> gestureRecognizers) {} +} + // This recognizer constructs gesture recognizers from a set of gesture recognizer factories // it was give, adds all of them to a gesture arena team with the _UiKitViewGestureRecognizer // as the team captain. @@ -614,8 +653,6 @@ class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer { /// integrates it with the gesture arenas system and adds relevant semantic nodes to the semantics tree. class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { /// Creating a render object for a [PlatformViewSurface]. - /// - /// The `controller` parameter must not be null. PlatformViewRenderBox({ required PlatformViewController controller, required PlatformViewHitTestBehavior hitTestBehavior, @@ -629,7 +666,7 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { /// The controller for this render object. PlatformViewController get controller => _controller; PlatformViewController _controller; - /// This value must not be null, and setting it to a new value will result in a repaint. + /// Setting this value to a new value will result in a repaint. set controller(covariant PlatformViewController controller) { assert(controller.viewId > -1); diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index a089fbe145204..bf80a69330b27 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -200,7 +200,7 @@ abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox { class RenderConstrainedBox extends RenderProxyBox { /// Creates a render box that constrains its child. /// - /// The [additionalConstraints] argument must not be null and must be valid. + /// The [additionalConstraints] argument must be valid. RenderConstrainedBox({ RenderBox? child, required BoxConstraints additionalConstraints, @@ -842,6 +842,19 @@ class RenderIntrinsicHeight extends RenderProxyBox { } } +/// Excludes the child from baseline computations in the parent. +class RenderIgnoreBaseline extends RenderProxyBox { + /// Create a render object that causes the parent to ignore the child for baseline computations. + RenderIgnoreBaseline({ + RenderBox? child, + }) : super(child); + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return null; + } +} + /// Makes its child partially transparent. /// /// This class paints its child into an intermediate buffer and then blends the @@ -878,8 +891,6 @@ class RenderOpacity extends RenderProxyBox { /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent /// (i.e., invisible). /// - /// The opacity must not be null. - /// /// Values 1.0 and 0.0 are painted with a fast path. Other values /// require painting the child into an intermediate buffer, which is /// expensive. @@ -1081,8 +1092,6 @@ mixin RenderAnimatedOpacityMixin on RenderObjectWithChil /// than a [double] to control the opacity. class RenderAnimatedOpacity extends RenderProxyBox with RenderAnimatedOpacityMixin { /// Creates a partially transparent render object. - /// - /// The [opacity] argument must not be null. RenderAnimatedOpacity({ required Animation opacity, bool alwaysIncludeSemantics = false, @@ -1104,8 +1113,6 @@ typedef ShaderCallback = Shader Function(Rect bounds); /// of a child by using a [ui.Gradient.linear] mask. class RenderShaderMask extends RenderProxyBox { /// Creates a render object that applies a mask generated by a [Shader] to its child. - /// - /// The [shaderCallback] and [blendMode] arguments must not be null. RenderShaderMask({ RenderBox? child, required ShaderCallback shaderCallback, @@ -1179,10 +1186,8 @@ class RenderShaderMask extends RenderProxyBox { /// such as a blur. class RenderBackdropFilter extends RenderProxyBox { /// Creates a backdrop filter. - /// - /// The [filter] argument must not be null. - /// The [blendMode] argument, if provided, must not be null - /// and will default to [BlendMode.srcOver]. + // + /// The [blendMode] argument defaults to [BlendMode.srcOver]. RenderBackdropFilter({ RenderBox? child, required ui.ImageFilter filter, BlendMode blendMode = BlendMode.srcOver }) : _filter = filter, _blendMode = blendMode, @@ -1328,8 +1333,6 @@ abstract class CustomClipper extends Listenable { class ShapeBorderClipper extends CustomClipper { /// Creates a [ShapeBorder] clipper. /// - /// The [shape] argument must not be null. - /// /// The [textDirection] argument must be provided non-null if [shape] /// has a text direction dependency (for example if it is expressed in terms /// of "start" and "end" instead of "left" and "right"). It may be null if @@ -1499,8 +1502,7 @@ class RenderClipRect extends _RenderCustomClip { /// If [clipper] is null, the clip will match the layout size and position of /// the child. /// - /// The [clipBehavior] must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. RenderClipRect({ super.child, super.clipper, @@ -1572,8 +1574,7 @@ class RenderClipRRect extends _RenderCustomClip { /// /// If [clipper] is non-null, then [borderRadius] is ignored. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. RenderClipRRect({ super.child, BorderRadiusGeometry borderRadius = BorderRadius.zero, @@ -1674,8 +1675,7 @@ class RenderClipOval extends _RenderCustomClip { /// If [clipper] is null, the oval will be inscribed into the layout size and /// position of the child. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. RenderClipOval({ super.child, super.clipper, @@ -1770,8 +1770,7 @@ class RenderClipPath extends _RenderCustomClip { /// consider using a [RenderClipRect], which can achieve the same effect more /// efficiently. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. RenderClipPath({ super.child, super.clipper, @@ -1836,8 +1835,7 @@ class RenderClipPath extends _RenderCustomClip { /// The concrete implementations [RenderPhysicalModel] and [RenderPhysicalShape] /// determine the actual shape of the physical model. abstract class _RenderPhysicalModelBase extends _RenderCustomClip { - /// The [shape], [elevation], [color], and [shadowColor] must not be null. - /// Additionally, the [elevation] must be non-negative. + /// The [elevation] parameter must be non-negative. _RenderPhysicalModelBase({ required super.child, required double elevation, @@ -1908,8 +1906,6 @@ abstract class _RenderPhysicalModelBase extends _RenderCustomClip { } } -final Paint _transparentPaint = Paint()..color = const Color(0x00000000); - /// Creates a physical model layer that clips its child to a rounded /// rectangle. /// @@ -1919,9 +1915,7 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase { /// /// The [color] is required. /// - /// The [shape], [elevation], [color], [clipBehavior], and [shadowColor] - /// arguments must not be null. Additionally, the [elevation] must be - /// non-negative. + /// The [elevation] parameter must be non-negative. RenderPhysicalModel({ super.child, BoxShape shape = BoxShape.rectangle, @@ -2079,8 +2073,7 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase { /// /// The [color] and [clipper] parameters are required. /// - /// The [clipper], [elevation], [color] and [shadowColor] must not be null. - /// Additionally, the [elevation] must be non-negative. + /// The [elevation] parameter must be non-negative. RenderPhysicalShape({ super.child, required CustomClipper super.clipper, @@ -2113,7 +2106,6 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase { } _updateClip(); - final Rect offsetBounds = offset & size; final Path offsetPath = _clip!.shift(offset); bool paintShadows = true; assert(() { @@ -2134,14 +2126,6 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase { final Canvas canvas = context.canvas; if (elevation != 0.0 && paintShadows) { - // The drawShadow call doesn't add the region of the shadow to the - // picture's bounds, so we draw a hardcoded amount of extra space to - // account for the maximum potential area of the shadow. - // TODO(jsimmons): remove this when Skia does it for us. - canvas.drawRect( - offsetBounds.inflate(20.0), - _transparentPaint, - ); canvas.drawShadow( offsetPath, shadowColor, @@ -2328,8 +2312,6 @@ class RenderDecoratedBox extends RenderProxyBox { /// Applies a transformation before painting its child. class RenderTransform extends RenderProxyBox { /// Creates a render object that transforms its child. - /// - /// The [transform] argument must not be null. RenderTransform({ required Matrix4 transform, Offset? origin, @@ -2596,8 +2578,6 @@ class RenderTransform extends RenderProxyBox { /// Scales and positions its child within itself according to [fit]. class RenderFittedBox extends RenderProxyBox { /// Scales and positions its child within itself. - /// - /// The [fit] and [alignment] arguments must not be null. RenderFittedBox({ BoxFit fit = BoxFit.contain, AlignmentGeometry alignment = Alignment.center, @@ -2760,7 +2740,7 @@ class RenderFittedBox extends RenderProxyBox { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.none; set clipBehavior(Clip value) { @@ -2889,8 +2869,6 @@ class RenderFittedBox extends RenderProxyBox { /// they overflow. class RenderFractionalTranslation extends RenderProxyBox { /// Creates a render object that translates its child's painting. - /// - /// The [translation] argument must not be null. RenderFractionalTranslation({ required Offset translation, this.transformHitTests = true, @@ -3152,8 +3130,7 @@ class RenderMouseRegion extends RenderProxyBoxWithHitTestBehavior implements Mou /// Creates a render object that forwards pointer events to callbacks. /// /// All parameters are optional. By default this method creates an opaque - /// mouse region with no callbacks and cursor being [MouseCursor.defer]. The - /// [cursor] must not be null. + /// mouse region with no callbacks and cursor being [MouseCursor.defer]. RenderMouseRegion({ this.onEnter, this.onHover, @@ -3545,7 +3522,14 @@ class RenderRepaintBoundary extends RenderProxyBox { /// as usual. It just cannot be the target of located events, because its render /// object returns false from [hitTest]. /// -/// {@macro flutter.widgets.IgnorePointer.Semantics} +/// ## Semantics +/// +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@macro flutter.widgets.IgnorePointer.semantics} +/// +/// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} /// /// See also: /// @@ -3553,8 +3537,6 @@ class RenderRepaintBoundary extends RenderProxyBox { /// nodes in the subtree from seeing them. class RenderIgnorePointer extends RenderProxyBox { /// Creates a render object that is invisible to hit testing. - /// - /// The [ignoring] argument must not be null. RenderIgnorePointer({ RenderBox? child, bool ignoring = true, @@ -3572,7 +3554,7 @@ class RenderIgnorePointer extends RenderProxyBox { /// Regardless of whether this render object is ignored during hit testing, it /// will still consume space during layout and be visible during painting. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.semantics} bool get ignoring => _ignoring; bool _ignoring; set ignoring(bool value) { @@ -3587,7 +3569,7 @@ class RenderIgnorePointer extends RenderProxyBox { /// Whether the semantics of this render object is ignored when compiling the semantics tree. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} /// /// See [SemanticsNode] for additional information about the semantics tree. @Deprecated( @@ -3788,7 +3770,14 @@ class RenderOffstage extends RenderProxyBox { /// its children from being the target of located events, because its render /// object returns true from [hitTest]. /// -/// {@macro flutter.widgets.AbsorbPointer.Semantics} +/// ## Semantics +/// +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@macro flutter.widgets.AbsorbPointer.semantics} +/// +/// {@macro flutter.widgets.AbsorbPointer.ignoringSemantics} /// /// See also: /// @@ -3796,8 +3785,6 @@ class RenderOffstage extends RenderProxyBox { /// subtree from considering entirely for the purposes of hit testing. class RenderAbsorbPointer extends RenderProxyBox { /// Creates a render object that absorbs pointers during hit testing. - /// - /// The [absorbing] argument must not be null. RenderAbsorbPointer({ RenderBox? child, bool absorbing = true, @@ -3816,7 +3803,7 @@ class RenderAbsorbPointer extends RenderProxyBox { /// testing, it will still consume space during layout and be visible during /// painting. /// - /// {@macro flutter.widgets.AbsorbPointer.Semantics} + /// {@macro flutter.widgets.AbsorbPointer.semantics} bool get absorbing => _absorbing; bool _absorbing; set absorbing(bool value) { @@ -3832,7 +3819,7 @@ class RenderAbsorbPointer extends RenderProxyBox { /// Whether the semantics of this render object is ignored when compiling the /// semantics tree. /// - /// {@macro flutter.widgets.AbsorbPointer.Semantics} + /// {@macro flutter.widgets.AbsorbPointer.ignoringSemantics} /// /// See [SemanticsNode] for additional information about the semantics tree. @Deprecated( @@ -3916,8 +3903,6 @@ class RenderMetaData extends RenderProxyBoxWithHitTestBehavior { /// an accessibility tool). class RenderSemanticsGestureHandler extends RenderProxyBoxWithHitTestBehavior { /// Creates a render object that listens for specific semantic gestures. - /// - /// The [scrollFactor] and [behavior] arguments must not be null. RenderSemanticsGestureHandler({ super.child, GestureTapCallback? onTap, @@ -4109,8 +4094,6 @@ class RenderSemanticsGestureHandler extends RenderProxyBoxWithHitTestBehavior { class RenderSemanticsAnnotations extends RenderProxyBox { /// Creates a render object that attaches a semantic annotation. /// - /// The [container] argument must not be null. - /// /// If the [SemanticsProperties.attributedLabel] is not null, the [textDirection] must also not be null. RenderSemanticsAnnotations({ RenderBox? child, @@ -4317,6 +4300,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { if (_properties.button != null) { config.isButton = _properties.button!; } + if (_properties.expanded != null) { + config.isExpanded = _properties.expanded; + } if (_properties.link != null) { config.isLink = _properties.link!; } @@ -4700,8 +4686,6 @@ class RenderIndexedSemantics extends RenderProxyBox { /// * [LeaderLayer], the layer that this render object creates. class RenderLeaderLayer extends RenderProxyBox { /// Creates a render object that uses a [LeaderLayer]. - /// - /// The [link] must not be null. RenderLeaderLayer({ required LayerLink link, RenderBox? child, @@ -4711,8 +4695,8 @@ class RenderLeaderLayer extends RenderProxyBox { /// The link object that connects this [RenderLeaderLayer] with one or more /// [RenderFollowerLayer]s. /// - /// This property must not be null. The object must not be associated with - /// another [RenderLeaderLayer] that is also being painted. + /// The object must not be associated with another [RenderLeaderLayer] that is + /// also being painted. LayerLink get link => _link; LayerLink _link; set link(LayerLink value) { @@ -4781,8 +4765,6 @@ class RenderLeaderLayer extends RenderProxyBox { /// * [FollowerLayer], the layer that this render object creates. class RenderFollowerLayer extends RenderProxyBox { /// Creates a render object that uses a [FollowerLayer]. - /// - /// The [link] and [offset] arguments must not be null. RenderFollowerLayer({ required LayerLink link, bool showWhenUnlinked = true, diff --git a/packages/flutter/lib/src/rendering/proxy_sliver.dart b/packages/flutter/lib/src/rendering/proxy_sliver.dart index bd7feec4cd4d3..a33ee947cb8d7 100644 --- a/packages/flutter/lib/src/rendering/proxy_sliver.dart +++ b/packages/flutter/lib/src/rendering/proxy_sliver.dart @@ -118,14 +118,11 @@ class RenderSliverOpacity extends RenderProxySliver { /// The fraction to scale the child's alpha value. /// - /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent + /// An opacity of one is fully opaque. An opacity of zero is fully transparent /// (i.e. invisible). /// - /// The opacity must not be null. - /// - /// Values 1.0 and 0.0 are painted with a fast path. Other values - /// require painting the child into an intermediate buffer, which is - /// expensive. + /// Values one and zero are painted with a fast path. Other values require + /// painting the child into an intermediate buffer, which is expensive. double get opacity => _opacity; double _opacity; set opacity(double value) { @@ -205,11 +202,16 @@ class RenderSliverOpacity extends RenderProxySliver { /// child as usual. It just cannot be the target of located events, because its /// render object returns false from [hitTest]. /// -/// {@macro flutter.widgets.IgnorePointer.Semantics} +/// ## Semantics +/// +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@macro flutter.widgets.IgnorePointer.semantics} +/// +/// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} class RenderSliverIgnorePointer extends RenderProxySliver { /// Creates a render object that is invisible to hit testing. - /// - /// The [ignoring] argument must not be null. RenderSliverIgnorePointer({ RenderSliver? sliver, bool ignoring = true, @@ -228,7 +230,7 @@ class RenderSliverIgnorePointer extends RenderProxySliver { /// Regardless of whether this render object is ignored during hit testing, it /// will still consume space during layout and be visible during painting. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.semantics} bool get ignoring => _ignoring; bool _ignoring; set ignoring(bool value) { @@ -244,7 +246,7 @@ class RenderSliverIgnorePointer extends RenderProxySliver { /// Whether the semantics of this render object is ignored when compiling the /// semantics tree. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} @Deprecated( 'Create a custom sliver ignore pointer widget instead. ' 'This feature was deprecated after v3.8.0-12.0.pre.' @@ -403,8 +405,6 @@ class RenderSliverOffstage extends RenderProxySliver { /// rather than a [double] to control the opacity. class RenderSliverAnimatedOpacity extends RenderProxySliver with RenderAnimatedOpacityMixin { /// Creates a partially transparent render object. - /// - /// The [opacity] argument must not be null. RenderSliverAnimatedOpacity({ required Animation opacity, bool alwaysIncludeSemantics = false, @@ -424,7 +424,7 @@ class RenderSliverAnimatedOpacity extends RenderProxySliver with RenderAnimatedO class RenderSliverConstrainedCrossAxis extends RenderProxySliver { /// Creates a render object that constrains the cross axis extent of its sliver child. /// - /// The [maxExtent] parameter must not be null and must be nonnegative. + /// The [maxExtent] parameter must be nonnegative. RenderSliverConstrainedCrossAxis({ required double maxExtent }) : _maxExtent = maxExtent, diff --git a/packages/flutter/lib/src/rendering/rotated_box.dart b/packages/flutter/lib/src/rendering/rotated_box.dart index 03cb105acfd71..89cf6aafc46ac 100644 --- a/packages/flutter/lib/src/rendering/rotated_box.dart +++ b/packages/flutter/lib/src/rendering/rotated_box.dart @@ -19,8 +19,6 @@ const double _kQuarterTurnsInRadians = math.pi / 2.0; /// rotated box consumes only as much space as required by the rotated child. class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin { /// Creates a rotated render box. - /// - /// The [quarterTurns] argument must not be null. RenderRotatedBox({ required int quarterTurns, RenderBox? child, diff --git a/packages/flutter/lib/src/rendering/selection.dart b/packages/flutter/lib/src/rendering/selection.dart index beaf5a02700cf..d5434f26f9d68 100644 --- a/packages/flutter/lib/src/rendering/selection.dart +++ b/packages/flutter/lib/src/rendering/selection.dart @@ -375,26 +375,44 @@ class SelectWordSelectionEvent extends SelectionEvent { /// /// The [globalPosition] contains the new offset of the edge. /// -/// This event is dispatched when the framework detects [DragStartDetails] in +/// The [granularity] contains the granularity that the selection edge should move by. +/// Only [TextGranularity.character] and [TextGranularity.word] are currently supported. +/// +/// This event is dispatched when the framework detects [TapDragStartDetails] in /// [SelectionArea]'s gesture recognizers for mouse devices, or the selection /// handles have been dragged to new locations. class SelectionEdgeUpdateEvent extends SelectionEvent { /// Creates a selection start edge update event. /// /// The [globalPosition] contains the location of the selection start edge. + /// + /// The [granularity] contains the granularity which the selection edge should move by. + /// This value defaults to [TextGranularity.character]. const SelectionEdgeUpdateEvent.forStart({ - required this.globalPosition - }) : super._(SelectionEventType.startEdgeUpdate); + required this.globalPosition, + TextGranularity? granularity + }) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.startEdgeUpdate); /// Creates a selection end edge update event. /// /// The [globalPosition] contains the new location of the selection end edge. + /// + /// The [granularity] contains the granularity which the selection edge should move by. + /// This value defaults to [TextGranularity.character]. const SelectionEdgeUpdateEvent.forEnd({ - required this.globalPosition - }) : super._(SelectionEventType.endEdgeUpdate); + required this.globalPosition, + TextGranularity? granularity + }) : granularity = granularity ?? TextGranularity.character, super._(SelectionEventType.endEdgeUpdate); /// The new location of the selection edge. final Offset globalPosition; + + /// The granularity for which the selection moves. + /// + /// Only [TextGranularity.character] and [TextGranularity.word] are currently supported. + /// + /// Defaults to [TextGranularity.character]. + final TextGranularity granularity; } /// Extends the start or end of the selection by a given [TextGranularity]. @@ -403,8 +421,6 @@ class SelectionEdgeUpdateEvent extends SelectionEvent { /// [isEnd], according to the [granularity]. class GranularlyExtendSelectionEvent extends SelectionEvent { /// Creates a [GranularlyExtendSelectionEvent]. - /// - /// All parameters are required and must not be null. const GranularlyExtendSelectionEvent({ required this.forward, required this.isEnd, @@ -481,8 +497,6 @@ enum SelectionExtendDirection { /// move to when moving to across lines. class DirectionallyExtendSelectionEvent extends SelectionEvent { /// Creates a [DirectionallyExtendSelectionEvent]. - /// - /// All parameters are required and must not be null. const DirectionallyExtendSelectionEvent({ required this.dx, required this.isEnd, @@ -686,10 +700,8 @@ class SelectionGeometry { /// The geometry information of a selection point. @immutable -class SelectionPoint { +class SelectionPoint with Diagnosticable { /// Creates a selection point object. - /// - /// All properties must not be null. const SelectionPoint({ required this.localPosition, required this.lineHeight, @@ -730,6 +742,14 @@ class SelectionPoint { handleType, ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('localPosition', localPosition)); + properties.add(DoubleProperty('lineHeight', lineHeight)); + properties.add(EnumProperty('handleType', handleType)); + } } /// The type of selection handle to be displayed. diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index c0e21a66d8d08..2afe15c064ad5 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -102,7 +102,7 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi class RenderPadding extends RenderShiftedBox { /// Creates a render object that insets its child. /// - /// The [padding] argument must not be null and must have non-negative insets. + /// The [padding] argument must have non-negative insets. RenderPadding({ required EdgeInsetsGeometry padding, TextDirection? textDirection, @@ -267,8 +267,6 @@ class RenderPadding extends RenderShiftedBox { abstract class RenderAligningShiftedBox extends RenderShiftedBox { /// Initializes member variables for subclasses. /// - /// The [alignment] argument must not be null. - /// /// The [textDirection] must be non-null if the [alignment] is /// direction-sensitive. RenderAligningShiftedBox({ @@ -308,8 +306,6 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { AlignmentGeometry get alignment => _alignment; AlignmentGeometry _alignment; /// Sets the alignment to a new value, and triggers a layout update. - /// - /// The new alignment must not be null. set alignment(AlignmentGeometry value) { if (_alignment == value) { return; @@ -685,8 +681,6 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin { /// Creates a [RenderBox] that sizes itself to the child and modifies the /// [constraints] before passing it down to that child. - /// - /// The [alignment] and [clipBehavior] must not be null. RenderConstraintsTransformBox({ required super.alignment, required super.textDirection, @@ -877,8 +871,6 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO class RenderSizedOverflowBox extends RenderAligningShiftedBox { /// Creates a render box of a given size that lets its child overflow. /// - /// The [requestedSize] and [alignment] arguments must not be null. - /// /// The [textDirection] argument must not be null if the [alignment] is /// direction-sensitive. RenderSizedOverflowBox({ @@ -959,8 +951,6 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { /// If non-null, the [widthFactor] and [heightFactor] arguments must be /// non-negative. /// - /// The [alignment] must not be null. - /// /// The [textDirection] must be non-null if the [alignment] is /// direction-sensitive. RenderFractionallySizedOverflowBox({ @@ -1313,8 +1303,6 @@ class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { /// and the bottom of the box. class RenderBaseline extends RenderShiftedBox { /// Creates a [RenderBaseline] object. - /// - /// The [baseline] and [baselineType] arguments must not be null. RenderBaseline({ RenderBox? child, required double baseline, diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index 7ccc6eaa19d71..8c197447ed21e 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -16,6 +16,71 @@ import 'viewport_offset.dart'; // CORE TYPES FOR SLIVERS // The RenderSliver base class and its helper types. +/// Called to get the item extent by the index of item. +/// +/// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder]. +typedef ItemExtentBuilder = double Function(int index, SliverLayoutDimensions dimensions); + +/// Relates the dimensions of the [RenderSliver] during layout. +/// +/// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder]. +@immutable +class SliverLayoutDimensions { + /// Constructs a [SliverLayoutDimensions] with the specified parameters. + const SliverLayoutDimensions({ + required this.scrollOffset, + required this.precedingScrollExtent, + required this.viewportMainAxisExtent, + required this.crossAxisExtent + }); + + /// {@macro flutter.rendering.SliverConstraints.scrollOffset} + final double scrollOffset; + + /// {@macro flutter.rendering.SliverConstraints.precedingScrollExtent} + final double precedingScrollExtent; + + /// The number of pixels the viewport can display in the main axis. + /// + /// For a vertical list, this is the height of the viewport. + final double viewportMainAxisExtent; + + /// The number of pixels in the cross-axis. + /// + /// For a vertical list, this is the width of the sliver. + final double crossAxisExtent; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! SliverLayoutDimensions) { + return false; + } + return other.scrollOffset == scrollOffset && + other.precedingScrollExtent == precedingScrollExtent && + other.viewportMainAxisExtent == viewportMainAxisExtent && + other.crossAxisExtent == crossAxisExtent; + } + + @override + String toString() { + return 'scrollOffset: $scrollOffset' + ' precedingScrollExtent: $precedingScrollExtent' + ' viewportMainAxisExtent: $viewportMainAxisExtent' + ' crossAxisExtent: $crossAxisExtent'; + } + + @override + int get hashCode => Object.hash( + scrollOffset, + precedingScrollExtent, + viewportMainAxisExtent, + viewportMainAxisExtent + ); +} + /// The direction in which a sliver's contents are ordered, relative to the /// scroll offset axis. /// @@ -118,8 +183,6 @@ ScrollDirection applyGrowthDirectionToScrollDirection(ScrollDirection scrollDire /// offset. class SliverConstraints extends Constraints { /// Creates sliver constraints with the given information. - /// - /// All of the argument must not be null. const SliverConstraints({ required this.axisDirection, required this.growthDirection, @@ -224,12 +287,13 @@ class SliverConstraints extends Constraints { /// {@macro flutter.rendering.ScrollDirection.sample} final ScrollDirection userScrollDirection; + /// {@template flutter.rendering.SliverConstraints.scrollOffset} /// The scroll offset, in this sliver's coordinate system, that corresponds to /// the earliest visible part of this sliver in the [AxisDirection] if - /// [growthDirection] is [GrowthDirection.forward] or in the opposite - /// [AxisDirection] direction if [growthDirection] is [GrowthDirection.reverse]. + /// [SliverConstraints.growthDirection] is [GrowthDirection.forward] or in the opposite + /// [AxisDirection] direction if [SliverConstraints.growthDirection] is [GrowthDirection.reverse]. /// - /// For example, if [AxisDirection] is [AxisDirection.down] and [growthDirection] + /// For example, if [AxisDirection] is [AxisDirection.down] and [SliverConstraints.growthDirection] /// is [GrowthDirection.forward], then scroll offset is the amount the top of /// the sliver has been scrolled past the top of the viewport. /// @@ -240,7 +304,7 @@ class SliverConstraints extends Constraints { /// /// For slivers whose top is not past the top of the viewport, the /// [scrollOffset] is `0` when [AxisDirection] is [AxisDirection.down] and - /// [growthDirection] is [GrowthDirection.forward]. The set of slivers with + /// [SliverConstraints.growthDirection] is [GrowthDirection.forward]. The set of slivers with /// [scrollOffset] `0` includes all the slivers that are below the bottom of the /// viewport. /// @@ -249,9 +313,11 @@ class SliverConstraints extends Constraints { /// partially 'protrude in' from the bottom of the viewport. /// /// Whether this corresponds to the beginning or the end of the sliver's - /// contents depends on the [growthDirection]. + /// contents depends on the [SliverConstraints.growthDirection]. + /// {@endtemplate} final double scrollOffset; + /// {@template flutter.rendering.SliverConstraints.precedingScrollExtent} /// The scroll distance that has been consumed by all [RenderSliver]s that /// came before this [RenderSliver]. /// @@ -273,6 +339,7 @@ class SliverConstraints extends Constraints { /// content forever without reaching the end. For any [RenderSliver]s that /// appear after the infinite [RenderSliver], the [precedingScrollExtent] will /// be [double.infinity]. + /// {@endtemplate} final double precedingScrollExtent; /// The number of pixels from where the pixels corresponding to the @@ -551,8 +618,6 @@ class SliverGeometry with Diagnosticable { /// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent] /// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to /// whether [paintExtent] is greater than zero. - /// - /// The other arguments must not be null. const SliverGeometry({ this.scrollExtent = 0.0, this.paintExtent = 0.0, @@ -928,8 +993,6 @@ class SliverHitTestResult extends HitTestResult { /// [AxisDirection] of the target sliver. class SliverHitTestEntry extends HitTestEntry { /// Creates a sliver hit test entry. - /// - /// The [mainAxisPosition] and [crossAxisPosition] arguments must not be null. SliverHitTestEntry( super.target, { required this.mainAxisPosition, diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart index 29000545a5a11..b5f2763ca0778 100644 --- a/packages/flutter/lib/src/rendering/sliver_fill.dart +++ b/packages/flutter/lib/src/rendering/sliver_fill.dart @@ -27,8 +27,6 @@ import 'sliver_fixed_extent_list.dart'; class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor { /// Creates a sliver that contains multiple box children that each fill the /// viewport. - /// - /// The [childManager] argument must not be null. RenderSliverFillViewport({ required super.childManager, double viewportFraction = 1.0, diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart index 4fe59a70264df..5b775f779abbe 100644 --- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart @@ -10,16 +10,17 @@ import 'box.dart'; import 'sliver.dart'; import 'sliver_multi_box_adaptor.dart'; -/// A sliver that contains multiple box children that have the same extent in +/// A sliver that contains multiple box children that have the explicit extent in /// the main axis. /// /// [RenderSliverFixedExtentBoxAdaptor] places its children in a linear array -/// along the main axis. Each child is forced to have the [itemExtent] in the -/// main axis and the [SliverConstraints.crossAxisExtent] in the cross axis. +/// along the main axis. Each child is forced to have the returned value of [itemExtentBuilder] +/// when the [itemExtentBuilder] is non-null or the [itemExtent] when [itemExtentBuilder] +/// is null in the main axis and the [SliverConstraints.crossAxisExtent] in the cross axis. /// -/// Subclasses should override [itemExtent] to control the size of the children -/// in the main axis. For a concrete subclass with a configurable [itemExtent], -/// see [RenderSliverFixedExtentList]. +/// Subclasses should override [itemExtent] or [itemExtentBuilder] to control +/// the size of the children in the main axis. For a concrete subclass with a +/// configurable [itemExtent], see [RenderSliverFixedExtentList] or [RenderSliverVariedExtentList]. /// /// [RenderSliverFixedExtentBoxAdaptor] is more efficient than /// [RenderSliverList] because [RenderSliverFixedExtentBoxAdaptor] does not need @@ -37,63 +38,88 @@ import 'sliver_multi_box_adaptor.dart'; abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { /// Creates a sliver that contains multiple box children that have the same /// extent in the main axis. - /// - /// The [childManager] argument must not be null. RenderSliverFixedExtentBoxAdaptor({ required super.childManager, }); /// The main-axis extent of each item. - double get itemExtent; + /// + /// If this is non-null, the [itemExtentBuilder] must be null. + /// If this is null, the [itemExtentBuilder] must be non-null. + double? get itemExtent; + + /// The main-axis extent builder of each item. + /// + /// If this is non-null, the [itemExtent] must be null. + /// If this is null, the [itemExtent] must be non-null. + ItemExtentBuilder? get itemExtentBuilder => null; /// The layout offset for the child with the given index. /// - /// This function is given the [itemExtent] as an argument to avoid - /// recomputing [itemExtent] repeatedly during layout. + /// This function uses the returned value of [itemExtentBuilder] or the [itemExtent] + /// as an argument to avoid recomputing item size repeatedly during layout. /// /// By default, places the children in order, without gaps, starting from /// layout offset zero. @protected - double indexToLayoutOffset(double itemExtent, int index) => itemExtent * index; + double indexToLayoutOffset(double itemExtent, int index) { + if (itemExtentBuilder == null) { + return itemExtent * index; + } else { + double offset = 0.0; + for (int i = 0; i < index; i++) { + offset += itemExtentBuilder!(i, _currentLayoutDimensions); + } + return offset; + } + } /// The minimum child index that is visible at the given scroll offset. /// - /// This function is given the [itemExtent] as an argument to avoid - /// recomputing [itemExtent] repeatedly during layout. + /// This function uses the returned value of [itemExtentBuilder] or the [itemExtent] + /// as an argument to avoid recomputing item size repeatedly during layout. /// /// By default, returns a value consistent with the children being placed in /// order, without gaps, starting from layout offset zero. @protected int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - if (itemExtent > 0.0) { - final double actual = scrollOffset / itemExtent; - final int round = actual.round(); - if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { - return round; + if (itemExtentBuilder == null) { + if (itemExtent > 0.0) { + final double actual = scrollOffset / itemExtent; + final int round = actual.round(); + if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { + return round; + } + return actual.floor(); } - return actual.floor(); + return 0; + } else { + return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!); } - return 0; } /// The maximum child index that is visible at the given scroll offset. /// - /// This function is given the [itemExtent] as an argument to avoid - /// recomputing [itemExtent] repeatedly during layout. + /// This function uses the returned value of [itemExtentBuilder] or the [itemExtent] + /// as an argument to avoid recomputing item size repeatedly during layout. /// /// By default, returns a value consistent with the children being placed in /// order, without gaps, starting from layout offset zero. @protected int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - if (itemExtent > 0.0) { - final double actual = scrollOffset / itemExtent - 1; - final int round = actual.round(); - if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { - return math.max(0, round); + if (itemExtentBuilder == null) { + if (itemExtent > 0.0) { + final double actual = scrollOffset / itemExtent - 1; + final int round = actual.round(); + if ((actual * itemExtent - round * itemExtent).abs() < precisionErrorTolerance) { + return math.max(0, round); + } + return math.max(0, actual.ceil()); } - return math.max(0, actual.ceil()); + return 0; + } else { + return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!); } - return 0; } /// Called to estimate the total scrollable extents of this object. @@ -138,8 +164,10 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda /// [childManager] returns an infinite number of children for positive /// indices. /// - /// By default, multiplies the [itemExtent] by the number of children reported - /// by [RenderSliverBoxChildManager.childCount]. + /// If [itemExtentBuilder] is null, multiplies the [itemExtent] by the number + /// of children reported by [RenderSliverBoxChildManager.childCount]. + /// If [itemExtentBuilder] is non-null, sum the extents of the first + /// [RenderSliverBoxChildManager.childCount] children. /// /// See also: /// @@ -147,7 +175,15 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda /// values. @protected double computeMaxScrollOffset(SliverConstraints constraints, double itemExtent) { - return childManager.childCount * itemExtent; + if (itemExtentBuilder == null) { + return childManager.childCount * itemExtent; + } else { + double offset = 0.0; + for (int i = 0; i < childManager.childCount; i++) { + offset += itemExtentBuilder!(i, _currentLayoutDimensions); + } + return offset; + } } int _calculateLeadingGarbage(int firstIndex) { @@ -170,28 +206,61 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda return trailingGarbage; } + int _getChildIndexForScrollOffset(double scrollOffset, ItemExtentBuilder callback) { + if (scrollOffset == 0.0) { + return 0; + } + double position = 0.0; + int index = 0; + while (position < scrollOffset) { + position += callback(index, _currentLayoutDimensions); + ++index; + } + return index - 1; + } + + BoxConstraints _getChildConstraints(int index) { + double extent; + if (itemExtentBuilder == null) { + extent = itemExtent!; + } else { + extent = itemExtentBuilder!(index, _currentLayoutDimensions); + } + return constraints.asBoxConstraints( + minExtent: extent, + maxExtent: extent, + ); + } + + late SliverLayoutDimensions _currentLayoutDimensions; + @override void performLayout() { + assert((itemExtent != null && itemExtentBuilder == null) || + (itemExtent == null && itemExtentBuilder != null)); + assert(itemExtentBuilder != null || (itemExtent!.isFinite && itemExtent! >= 0)); + final SliverConstraints constraints = this.constraints; childManager.didStartLayout(); childManager.setDidUnderflow(false); - final double itemExtent = this.itemExtent; - + final double itemFixedExtent = itemExtent ?? 0; final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; assert(scrollOffset >= 0.0); final double remainingExtent = constraints.remainingCacheExtent; assert(remainingExtent >= 0.0); final double targetEndScrollOffset = scrollOffset + remainingExtent; - final BoxConstraints childConstraints = constraints.asBoxConstraints( - minExtent: itemExtent, - maxExtent: itemExtent, + _currentLayoutDimensions = SliverLayoutDimensions( + scrollOffset: constraints.scrollOffset, + precedingScrollExtent: constraints.precedingScrollExtent, + viewportMainAxisExtent: constraints.viewportMainAxisExtent, + crossAxisExtent: constraints.crossAxisExtent ); - final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemExtent); + final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemFixedExtent); final int? targetLastIndex = targetEndScrollOffset.isFinite ? - getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemExtent) : null; + getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemFixedExtent) : null; if (firstChild != null) { final int leadingGarbage = _calculateLeadingGarbage(firstIndex); @@ -202,13 +271,13 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda } if (firstChild == null) { - if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) { + if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemFixedExtent, firstIndex))) { // There are either no children, or we are past the end of all our children. final double max; if (firstIndex <= 0) { max = 0.0; } else { - max = computeMaxScrollOffset(constraints, itemExtent); + max = computeMaxScrollOffset(constraints, itemFixedExtent); } geometry = SliverGeometry( scrollExtent: max, @@ -222,24 +291,24 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda RenderBox? trailingChildWithLayout; for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { - final RenderBox? child = insertAndLayoutLeadingChild(childConstraints); + final RenderBox? child = insertAndLayoutLeadingChild(_getChildConstraints(index)); if (child == null) { // Items before the previously first child are no longer present. // Reset the scroll offset to offset all items prior and up to the // missing item. Let parent re-layout everything. - geometry = SliverGeometry(scrollOffsetCorrection: index * itemExtent); + geometry = SliverGeometry(scrollOffsetCorrection: indexToLayoutOffset(itemFixedExtent, index)); return; } final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; - childParentData.layoutOffset = indexToLayoutOffset(itemExtent, index); + childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, index); assert(childParentData.index == index); trailingChildWithLayout ??= child; } if (trailingChildWithLayout == null) { - firstChild!.layout(childConstraints); + firstChild!.layout(_getChildConstraints(indexOf(firstChild!))); final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; - childParentData.layoutOffset = indexToLayoutOffset(itemExtent, firstIndex); + childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, firstIndex); trailingChildWithLayout = firstChild; } @@ -247,24 +316,24 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda for (int index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { RenderBox? child = childAfter(trailingChildWithLayout!); if (child == null || indexOf(child) != index) { - child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); + child = insertAndLayoutChild(_getChildConstraints(index), after: trailingChildWithLayout); if (child == null) { // We have run out of children. - estimatedMaxScrollOffset = index * itemExtent; + estimatedMaxScrollOffset = indexToLayoutOffset(itemFixedExtent, index); break; } } else { - child.layout(childConstraints); + child.layout(_getChildConstraints(index)); } trailingChildWithLayout = child; final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; assert(childParentData.index == index); - childParentData.layoutOffset = indexToLayoutOffset(itemExtent, childParentData.index!); + childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, childParentData.index!); } final int lastIndex = indexOf(lastChild!); - final double leadingScrollOffset = indexToLayoutOffset(itemExtent, firstIndex); - final double trailingScrollOffset = indexToLayoutOffset(itemExtent, lastIndex + 1); + final double leadingScrollOffset = indexToLayoutOffset(itemFixedExtent, firstIndex); + final double trailingScrollOffset = indexToLayoutOffset(itemFixedExtent, lastIndex + 1); assert(firstIndex == 0 || childScrollOffset(firstChild!)! - scrollOffset <= precisionErrorTolerance); assert(debugAssertChildListIsNonEmptyAndContiguous()); @@ -296,7 +365,8 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? - getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, itemExtent) : null; + getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, itemFixedExtent) : null; + geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, paintExtent: paintExtent, @@ -339,8 +409,6 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda class RenderSliverFixedExtentList extends RenderSliverFixedExtentBoxAdaptor { /// Creates a sliver that contains multiple box children that have a given /// extent in the main axis. - /// - /// The [childManager] argument must not be null. RenderSliverFixedExtentList({ required super.childManager, required double itemExtent, diff --git a/packages/flutter/lib/src/rendering/sliver_grid.dart b/packages/flutter/lib/src/rendering/sliver_grid.dart index 78d412edf4118..b2141dcd55cc6 100644 --- a/packages/flutter/lib/src/rendering/sliver_grid.dart +++ b/packages/flutter/lib/src/rendering/sliver_grid.dart @@ -13,6 +13,16 @@ import 'sliver_multi_box_adaptor.dart'; /// Describes the placement of a child in a [RenderSliverGrid]. /// +/// This class is similar to [Rect], in that it gives a two-dimensional position +/// and a two-dimensional dimension, but is direction-agnostic. +/// +/// {@tool dartpad} +/// This example shows how a custom [SliverGridLayout] uses [SliverGridGeometry] +/// to lay out the children. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SliverGridLayout], which represents the geometry of all the tiles in a @@ -60,7 +70,7 @@ class SliverGridGeometry { double get trailingScrollOffset => scrollOffset + mainAxisExtent; /// Returns a tight [BoxConstraints] that forces the child to have the - /// required size. + /// required size, given a [SliverConstraints]. BoxConstraints getBoxConstraints(SliverConstraints constraints) { return constraints.asBoxConstraints( minExtent: mainAxisExtent, @@ -83,13 +93,22 @@ class SliverGridGeometry { /// The size and position of all the tiles in a [RenderSliverGrid]. /// -/// Rather that providing a grid with a [SliverGridLayout] directly, you instead -/// provide the grid a [SliverGridDelegate], which can compute a -/// [SliverGridLayout] given the current [SliverConstraints]. +/// Rather that providing a grid with a [SliverGridLayout] directly, the grid is +/// provided a [SliverGridDelegate], which computes a [SliverGridLayout] given a +/// set of [SliverConstraints]. This allows the algorithm to dynamically respond +/// to changes in the environment (e.g. the user rotating the device). /// /// The tiles can be placed arbitrarily, but it is more efficient to place tiles -/// in roughly in order by scroll offset because grids reify a contiguous -/// sequence of children. +/// roughly in order by scroll offset because grids reify a contiguous sequence +/// of children. +/// +/// {@tool dartpad} +/// This example shows how to construct a custom [SliverGridLayout] to lay tiles +/// in a grid form with some cells stretched to fit the entire width of the +/// grid (sometimes called "hero tiles"). +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} /// /// See also: /// @@ -146,8 +165,8 @@ abstract class SliverGridLayout { class SliverGridRegularTileLayout extends SliverGridLayout { /// Creates a layout that uses equally sized and spaced tiles. /// - /// All of the arguments must not be null and must not be negative. The - /// `crossAxisCount` argument must be greater than zero. + /// All of the arguments must not be negative. The `crossAxisCount` argument + /// must be greater than zero. const SliverGridRegularTileLayout({ required this.crossAxisCount, required this.mainAxisStride, @@ -240,9 +259,16 @@ class SliverGridRegularTileLayout extends SliverGridLayout { /// /// Given the current constraints on the grid, a [SliverGridDelegate] computes /// the layout for the tiles in the grid. The tiles can be placed arbitrarily, -/// but it is more efficient to place tiles in roughly in order by scroll offset +/// but it is more efficient to place tiles roughly in order by scroll offset /// because grids reify a contiguous sequence of children. /// +/// {@tool dartpad} +/// This example shows how a [SliverGridDelegate] returns a [SliverGridLayout] +/// configured based on the provided [SliverConstraints] in [getLayout]. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with @@ -310,7 +336,6 @@ class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate { /// Creates a delegate that makes grid layouts with a fixed number of tiles in /// the cross axis. /// - /// All of the arguments except [mainAxisExtent] must not be null. /// The `mainAxisSpacing`, `mainAxisExtent` and `crossAxisSpacing` arguments /// must not be negative. The `crossAxisCount` and `childAspectRatio` /// arguments must be greater than zero. @@ -409,7 +434,6 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate { /// Creates a delegate that makes grid layouts with tiles that have a maximum /// cross-axis extent. /// - /// All of the arguments except [mainAxisExtent] must not be null. /// The [maxCrossAxisExtent], [mainAxisExtent], [mainAxisSpacing], /// and [crossAxisSpacing] arguments must not be negative. /// The [childAspectRatio] argument must be greater than zero. @@ -523,8 +547,6 @@ class SliverGridParentData extends SliverMultiBoxAdaptorParentData { class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { /// Creates a sliver that contains multiple box children that whose size and /// position are determined by a delegate. - /// - /// The [childManager] and [gridDelegate] arguments must not be null. RenderSliverGrid({ required super.childManager, required SliverGridDelegate gridDelegate, @@ -605,6 +627,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { final double leadingScrollOffset = firstChildGridGeometry.scrollOffset; double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset; RenderBox? trailingChildWithLayout; + bool reachedEnd = false; for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index); @@ -634,6 +657,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { if (child == null || indexOf(child) != index) { child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); if (child == null) { + reachedEnd = true; // We have run out of children. break; } @@ -654,13 +678,15 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { assert(indexOf(firstChild!) == firstIndex); assert(targetLastIndex == null || lastIndex <= targetLastIndex); - final double estimatedTotalExtent = childManager.estimateMaxScrollOffset( - constraints, - firstIndex: firstIndex, - lastIndex: lastIndex, - leadingScrollOffset: leadingScrollOffset, - trailingScrollOffset: trailingScrollOffset, - ); + final double estimatedTotalExtent = reachedEnd + ? trailingScrollOffset + : childManager.estimateMaxScrollOffset( + constraints, + firstIndex: firstIndex, + lastIndex: lastIndex, + leadingScrollOffset: leadingScrollOffset, + trailingScrollOffset: trailingScrollOffset, + ); final double paintExtent = calculatePaintOffset( constraints, from: math.min(constraints.scrollOffset, leadingScrollOffset), diff --git a/packages/flutter/lib/src/rendering/sliver_group.dart b/packages/flutter/lib/src/rendering/sliver_group.dart index b47daff6da70e..40e8ee85ec7bf 100644 --- a/packages/flutter/lib/src/rendering/sliver_group.dart +++ b/packages/flutter/lib/src/rendering/sliver_group.dart @@ -129,8 +129,10 @@ class RenderSliverCrossAxisGroup extends RenderSliver with ContainerRenderObject RenderSliver? child = firstChild; while (child != null) { - final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; - context.paintChild(child, offset + childParentData.paintOffset); + if (child.geometry!.visible) { + final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; + context.paintChild(child, offset + childParentData.paintOffset); + } child = childAfter(child); } } @@ -295,8 +297,10 @@ class RenderSliverMainAxisGroup extends RenderSliver with ContainerRenderObjectM RenderSliver? child = lastChild; while (child != null) { - final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; - context.paintChild(child, offset + childParentData.paintOffset); + if (child.geometry!.visible) { + final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; + context.paintChild(child, offset + childParentData.paintOffset); + } child = childBefore(child); } } diff --git a/packages/flutter/lib/src/rendering/sliver_list.dart b/packages/flutter/lib/src/rendering/sliver_list.dart index 8c5658e100659..e1fdedad1ca2f 100644 --- a/packages/flutter/lib/src/rendering/sliver_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_list.dart @@ -36,8 +36,6 @@ import 'sliver_multi_box_adaptor.dart'; class RenderSliverList extends RenderSliverMultiBoxAdaptor { /// Creates a sliver that places multiple box children in a linear array along /// the main axis. - /// - /// The [childManager] argument must not be null. RenderSliverList({ required super.childManager, }); diff --git a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart index 546087eac6188..76188fcbcaa74 100644 --- a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart +++ b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart @@ -184,8 +184,6 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver RenderSliverHelpers, RenderSliverWithKeepAliveMixin { /// Creates a sliver with multiple box children. - /// - /// The [childManager] argument must not be null. RenderSliverMultiBoxAdaptor({ required RenderSliverBoxChildManager childManager, }) : _childManager = childManager { diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart index a245b79b461e7..62540d9655716 100644 --- a/packages/flutter/lib/src/rendering/sliver_padding.dart +++ b/packages/flutter/lib/src/rendering/sliver_padding.dart @@ -18,7 +18,7 @@ import 'sliver.dart'; /// /// {@template flutter.rendering.RenderSliverEdgeInsetsPadding} /// Applying padding in the main extent of the viewport to slivers that have scroll effects is likely to have -/// undesired effects. For example, For example, wrapping a [SliverPersistentHeader] with +/// undesired effects. For example, wrapping a [SliverPersistentHeader] with /// `pinned:true` will cause only the appbar to stay pinned while the padding will scroll away. /// {@endtemplate} abstract class RenderSliverEdgeInsetsPadding extends RenderSliver with RenderObjectWithChildMixin { @@ -298,7 +298,7 @@ abstract class RenderSliverEdgeInsetsPadding extends RenderSliver with RenderObj class RenderSliverPadding extends RenderSliverEdgeInsetsPadding { /// Creates a render object that insets its child in a viewport. /// - /// The [padding] argument must not be null and must have non-negative insets. + /// The [padding] argument must have non-negative insets. RenderSliverPadding({ required EdgeInsetsGeometry padding, TextDirection? textDirection, diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index bb2e3c9a708a5..5fe64c47b7c77 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -18,13 +18,9 @@ import 'object.dart'; /// container, this class has no width and height members. To determine the /// width or height of the rectangle, convert it to a [Rect] using [toRect()] /// (passing the container's own Rect), and then examine that object. -/// -/// The fields [left], [right], [bottom], and [top] must not be null. @immutable class RelativeRect { /// Creates a RelativeRect with the given values. - /// - /// The arguments must not be null. const RelativeRect.fromLTRB(this.left, this.top, this.right, this.bottom); /// Creates a RelativeRect from a Rect and a Size. The Rect (first argument) @@ -435,7 +431,16 @@ class RenderStack extends RenderBox /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Stacks only clip children whose geometry overflow the stack. A child that + /// paints outside its bounds (e.g. a box with a shadow) will not be clipped, + /// regardless of the value of this property. Similarly, a child that itself + /// has a descendant that overflows the stack will not be clipped, as only the + /// geometry of the stack's direct children are considered. + /// + /// To clip children whose geometry does not overflow the stack, consider + /// using a [RenderClipRect] render object. + /// + /// Defaults to [Clip.hardEdge]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -560,15 +565,11 @@ class RenderStack extends RenderBox double width = constraints.minWidth; double height = constraints.minHeight; - final BoxConstraints nonPositionedConstraints; - switch (fit) { - case StackFit.loose: - nonPositionedConstraints = constraints.loosen(); - case StackFit.expand: - nonPositionedConstraints = BoxConstraints.tight(constraints.biggest); - case StackFit.passthrough: - nonPositionedConstraints = constraints; - } + final BoxConstraints nonPositionedConstraints = switch (fit) { + StackFit.loose => constraints.loosen(), + StackFit.expand => BoxConstraints.tight(constraints.biggest), + StackFit.passthrough => constraints, + }; RenderBox? child = firstChild; while (child != null) { diff --git a/packages/flutter/lib/src/rendering/table.dart b/packages/flutter/lib/src/rendering/table.dart index 2208664399fc3..2aa4658c3b583 100644 --- a/packages/flutter/lib/src/rendering/table.dart +++ b/packages/flutter/lib/src/rendering/table.dart @@ -132,8 +132,6 @@ class IntrinsicColumnWidth extends TableColumnWidth { /// This is the cheapest way to size a column. class FixedColumnWidth extends TableColumnWidth { /// Creates a column width based on a fixed number of logical pixels. - /// - /// The [value] argument must not be null. const FixedColumnWidth(this.value); /// The width the column should occupy in logical pixels. @@ -159,8 +157,6 @@ class FixedColumnWidth extends TableColumnWidth { class FractionColumnWidth extends TableColumnWidth { /// Creates a column width based on a fraction of the table's constraints' /// maxWidth. - /// - /// The [value] argument must not be null. const FractionColumnWidth(this.value); /// The fraction of the table's constraints' maxWidth that this column should @@ -197,8 +193,6 @@ class FractionColumnWidth extends TableColumnWidth { class FlexColumnWidth extends TableColumnWidth { /// Creates a column width based on a fraction of the remaining space once all /// the other columns have been laid out. - /// - /// The [value] argument must not be null. const FlexColumnWidth([this.value = 1.0]); /// The fraction of the remaining space once all the other columns have @@ -263,12 +257,11 @@ class MaxColumnWidth extends TableColumnWidth { @override double? flex(Iterable cells) { final double? aFlex = a.flex(cells); - if (aFlex == null) { - return b.flex(cells); - } final double? bFlex = b.flex(cells); - if (bFlex == null) { - return null; + if (aFlex == null) { + return bFlex; + } else if (bFlex == null) { + return aFlex; } return math.max(aFlex, bFlex); } @@ -316,12 +309,11 @@ class MinColumnWidth extends TableColumnWidth { @override double? flex(Iterable cells) { final double? aFlex = a.flex(cells); - if (aFlex == null) { - return b.flex(cells); - } final double? bFlex = b.flex(cells); - if (bFlex == null) { - return null; + if (aFlex == null) { + return bFlex; + } else if (bFlex == null) { + return aFlex; } return math.min(aFlex, bFlex); } @@ -371,8 +363,6 @@ class RenderTable extends RenderBox { /// * `children` must either be null or contain lists of all the same length. /// if `children` is not null, then `rows` must be null. /// * [columnWidths] may be null, in which case it defaults to an empty map. - /// * [defaultColumnWidth] must not be null. - /// * [configuration] must not be null (but has a default value). RenderTable({ int? columns, int? rows, diff --git a/packages/flutter/lib/src/rendering/texture.dart b/packages/flutter/lib/src/rendering/texture.dart index 0485ab12abe95..fcbaaa3ed09d9 100644 --- a/packages/flutter/lib/src/rendering/texture.dart +++ b/packages/flutter/lib/src/rendering/texture.dart @@ -29,9 +29,9 @@ import 'object.dart'; /// /// See also: /// -/// * +/// * [TextureRegistry](/javadoc/io/flutter/view/TextureRegistry.html) /// for how to create and manage backend textures on Android. -/// * +/// * [TextureRegistry Protocol](/ios-embedder/protocol_flutter_texture_registry-p.html) /// for how to create and manage backend textures on iOS. class TextureBox extends RenderBox { /// Creates a box backed by the texture identified by [textureId], and use diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 906d237c1da50..03db7f1d608ca 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -67,10 +67,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin /// /// Typically created by the binding (e.g., [RendererBinding]). /// - /// The [configuration] must not be null. + /// Providing a [configuration] is optional, but a configuration must be set + /// before calling [prepareInitialFrame]. This decouples creating the + /// [RenderView] object from configuring it. Typically, the object is created + /// by the [View] widget and configured by the [RendererBinding] when the + /// [RenderView] is registered with it by the [View] widget. RenderView({ RenderBox? child, - required ViewConfiguration configuration, + ViewConfiguration? configuration, required ui.FlutterView view, }) : _configuration = configuration, _view = view { @@ -82,26 +86,39 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin Size _size = Size.zero; /// The constraints used for the root layout. - ViewConfiguration get configuration => _configuration; - ViewConfiguration _configuration; - - /// The configuration is initially set by the [configuration] argument - /// passed to the constructor. /// - /// Always call [prepareInitialFrame] before changing the configuration. + /// Typically, this configuration is set by the [RendererBinding], when the + /// [RenderView] is registered with it. It will also update the configuration + /// if necessary. Therefore, if used in conjunction with the [RendererBinding] + /// this property must not be set manually as the [RendererBinding] will just + /// override it. + /// + /// For tests that want to change the size of the view, set + /// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView] + /// (typically [WidgetTester.view]) instead of setting a configuration + /// directly on the [RenderView]. + ViewConfiguration get configuration => _configuration!; + ViewConfiguration? _configuration; set configuration(ViewConfiguration value) { - if (configuration == value) { + if (_configuration == value) { return; } - final ViewConfiguration oldConfiguration = _configuration; + final ViewConfiguration? oldConfiguration = _configuration; _configuration = value; - if (oldConfiguration.toMatrix() != _configuration.toMatrix()) { + if (_rootTransform == null) { + // [prepareInitialFrame] has not been called yet, nothing to do for now. + return; + } + if (oldConfiguration?.toMatrix() != configuration.toMatrix()) { replaceRootLayer(_updateMatricesAndCreateNewRootLayer()); } assert(_rootTransform != null); markNeedsLayout(); } + /// Whether a [configuration] has been set. + bool get hasConfiguration => _configuration != null; + /// The [FlutterView] into which this [RenderView] will render. ui.FlutterView get flutterView => _view; final ui.FlutterView _view; diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 44876d8326e54..8756c8fe63f85 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -108,10 +108,27 @@ abstract interface class RenderAbstractViewport extends RenderObject { /// when the offset of the viewport is changed by x then `target` also moves /// by x within the viewport. /// + /// The optional [Axis] is used by + /// [RenderTwoDimensionalViewport.getOffsetToReveal] to + /// determine which of the two axes to compute an offset for. One dimensional + /// subclasses like [RenderViewportBase] and [RenderListWheelViewport] + /// will ignore the `axis` value if provided, since there is only one [Axis]. + /// + /// If the `axis` is omitted when called on [RenderTwoDimensionalViewport], + /// the [RenderTwoDimensionalViewport.mainAxis] is used. To reveal an object + /// properly in both axes, this method should be called for each [Axis] as the + /// returned [RevealedOffset.offset] only represents the offset of one of the + /// the two [ScrollPosition]s. + /// /// See also: /// /// * [RevealedOffset], which describes the return value of this method. - RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }); + RevealedOffset getOffsetToReveal( + RenderObject target, + double alignment, { + Rect? rect, + Axis? axis, + }); /// The default value for the cache extent of the viewport. /// @@ -169,6 +186,56 @@ class RevealedOffset { /// value for a specific element. final Rect rect; + /// Determines which provided leading or trailing edge of the viewport, as + /// [RevealedOffset]s, will be used for [RenderViewportBase.showInViewport] + /// accounting for the size and already visible portion of the [RenderObject] + /// that is being revealed. + /// + /// Also used by [RenderTwoDimensionalViewport.showInViewport] for each + /// horizontal and vertical [Axis]. + /// + /// If the target [RenderObject] is already fully visible, this will return + /// null. + static RevealedOffset? clampOffset({ + required RevealedOffset leadingEdgeOffset, + required RevealedOffset trailingEdgeOffset, + required double currentOffset, + }) { + // scrollOffset + // 0 +---------+ + // | | + // _ | | + // viewport position | | | + // with `descendant` at | | | _ + // trailing edge |_ | xxxxxxx | | viewport position + // | | | with `descendant` at + // | | _| leading edge + // | | + // 800 +---------+ + // + // `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the + // viewport on the left in image above. + // `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the + // viewport on the right in image above. + // + // The viewport position on the left is achieved by setting `offset.pixels` + // to `trailingEdgeOffset`, the one on the right by setting it to + // `leadingEdgeOffset`. + final bool inverted = leadingEdgeOffset.offset < trailingEdgeOffset.offset; + final RevealedOffset smaller; + final RevealedOffset larger; + (smaller, larger) = inverted + ? (leadingEdgeOffset, trailingEdgeOffset) + : (trailingEdgeOffset, leadingEdgeOffset); + if (currentOffset > larger.offset) { + return larger; + } else if (currentOffset < smaller.offset) { + return smaller; + } else { + return null; + } + } + @override String toString() { return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)'; @@ -386,7 +453,7 @@ abstract class RenderViewportBase _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -753,7 +820,16 @@ abstract class RenderViewportBase leadingEdgeOffset.offset) { - // `descendant` currently starts above the leading edge and can be shown - // fully on screen by scrolling down (which means: moving viewport up). - targetOffset = leadingEdgeOffset; - } else if (currentOffset < trailingEdgeOffset.offset) { - // `descendant currently ends below the trailing edge and can be shown - // fully on screen by scrolling up (which means: moving viewport down) - targetOffset = trailingEdgeOffset; - } else { + final RevealedOffset? targetOffset = RevealedOffset.clampOffset( + leadingEdgeOffset: leadingEdgeOffset, + trailingEdgeOffset: trailingEdgeOffset, + currentOffset: currentOffset, + ); + if (targetOffset == null) { // `descendant` is between leading and trailing edge and hence already // fully shown on screen. No action necessary. assert(viewport.parent != null); @@ -1210,7 +1253,6 @@ abstract class RenderViewportBase _clipBehavior; Clip _clipBehavior = Clip.none; set clipBehavior(Clip value) { diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index dc690ab518825..2035b84927a11 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -14,7 +14,6 @@ import 'debug.dart'; import 'priority.dart'; import 'service_extensions.dart'; -export 'dart:developer' show Flow; export 'dart:ui' show AppLifecycleState, FrameTiming, TimingsCallback; export 'priority.dart' show Priority; @@ -934,13 +933,40 @@ mixin SchedulerBinding on BindingBase { /// [scheduleWarmUpFrame] was already called, this call will be ignored. /// /// Prefer [scheduleFrame] to update the display in normal operation. + /// + /// ## Design discussion + /// + /// The Flutter engine prompts the framework to generate frames when it + /// receives a request from the operating system (known for historical reasons + /// as a vsync). However, this may not happen for several milliseconds after + /// the app starts (or after a hot reload). To make use of the time between + /// when the widget tree is first configured and when the engine requests an + /// update, the framework schedules a _warm-up frame_. + /// + /// A warm-up frame may never actually render (as the engine did not request + /// it and therefore does not have a valid context in which to paint), but it + /// will cause the framework to go through the steps of building, laying out, + /// and painting, which can together take several milliseconds. Thus, when the + /// engine requests a real frame, much of the work will already have been + /// completed, and the framework can generate the frame with minimal + /// additional effort. + /// + /// Warm-up frames are scheduled by [runApp] on startup, and by + /// [RendererBinding.performReassemble] during a hot reload. + /// + /// Warm-up frames are also scheduled when the framework is unblocked by a + /// call to [RendererBinding.allowFirstFrame] (corresponding to a call to + /// [RendererBinding.deferFirstFrame] that blocked the rendering). void scheduleWarmUpFrame() { if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) { return; } _warmUpFrame = true; - final TimelineTask timelineTask = TimelineTask()..start('Warm-up frame'); + TimelineTask? debugTimelineTask; + if (!kReleaseMode) { + debugTimelineTask = TimelineTask()..start('Warm-up frame'); + } final bool hadScheduledFrame = _hasScheduledFrame; // We use timers here to ensure that microtasks flush in between. Timer.run(() { @@ -969,7 +995,9 @@ mixin SchedulerBinding on BindingBase { // scheduled frame has finished. lockEvents(() async { await endOfFrame; - timelineTask.finish(); + if (!kReleaseMode) { + debugTimelineTask!.finish(); + } }); } @@ -1223,7 +1251,7 @@ mixin SchedulerBinding on BindingBase { try { // PERSISTENT FRAME CALLBACKS _schedulerPhase = SchedulerPhase.persistentCallbacks; - for (final FrameCallback callback in _persistentCallbacks) { + for (final FrameCallback callback in List.of(_persistentCallbacks)) { _invokeFrameCallback(callback, _currentFrameTimeStamp!); } diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index cf39779d17061..29f87d6f94369 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' as ui; -import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection; +import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilder, StringAttribute, TextDirection; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -52,7 +51,7 @@ typedef SemanticsActionHandler = void Function(Object? args); /// Signature for a function that receives a semantics update and returns no result. /// /// Used by [SemanticsOwner.onSemanticsUpdate]. -typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update); +typedef SemanticsUpdateCallback = void Function(SemanticsUpdate update); /// Signature for the [SemanticsConfiguration.childConfigurationsDelegate]. /// @@ -212,7 +211,7 @@ class ChildSemanticsConfigurationsResultBuilder { class CustomSemanticsAction { /// Creates a new [CustomSemanticsAction]. /// - /// The [label] must not be null or the empty string. + /// The [label] must not be empty. const CustomSemanticsAction({required String this.label}) : assert(label != ''), hint = null, @@ -221,7 +220,7 @@ class CustomSemanticsAction { /// Creates a new [CustomSemanticsAction] that overrides a standard semantics /// action. /// - /// The [hint] must not be null or the empty string. + /// The [hint] must not be empty. const CustomSemanticsAction.overridingAction({required String this.hint, required SemanticsAction this.action}) : assert(hint != ''), label = null; @@ -412,8 +411,6 @@ class AttributedStringProperty extends DiagnosticsProperty { class SemanticsData with Diagnosticable { /// Creates a semantics data object. /// - /// The [flags], [actions], [label], and [Rect] arguments must not be null. - /// /// If [label] is not empty, then [textDirection] must also not be null. SemanticsData({ required this.flags, @@ -871,6 +868,7 @@ class SemanticsProperties extends DiagnosticableTree { this.enabled, this.checked, this.mixed, + this.expanded, this.selected, this.toggled, this.button, @@ -965,6 +963,14 @@ class SemanticsProperties extends DiagnosticableTree { /// This is mutually exclusive with [checked] and [toggled]. final bool? mixed; + /// If non-null, indicates that this subtree represents something + /// that can be in an "expanded" or "collapsed" state. + /// + /// For example, if a [SubmenuButton] is opened, this property + /// should be set to true; otherwise, this property should be + /// false. + final bool? expanded; + /// If non-null, indicates that this subtree represents a toggle switch /// or similar widget with an "on" state, and what its current /// state is. @@ -1613,6 +1619,7 @@ class SemanticsProperties extends DiagnosticableTree { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('checked', checked, defaultValue: null)); properties.add(DiagnosticsProperty('mixed', mixed, defaultValue: null)); + properties.add(DiagnosticsProperty('expanded', expanded, defaultValue: null)); properties.add(DiagnosticsProperty('selected', selected, defaultValue: null)); properties.add(StringProperty('label', label, defaultValue: null)); properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null)); @@ -2000,25 +2007,29 @@ class SemanticsNode with DiagnosticableTreeMixin { /// The owner for this node (null if unattached). /// - /// The entire subtree that this node belongs to will have the same owner. + /// The entire semantics tree that this node belongs to will have the same owner. SemanticsOwner? get owner => _owner; SemanticsOwner? _owner; - /// Whether this node is in a tree whose root is attached to something. + /// Whether the semantics tree this node belongs to is attached to a [SemanticsOwner]. /// /// This becomes true during the call to [attach]. /// /// This becomes false during the call to [detach]. bool get attached => _owner != null; - /// The parent of this node in the tree. + /// The parent of this node in the semantics tree. + /// + /// The [parent] of the root node in the semantics tree is null. SemanticsNode? get parent => _parent; SemanticsNode? _parent; - /// The depth of this node in the tree. + /// The depth of this node in the semantics tree. /// /// The depth of nodes in a tree monotonically increases as you traverse down - /// the tree. + /// the tree. There's no guarantee regarding depth between siblings. + /// + /// The depth is used to ensure that nodes are processed in depth order. int get depth => _depth; int _depth = 0; @@ -2083,7 +2094,7 @@ class SemanticsNode with DiagnosticableTreeMixin { } } - /// Mark this node as detached. + /// Mark this node as detached from its owner. @visibleForTesting void detach() { assert(_owner != null); @@ -2668,7 +2679,7 @@ class SemanticsNode with DiagnosticableTreeMixin { static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0); static final Float64List _kIdentityTransform = _initIdentityTransform(); - void _addToUpdate(ui.SemanticsUpdateBuilder builder, Set customSemanticsActionIdsUpdate) { + void _addToUpdate(SemanticsUpdateBuilder builder, Set customSemanticsActionIdsUpdate) { assert(_dirty); final SemanticsData data = getSemanticsData(); final Int32List childrenInTraversalOrder; @@ -3288,7 +3299,7 @@ class SemanticsOwner extends ChangeNotifier { } } visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); - final ui.SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder(); + final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder(); for (final SemanticsNode node in visitedNodes) { assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) // The _serialize() method marks the node as not dirty, and @@ -4395,6 +4406,20 @@ class SemanticsConfiguration { _setFlag(SemanticsFlag.isSelected, value); } + /// If this node has Boolean state that can be controlled by the user, whether + /// that state is expanded or collapsed, corresponding to true and false, respectively. + /// + /// Do not call the setter for this field if the owning [RenderObject] doesn't + /// have expanded/collapsed state that can be controlled by the user. + /// + /// The getter returns null if the owning [RenderObject] does not have + /// expanded/collapsed state. + bool? get isExpanded => _hasFlag(SemanticsFlag.hasExpandedState) ? _hasFlag(SemanticsFlag.isExpanded) : null; + set isExpanded(bool? value) { + _setFlag(SemanticsFlag.hasExpandedState, true); + _setFlag(SemanticsFlag.isExpanded, value!); + } + /// Whether the owning [RenderObject] is currently enabled. /// /// A disabled object does not respond to user interactions. Only objects that @@ -4967,7 +4992,7 @@ abstract class SemanticsSortKey with Diagnosticable implements Comparable { -/// bool noticeAccepted = false; /// final GlobalKey mykey = GlobalKey(); /// /// @override diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index 5a752930ad69e..0c65ca323ca74 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -57,11 +57,10 @@ abstract class AssetBundle { /// Throws an exception if the asset is not found. /// /// The returned [ByteData] can be converted to a [Uint8List] (a list of bytes) - /// using [ByteData.buffer] to obtain a [ByteBuffer], and then - /// [ByteBuffer.asUint8List] to obtain the byte list. Lists of bytes can be - /// used with APIs that accept [Uint8List] objects, such as - /// [decodeImageFromList], as well as any API that accepts a [List], such - /// as [File.writeAsBytes] or [Utf8Codec.decode] (accessible via [utf8]). + /// using [Uint8List.sublistView]. Lists of bytes can be used with APIs that + /// accept [Uint8List] objects, such as [decodeImageFromList], as well as any + /// API that accepts a [List], such as [File.writeAsBytes] or + /// [Utf8Codec.decode] (accessible via [utf8]). Future load(String key); /// Retrieve a binary resource from the asset bundle as an immutable @@ -70,7 +69,7 @@ abstract class AssetBundle { /// Throws an exception if the asset is not found. Future loadBuffer(String key) async { final ByteData data = await load(key); - return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); + return ui.ImmutableBuffer.fromUint8List(Uint8List.sublistView(data)); } /// Retrieve a string from the asset bundle. @@ -89,9 +88,9 @@ abstract class AssetBundle { Future loadString(String key, { bool cache = true }) async { final ByteData data = await load(key); // 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs - // on a Pixel 4. - if (data.lengthInBytes < 50 * 1024) { - return utf8.decode(data.buffer.asUint8List()); + // on a Pixel 4. On the web we can't bail to isolates, though... + if (data.lengthInBytes < 50 * 1024 || kIsWeb) { + return utf8.decode(Uint8List.sublistView(data)); } // For strings larger than 50 KB, run the computation in an isolate to // avoid causing main thread jank. @@ -99,24 +98,27 @@ abstract class AssetBundle { } static String _utf8decode(ByteData data) { - return utf8.decode(data.buffer.asUint8List()); + return utf8.decode(Uint8List.sublistView(data)); } /// Retrieve a string from the asset bundle, parse it with the given function, /// and return that function's result. /// - /// Implementations may cache the result, so a particular key should only be - /// used with one parser for the lifetime of the asset bundle. - Future loadStructuredData(String key, Future Function(String value) parser); + /// The result is not cached by the default implementation; the parser is run + /// each time the resource is fetched. However, some subclasses may implement + /// caching (notably, subclasses of [CachingAssetBundle]). + Future loadStructuredData(String key, Future Function(String value) parser) async { + return parser(await loadString(key)); + } /// Retrieve [ByteData] from the asset bundle, parse it with the given function, /// and return that function's result. /// - /// Implementations may cache the result, so a particular key should only be - /// used with one parser for the lifetime of the asset bundle. + /// The result is not cached by the default implementation; the parser is run + /// each time the resource is fetched. However, some subclasses may implement + /// caching (notably, subclasses of [CachingAssetBundle]). Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) async { - final ByteData data = await load(key); - return parser(data); + return parser(await load(key)); } /// If this is a caching asset bundle, and the given key describes a cached @@ -158,27 +160,7 @@ class NetworkAssetBundle extends AssetBundle { ]); } final Uint8List bytes = await consolidateHttpClientResponseBytes(response); - return bytes.buffer.asByteData(); - } - - /// Retrieve a string from the asset bundle, parse it with the given function, - /// and return the function's result. - /// - /// The result is not cached. The parser is run each time the resource is - /// fetched. - @override - Future loadStructuredData(String key, Future Function(String value) parser) async { - return parser(await loadString(key)); - } - - /// Retrieve [ByteData] from the asset bundle, parse it with the given function, - /// and return the function's result. - /// - /// The result is not cached. The parser is run each time the resource is - /// fetched. - @override - Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) async { - return parser(await load(key)); + return ByteData.sublistView(bytes); } // TODO(ianh): Once the underlying network logic learns about caching, we @@ -217,30 +199,40 @@ abstract class CachingAssetBundle extends AssetBundle { /// unless you also fetch it with [loadString]). For any given `key`, the /// `parser` is only run the first time. /// - /// Once the value has been parsed, the future returned by this function for - /// subsequent calls will be a [SynchronousFuture], which resolves its - /// callback synchronously. + /// Once the value has been successfully parsed, the future returned by this + /// function for subsequent calls will be a [SynchronousFuture], which + /// resolves its callback synchronously. + /// + /// Failures are not cached, and are returned as [Future]s with errors. @override Future loadStructuredData(String key, Future Function(String value) parser) { if (_structuredDataCache.containsKey(key)) { return _structuredDataCache[key]! as Future; } - Completer? completer; - Future? result; + // loadString can return a SynchronousFuture in certain cases, like in the + // flutter_test framework. So, we need to support both async and sync flows. + Completer? completer; // For async flow. + Future? synchronousResult; // For sync flow. loadString(key, cache: false).then(parser).then((T value) { - result = SynchronousFuture(value); - _structuredDataCache[key] = result!; + synchronousResult = SynchronousFuture(value); + _structuredDataCache[key] = synchronousResult!; if (completer != null) { // We already returned from the loadStructuredData function, which means // we are in the asynchronous mode. Pass the value to the completer. The // completer's future is what we returned. completer.complete(value); } + }, onError: (Object error, StackTrace stack) { + assert(completer != null, 'unexpected synchronous failure'); + // Either loading or parsing failed. We must report the error back to the + // caller and anyone waiting on this call. We clear the cache for this + // key, however, because we want future attempts to try again. + _structuredDataCache.remove(key); + completer!.completeError(error, stack); }); - if (result != null) { - // The code above ran synchronously, and came up with an answer. - // Return the SynchronousFuture that we created above. - return result!; + if (synchronousResult != null) { + // The above code ran synchronously. We can synchronously return the result. + return synchronousResult!; } // The code above hasn't yet run its "then" handler yet. Let's prepare a // completer for it to use when it does run. @@ -255,40 +247,41 @@ abstract class CachingAssetBundle extends AssetBundle { /// The result of parsing the bytedata is cached (the bytedata itself is not). /// For any given `key`, the `parser` is only run the first time. /// - /// Once the value has been parsed, the future returned by this function for - /// subsequent calls will be a [SynchronousFuture], which resolves its - /// callback synchronously. + /// Once the value has been successfully parsed, the future returned by this + /// function for subsequent calls will be a [SynchronousFuture], which + /// resolves its callback synchronously. + /// + /// Failures are not cached, and are returned as [Future]s with errors. @override Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) { if (_structuredBinaryDataCache.containsKey(key)) { return _structuredBinaryDataCache[key]! as Future; } - // load can return a SynchronousFuture in certain cases, like in the // flutter_test framework. So, we need to support both async and sync flows. Completer? completer; // For async flow. - SynchronousFuture? result; // For sync flow. - - load(key) - .then(parser) - .then((T value) { - result = SynchronousFuture(value); - _structuredBinaryDataCache[key] = result!; - if (completer != null) { - // The load and parse operation ran asynchronously. We already returned - // from the loadStructuredBinaryData function and therefore the caller - // was given the future of the completer. - completer.complete(value); - } - }, onError: (Object error, StackTrace stack) { - completer!.completeError(error, stack); - }); - - if (result != null) { + Future? synchronousResult; // For sync flow. + load(key).then(parser).then((T value) { + synchronousResult = SynchronousFuture(value); + _structuredBinaryDataCache[key] = synchronousResult!; + if (completer != null) { + // The load and parse operation ran asynchronously. We already returned + // from the loadStructuredBinaryData function and therefore the caller + // was given the future of the completer. + completer.complete(value); + } + }, onError: (Object error, StackTrace stack) { + assert(completer != null, 'unexpected synchronous failure'); + // Either loading or parsing failed. We must report the error back to the + // caller and anyone waiting on this call. We clear the cache for this + // key, however, because we want future attempts to try again. + _structuredBinaryDataCache.remove(key); + completer!.completeError(error, stack); + }); + if (synchronousResult != null) { // The above code ran synchronously. We can synchronously return the result. - return result!; + return synchronousResult!; } - // Since the above code is being run asynchronously and thus hasn't run its // `then` handler yet, we'll return a completer that will be completed // when the handler does run. @@ -314,7 +307,7 @@ abstract class CachingAssetBundle extends AssetBundle { @override Future loadBuffer(String key) async { final ByteData data = await load(key); - return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); + return ui.ImmutableBuffer.fromUint8List(Uint8List.sublistView(data)); } } @@ -322,10 +315,10 @@ abstract class CachingAssetBundle extends AssetBundle { class PlatformAssetBundle extends CachingAssetBundle { @override Future load(String key) { - final Uint8List encoded = utf8.encoder.convert(Uri(path: Uri.encodeFull(key)).path); + final Uint8List encoded = utf8.encode(Uri(path: Uri.encodeFull(key)).path); final Future? future = ServicesBinding.instance.defaultBinaryMessenger.send( 'flutter/assets', - encoded.buffer.asByteData(), + ByteData.sublistView(encoded), )?.then((ByteData? asset) { if (asset == null) { throw FlutterError.fromParts([ @@ -348,7 +341,7 @@ class PlatformAssetBundle extends CachingAssetBundle { Future loadBuffer(String key) async { if (kIsWeb) { final ByteData bytes = await load(key); - return ui.ImmutableBuffer.fromUint8List(bytes.buffer.asUint8List()); + return ui.ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes)); } bool debugUsePlatformChannel = false; assert(() { @@ -364,7 +357,7 @@ class PlatformAssetBundle extends CachingAssetBundle { }()); if (debugUsePlatformChannel) { final ByteData bytes = await load(key); - return ui.ImmutableBuffer.fromUint8List(bytes.buffer.asUint8List()); + return ui.ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes)); } try { return await ui.ImmutableBuffer.fromAsset(key); diff --git a/packages/flutter/lib/src/services/asset_manifest.dart b/packages/flutter/lib/src/services/asset_manifest.dart index 3b27490098a8d..c948067e4cb39 100644 --- a/packages/flutter/lib/src/services/asset_manifest.dart +++ b/packages/flutter/lib/src/services/asset_manifest.dart @@ -2,18 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'asset_bundle.dart'; import 'message_codecs.dart'; // We use .bin as the extension since it is well-known to represent -// data in some arbitrary binary format. Using a well-known extension here -// is important for web, because some web servers will not serve files with -// unrecognized file extensions by default. -// See https://github.com/flutter/flutter/issues/128456. +// data in some arbitrary binary format. const String _kAssetManifestFilename = 'AssetManifest.bin'; +// We use the same bin file for the web, but re-encoded as JSON(base64(bytes)) +// so it can be downloaded by even the dumbest of browsers. +// See https://github.com/flutter/flutter/issues/128456 +const String _kAssetManifestWebFilename = 'AssetManifest.bin.json'; + /// Contains details about available assets and their variants. /// See [Resolution-aware image assets](https://docs.flutter.dev/ui/assets-and-images#resolution-aware) /// to learn about asset variants and how to declare them. @@ -21,6 +25,22 @@ abstract class AssetManifest { /// Loads asset manifest data from an [AssetBundle] object and creates an /// [AssetManifest] object from that data. static Future loadFromAssetBundle(AssetBundle bundle) { + // The AssetManifest file contains binary data. + // + // On the web, the build process wraps this binary data in json+base64 so + // it can be transmitted over the network without special configuration + // (see #131382). + if (kIsWeb) { + // On the web, the AssetManifest is downloaded as a String, then + // json+base64-decoded to get to the binary data. + return bundle.loadStructuredData(_kAssetManifestWebFilename, (String jsonData) async { + // Decode the manifest JSON file to the underlying BIN, and convert to ByteData. + final ByteData message = ByteData.sublistView(base64.decode(json.decode(jsonData) as String)); + // Now we can keep operating as usual. + return _AssetManifestBin.fromStandardMessageCodecMessage(message); + }); + } + // On every other platform, the binary file contents are used directly. return bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage); } diff --git a/packages/flutter/lib/src/services/autofill.dart b/packages/flutter/lib/src/services/autofill.dart index 8cf825fc5f71d..992d72557b613 100644 --- a/packages/flutter/lib/src/services/autofill.dart +++ b/packages/flutter/lib/src/services/autofill.dart @@ -670,15 +670,11 @@ class AutofillConfiguration { /// /// The identifier needs to be unique within the [AutofillScope] for the /// [AutofillClient] to receive the correct autofill value. - /// - /// Must not be null. final String uniqueIdentifier; /// A list of strings that helps the autofill service identify the type of the /// [AutofillClient]. /// - /// Must not be null. - /// /// {@template flutter.services.AutofillConfiguration.autofillHints} /// For the best results, hint strings need to be understood by the platform's /// autofill service. The common values of hint strings can be found in @@ -753,7 +749,7 @@ class AutofillConfiguration { abstract class AutofillClient { /// The unique identifier of this [AutofillClient]. /// - /// Must not be null and the identifier must not be changed. + /// The identifier must not be changed. String get autofillId; /// The [TextInputConfiguration] that describes this [AutofillClient]. diff --git a/packages/flutter/lib/src/services/debug.dart b/packages/flutter/lib/src/services/debug.dart index 518c29c5359bf..ce63157aaf760 100644 --- a/packages/flutter/lib/src/services/debug.dart +++ b/packages/flutter/lib/src/services/debug.dart @@ -15,18 +15,6 @@ export 'hardware_keyboard.dart' show KeyDataTransitMode; /// of their extent of support for keyboard API. KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride; -/// Profile and print statistics on Platform Channel usage. -/// -/// When this is true statistics about the usage of Platform Channels will be -/// printed out periodically to the console and Timeline events will show the -/// time between sending and receiving a message (encoding and decoding time -/// excluded). -/// -/// The statistics include the total bytes transmitted and the average number of -/// bytes per invocation in the last quantum. "Up" means in the direction of -/// Flutter to the host platform, "down" is the host platform to flutter. -bool debugProfilePlatformChannels = false; - /// Setting to true will cause extensive logging to occur when key events are /// received. /// @@ -46,7 +34,7 @@ bool debugAssertAllServicesVarsUnset(String reason) { if (debugKeyEventSimulatorTransitModeOverride != null) { throw FlutterError(reason); } - if (debugProfilePlatformChannels || debugPrintKeyboardEvents) { + if (debugPrintKeyboardEvents) { throw FlutterError(reason); } return true; diff --git a/packages/flutter/lib/src/services/dom.dart b/packages/flutter/lib/src/services/dom.dart deleted file mode 100644 index 00d3770949b3d..0000000000000 --- a/packages/flutter/lib/src/services/dom.dart +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:js_interop'; - -/// This file includes static interop helpers for Flutter Web. -// TODO(joshualitt): This file will eventually be removed, -// https://github.com/flutter/flutter/issues/113402. - -/// [DomWindow] interop object. -@JS() -@staticInterop -class DomWindow {} - -/// [DomWindow] required extension. -extension DomWindowExtension on DomWindow { - @JS('matchMedia') - external DomMediaQueryList _matchMedia(JSString? query); - - /// Returns a [DomMediaQueryList] of the media that matches [query]. - DomMediaQueryList matchMedia(String? query) => _matchMedia(query?.toJS); - - /// Returns the [DomNavigator] associated with this window. - external DomNavigator get navigator; - - /// Gets the current selection. - external DomSelection? getSelection(); -} - -/// The underyling window. -@JS('window') -external DomWindow get domWindow; - -/// [DomMediaQueryList] interop object. -@JS() -@staticInterop -class DomMediaQueryList {} - -/// [DomMediaQueryList] required extension. -extension DomMediaQueryListExtension on DomMediaQueryList { - @JS('matches') - external JSBoolean get _matches; - - /// Whether or not the query matched. - bool get matches => _matches.toDart; -} - -/// [DomNavigator] interop object. -@JS() -@staticInterop -class DomNavigator {} - -/// [DomNavigator] required extension. -extension DomNavigatorExtension on DomNavigator { - @JS('platform') - external JSString? get _platform; - - /// The underyling platform string. - String? get platform => _platform?.toDart; -} - -/// A DOM event target. -@JS() -@staticInterop -class DomEventTarget {} - -/// [DomEventTarget]'s required extension. -extension DomEventTargetExtension on DomEventTarget { - @JS('addEventListener') - external JSVoid _addEventListener1(JSString type, DomEventListener? listener); - - @JS('addEventListener') - external JSVoid _addEventListener2( - JSString type, DomEventListener? listener, JSBoolean useCapture); - - /// Adds an event listener to this event target. - @JS('addEventListener') - void addEventListener(String type, DomEventListener? listener, - [bool? useCapture]) { - if (listener != null) { - if (useCapture == null) { - _addEventListener1(type.toJS, listener); - } else { - _addEventListener2(type.toJS, listener, useCapture.toJS); - } - } - } -} - -/// [DomXMLHttpRequest] interop class. -@JS('XMLHttpRequest') -@staticInterop -class DomXMLHttpRequest extends DomEventTarget { - /// Constructor for [DomXMLHttpRequest]. - external factory DomXMLHttpRequest(); -} - -/// [DomXMLHttpRequest] extension. -extension DomXMLHttpRequestExtension on DomXMLHttpRequest { - /// Gets the response. - external JSAny? get response; - - @JS('responseText') - external JSString? get _responseText; - - /// Gets the response text. - String? get responseText => _responseText?.toDart; - - @JS('responseType') - external JSString get _responseType; - - /// Gets the response type. - String get responseType => _responseType.toDart; - - @JS('status') - external JSNumber? get _status; - - /// Gets the status. - int? get status => _status?.toDart.toInt(); - - @JS('responseType') - external set _responseType(JSString value); - - /// Set the response type. - set responseType(String value) => _responseType = value.toJS; - - @JS('setRequestHeader') - external void _setRequestHeader(JSString header, JSString value); - - /// Set the request header. - void setRequestHeader(String header, String value) => - _setRequestHeader(header.toJS, value.toJS); - - @JS('open') - external JSVoid _open(JSString method, JSString url, JSBoolean isAsync); - - /// Open the request. - void open(String method, String url, bool isAsync) => - _open(method.toJS, url.toJS, isAsync.toJS); - - /// Send the request. - external JSVoid send(); -} - -/// Type for event listener. -typedef DartDomEventListener = JSVoid Function(DomEvent event); - -/// The type of [JSFunction] expected as an `EventListener`. -@JS() -@staticInterop -class DomEventListener {} - -/// Creates a [DomEventListener] from a [DartDomEventListener]. -DomEventListener createDomEventListener(DartDomEventListener listener) => - listener.toJS as DomEventListener; - -/// [DomEvent] interop object. -@JS() -@staticInterop -class DomEvent {} - -/// [DomEvent] required extension. -extension DomEventExtension on DomEvent { - @JS('type') - external JSString get _type; - - /// Get the event type. - String get type => _type.toDart; - - /// Initialize an event. - external JSVoid initEvent( - JSString type, JSBoolean bubbles, JSBoolean cancelable); -} - -/// [DomProgressEvent] interop object. -@JS() -@staticInterop -class DomProgressEvent extends DomEvent {} - -/// [DomProgressEvent] required extension. -extension DomProgressEventExtension on DomProgressEvent { - @JS('loaded') - external JSNumber? get _loaded; - - /// Amount of work done. - int? get loaded => _loaded?.toDart.toInt(); - - @JS('total') - external JSNumber? get _total; - - /// Total amount of work. - int? get total => _total?.toDart.toInt(); -} - -/// The underlying DOM document. -@JS() -@staticInterop -class DomDocument {} - -/// [DomDocument]'s required extension. -extension DomDocumentExtension on DomDocument { - @JS('createEvent') - external DomEvent _createEvent(JSString eventType); - - /// Creates an event. - DomEvent createEvent(String eventType) => _createEvent(eventType.toJS); - - /// Creates a range. - external DomRange createRange(); - - /// Gets the head element. - external DomHTMLHeadElement? get head; - - /// Creates a [DomElement]. - @JS('createElement') - external DomElement createElement(JSString name); -} - -/// Returns the top level document. -@JS('window.document') -external DomDocument get domDocument; - -/// Creates a new DOM event. -DomEvent createDomEvent(String type, String name) { - final DomEvent event = domDocument.createEvent(type); - event.initEvent(name.toJS, true.toJS, true.toJS); - return event; -} - -/// A Range object. -@JS() -@staticInterop -class DomRange {} - -/// [DomRange]'s required extension. -extension DomRangeExtension on DomRange { - /// Selects the provided node. - external JSVoid selectNode(DomNode node); -} - -/// A node in the DOM. -@JS() -@staticInterop -class DomNode extends DomEventTarget {} - -/// [DomNode]'s required extension. -extension DomNodeExtension on DomNode { - @JS('innerText') - external set _innerText(JSString text); - - /// Sets the innerText of this node. - set innerText(String text) => _innerText = text.toJS; - - /// Appends a node this node. - external JSVoid append(DomNode node); -} - -/// An element in the DOM. -@JS() -@staticInterop -class DomElement extends DomNode {} - -/// [DomElement]'s required extension. -extension DomElementExtension on DomElement { - /// Returns the style of this element. - external DomCSSStyleDeclaration get style; - - /// Returns the class list of this element. - external DomTokenList get classList; -} - -/// An HTML element in the DOM. -@JS() -@staticInterop -class DomHTMLElement extends DomElement {} - -/// A UI event. -@JS() -@staticInterop -class DomUIEvent extends DomEvent {} - -/// A mouse event. -@JS() -@staticInterop -class DomMouseEvent extends DomUIEvent {} - -/// [DomMouseEvent]'s required extension. -extension DomMouseEventExtension on DomMouseEvent { - @JS('offsetX') - external JSNumber get _offsetX; - - /// Returns the current x offset. - num get offsetX => _offsetX.toDart; - - @JS('offsetY') - external JSNumber get _offsetY; - - /// Returns the current y offset. - num get offsetY => _offsetY.toDart; - - @JS('button') - external JSNumber get _button; - - /// Returns the current button. - int get button => _button.toDart.toInt(); -} - -/// A DOM selection. -@JS() -@staticInterop -class DomSelection {} - -/// [DomSelection]'s required extension. -extension DomSelectionExtension on DomSelection { - /// Removes all ranges from this selection. - external JSVoid removeAllRanges(); - - /// Adds a range to this selection. - external JSVoid addRange(DomRange range); -} - -/// A DOM html div element. -@JS() -@staticInterop -class DomHTMLDivElement extends DomHTMLElement {} - -/// Factory constructor for [DomHTMLDivElement]. -DomHTMLDivElement createDomHTMLDivElement() => - domDocument.createElement('div'.toJS) as DomHTMLDivElement; - -/// An html style element. -@JS() -@staticInterop -class DomHTMLStyleElement extends DomHTMLElement {} - -/// [DomHTMLStyleElement]'s required extension. -extension DomHTMLStyleElementExtension on DomHTMLStyleElement { - /// Get's the style sheet of this element. - external DomStyleSheet? get sheet; -} - -/// Factory constructor for [DomHTMLStyleElement]. -DomHTMLStyleElement createDomHTMLStyleElement() => - domDocument.createElement('style'.toJS) as DomHTMLStyleElement; - -/// CSS styles. -@JS() -@staticInterop -class DomCSSStyleDeclaration {} - -/// [DomCSSStyleDeclaration]'s required extension. -extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { - /// Sets the width. - set width(String value) => setProperty('width', value); - - /// Sets the height. - set height(String value) => setProperty('height', value); - - @JS('setProperty') - external JSVoid _setProperty( - JSString propertyName, JSString value, JSString priority); - - /// Sets a CSS property by name. - void setProperty(String propertyName, String value, [String? priority]) { - priority ??= ''; - _setProperty(propertyName.toJS, value.toJS, priority.toJS); - } -} - -/// The HTML head element. -@JS() -@staticInterop -class DomHTMLHeadElement extends DomHTMLElement {} - -/// A DOM style sheet. -@JS() -@staticInterop -class DomStyleSheet {} - -/// A DOM CSS style sheet. -@JS() -@staticInterop -class DomCSSStyleSheet extends DomStyleSheet {} - -/// [DomCSSStyleSheet]'s required extension. -extension DomCSSStyleSheetExtension on DomCSSStyleSheet { - @JS('insertRule') - external JSNumber _insertRule1(JSString rule); - - @JS('insertRule') - external JSNumber _insertRule2(JSString rule, JSNumber index); - - /// Inserts a rule into this style sheet. - int insertRule(String rule, [int? index]) { - if (index == null) { - return _insertRule1(rule.toJS).toDart.toInt(); - } else { - return _insertRule2(rule.toJS, index.toDouble().toJS).toDart.toInt(); - } - } -} - -/// A list of token. -@JS() -@staticInterop -class DomTokenList {} - -/// [DomTokenList]'s required extension. -extension DomTokenListExtension on DomTokenList { - @JS('add') - external JSVoid _add(JSString value); - - /// Adds a token to this token list. - void add(String value) => _add(value.toJS); -} diff --git a/packages/flutter/lib/src/services/flavor.dart b/packages/flutter/lib/src/services/flavor.dart new file mode 100644 index 0000000000000..292c3b37c43e8 --- /dev/null +++ b/packages/flutter/lib/src/services/flavor.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The flavor this app was built with. +/// +/// This is equivalent to the value argued to the `--flavor` option at build time. +/// This will be `null` if the `--flavor` option was not provided. +const String? appFlavor = String.fromEnvironment('FLUTTER_APP_FLAVOR') != '' ? + String.fromEnvironment('FLUTTER_APP_FLAVOR') : null; diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart index 9f12f723c6d38..ee994306f41bd 100644 --- a/packages/flutter/lib/src/services/hardware_keyboard.dart +++ b/packages/flutter/lib/src/services/hardware_keyboard.dart @@ -532,6 +532,10 @@ class HardwareKeyboard { } /// Query the engine and update _pressedKeys accordingly to the engine answer. + // + /// Both the framework and the engine maintain a state of the current pressed + /// keys. There are edge cases, related to startup and restart, where the framework + /// needs to resynchronize its keyboard state. Future syncKeyboardState() async { final Map? keyboardState = await SystemChannels.keyboard.invokeMapMethod( 'getKeyboardState', @@ -788,14 +792,42 @@ typedef KeyMessageHandler = bool Function(KeyMessage message); /// and [RawKeyboard] for recording keeping, and then dispatches the [KeyMessage] /// to [keyMessageHandler], the global message handler. /// -/// [KeyEventManager] also resolves cross-platform compatibility of keyboard -/// implementations. Legacy platforms might have not implemented the new key -/// data API and only send raw key data on each key message. [KeyEventManager] -/// recognize platform types as [KeyDataTransitMode] and dispatches events in -/// different ways accordingly. -/// /// [KeyEventManager] is typically created, owned, and invoked by /// [ServicesBinding]. +/// +/// ## On embedder implementation +/// +/// Currently, Flutter has two sets of key event APIs running in parallel. +/// +/// * The legacy "raw key event" route receives messages from the +/// "flutter/keyevent" message channel ([SystemChannels.keyEvent]) and +/// dispatches [RawKeyEvent] to [RawKeyboard] and [Focus.onKey] as well as +/// similar methods. +/// * The newer "hardware key event" route receives messages from the +/// "flutter/keydata" message channel (embedder API +/// `FlutterEngineSendKeyEvent`) and dispatches [KeyEvent] to +/// [HardwareKeyboard] and some methods such as [Focus.onKeyEvent]. +/// +/// [KeyEventManager] resolves cross-platform compatibility of keyboard +/// implementations, since legacy platforms might have not implemented the new +/// key data API and only send raw key data on each key message. +/// [KeyEventManager] recognizes the platform support by detecting whether a +/// message comes from platform channel "flutter/keyevent" before one from +/// "flutter/keydata", or vice versa, at the beginning of the app. +/// +/// * If a "flutter/keyevent" message is received first, then this platform is +/// considered a legacy platform. The raw key event is transformed into a +/// hardware key event at best effort. No messages from "flutter/keydata" are +/// expected. +/// * If a "flutter/keydata" message is received first, then this platform is +/// considered a newer platform. The hardware key events are stored, and +/// dispatched only when a raw key message is received. +/// +/// Therefore, to correctly implement a platform that supports +/// `FlutterEngineSendKeyEvent`, the platform must ensure that +/// `FlutterEngineSendKeyEvent` is called before sending a message to +/// "flutter/keyevent" at the beginning of the app, and every physical key event +/// is ended with a "flutter/keyevent" message. class KeyEventManager { /// Create an instance. /// diff --git a/packages/flutter/lib/src/services/message_codec.dart b/packages/flutter/lib/src/services/message_codec.dart index cc2efd41a3d47..d9b87721e6f03 100644 --- a/packages/flutter/lib/src/services/message_codec.dart +++ b/packages/flutter/lib/src/services/message_codec.dart @@ -30,6 +30,7 @@ abstract class MessageCodec { } /// A command object representing the invocation of a named method. +@pragma('vm:keep-name') @immutable class MethodCall { /// Creates a [MethodCall] representing the invocation of [method] with the diff --git a/packages/flutter/lib/src/services/message_codecs.dart b/packages/flutter/lib/src/services/message_codecs.dart index fcd90731caf10..2b103d9624cc5 100644 --- a/packages/flutter/lib/src/services/message_codecs.dart +++ b/packages/flutter/lib/src/services/message_codecs.dart @@ -50,7 +50,7 @@ class StringCodec implements MessageCodec { if (message == null) { return null; } - return utf8.decoder.convert(message.buffer.asUint8List(message.offsetInBytes, message.lengthInBytes)); + return utf8.decode(Uint8List.sublistView(message)); } @override @@ -58,8 +58,7 @@ class StringCodec implements MessageCodec { if (message == null) { return null; } - final Uint8List encoded = utf8.encoder.convert(message); - return encoded.buffer.asByteData(); + return ByteData.sublistView(utf8.encode(message)); } } @@ -415,7 +414,7 @@ class StandardMessageCodec implements MessageCodec { if (char <= 0x7f) { asciiBytes[i] = char; } else { - utf8Bytes = utf8.encoder.convert(value.substring(i)); + utf8Bytes = utf8.encode(value.substring(i)); utf8Offset = i; break; } diff --git a/packages/flutter/lib/src/services/mouse_cursor.dart b/packages/flutter/lib/src/services/mouse_cursor.dart index c698aa0b0bc97..a9cd9e94231a7 100644 --- a/packages/flutter/lib/src/services/mouse_cursor.dart +++ b/packages/flutter/lib/src/services/mouse_cursor.dart @@ -102,8 +102,6 @@ class MouseCursorManager { /// will no longer be used in the future. abstract class MouseCursorSession { /// Create a session. - /// - /// All arguments must be non-null. MouseCursorSession(this.cursor, this.device); /// The cursor that created this session. @@ -207,7 +205,7 @@ abstract class MouseCursor with Diagnosticable { /// to make debug information more readable. It is returned as the [toString] /// when the diagnostic level is at or above [DiagnosticLevel.info]. /// - /// The [debugDescription] must not be null or empty string. + /// The [debugDescription] must not be empty. String get debugDescription; @override diff --git a/packages/flutter/lib/src/services/mouse_tracking.dart b/packages/flutter/lib/src/services/mouse_tracking.dart index e0c405b90db38..a6a066e48286c 100644 --- a/packages/flutter/lib/src/services/mouse_tracking.dart +++ b/packages/flutter/lib/src/services/mouse_tracking.dart @@ -44,8 +44,6 @@ typedef PointerHoverEventListener = void Function(PointerHoverEvent event); /// * [MouseTracker], which uses [MouseTrackerAnnotation]. class MouseTrackerAnnotation with Diagnosticable { /// Creates an immutable [MouseTrackerAnnotation]. - /// - /// All arguments are optional. The [cursor] must not be null. const MouseTrackerAnnotation({ this.onEnter, this.onExit, diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index b794253dd1834..cba9e1654d4a0 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -12,7 +12,6 @@ import '_background_isolate_binary_messenger_io.dart' import 'binary_messenger.dart'; import 'binding.dart'; -import 'debug.dart' show debugProfilePlatformChannels; import 'message_codec.dart'; import 'message_codecs.dart'; @@ -23,9 +22,21 @@ export 'binary_messenger.dart' show BinaryMessenger; export 'binding.dart' show RootIsolateToken; export 'message_codec.dart' show MessageCodec, MethodCall, MethodCodec; -bool _debugProfilePlatformChannelsIsRunning = false; -const Duration _debugProfilePlatformChannelsRate = Duration(seconds: 1); -final Expando _debugBinaryMessengers = Expando(); +/// Profile and print statistics on Platform Channel usage. +/// +/// When this is true statistics about the usage of Platform Channels will be +/// printed out periodically to the console and Timeline events will show the +/// time between sending and receiving a message (encoding and decoding time +/// excluded). +/// +/// The statistics include the total bytes transmitted and the average number of +/// bytes per invocation in the last quantum. "Up" means in the direction of +/// Flutter to the host platform, "down" is the host platform to flutter. +const bool kProfilePlatformChannels = false; + +bool _profilePlatformChannelsIsRunning = false; +const Duration _profilePlatformChannelsRate = Duration(seconds: 1); +final Expando _profiledBinaryMessengers = Expando(); class _ProfiledBinaryMessenger implements BinaryMessenger { const _ProfiledBinaryMessenger(this.proxy, this.channelTypeName, this.codecTypeName); @@ -39,14 +50,13 @@ class _ProfiledBinaryMessenger implements BinaryMessenger { } Future? sendWithPostfix(String channel, String postfix, ByteData? message) async { - final TimelineTask task = TimelineTask(); _debugRecordUpStream(channelTypeName, '$channel$postfix', codecTypeName, message); - task.start('Platform Channel send $channel$postfix'); + final TimelineTask timelineTask = TimelineTask()..start('Platform Channel send $channel$postfix'); final ByteData? result; try { result = await proxy.send(channel, message); } finally { - task.finish(); + timelineTask.finish(); } _debugRecordDownStream(channelTypeName, '$channel$postfix', codecTypeName, result); return result; @@ -89,17 +99,17 @@ class _PlatformChannelStats { double get averageDownPayload => _downBytes / _downCount; } -final Map _debugProfilePlatformChannelsStats = {}; +final Map _profilePlatformChannelsStats = {}; Future _debugLaunchProfilePlatformChannels() async { - if (!_debugProfilePlatformChannelsIsRunning) { - _debugProfilePlatformChannelsIsRunning = true; - await Future.delayed(_debugProfilePlatformChannelsRate); - _debugProfilePlatformChannelsIsRunning = false; + if (!_profilePlatformChannelsIsRunning) { + _profilePlatformChannelsIsRunning = true; + await Future.delayed(_profilePlatformChannelsRate); + _profilePlatformChannelsIsRunning = false; final StringBuffer log = StringBuffer(); log.writeln('Platform Channel Stats:'); final List<_PlatformChannelStats> allStats = - _debugProfilePlatformChannelsStats.values.toList(); + _profilePlatformChannelsStats.values.toList(); // Sort highest combined bandwidth first. allStats.sort((_PlatformChannelStats x, _PlatformChannelStats y) => (y.upBytes + y.downBytes) - (x.upBytes + x.downBytes)); @@ -108,14 +118,14 @@ Future _debugLaunchProfilePlatformChannels() async { ' (name:"${stats.channel}" type:"${stats.type}" codec:"${stats.codec}" upBytes:${stats.upBytes} upBytes_avg:${stats.averageUpPayload.toStringAsFixed(1)} downBytes:${stats.downBytes} downBytes_avg:${stats.averageDownPayload.toStringAsFixed(1)})'); } debugPrint(log.toString()); - _debugProfilePlatformChannelsStats.clear(); + _profilePlatformChannelsStats.clear(); } } void _debugRecordUpStream(String channelTypeName, String name, String codecTypeName, ByteData? bytes) { final _PlatformChannelStats stats = - _debugProfilePlatformChannelsStats[name] ??= + _profilePlatformChannelsStats[name] ??= _PlatformChannelStats(name, codecTypeName, channelTypeName); stats.addUpStream(bytes?.lengthInBytes ?? 0); _debugLaunchProfilePlatformChannels(); @@ -124,7 +134,7 @@ void _debugRecordUpStream(String channelTypeName, String name, void _debugRecordDownStream(String channelTypeName, String name, String codecTypeName, ByteData? bytes) { final _PlatformChannelStats stats = - _debugProfilePlatformChannelsStats[name] ??= + _profilePlatformChannelsStats[name] ??= _PlatformChannelStats(name, codecTypeName, channelTypeName); stats.addDownStream(bytes?.lengthInBytes ?? 0); _debugLaunchProfilePlatformChannels(); @@ -158,10 +168,11 @@ BinaryMessenger _findBinaryMessenger() { /// /// See: class BasicMessageChannel { - /// Creates a [BasicMessageChannel] with the specified [name], [codec] and [binaryMessenger]. + /// Creates a [BasicMessageChannel] with the specified [name], [codec] and + /// [binaryMessenger]. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The default [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null. const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger }) : _binaryMessenger = binaryMessenger; @@ -179,8 +190,8 @@ class BasicMessageChannel { /// [BackgroundIsolateBinaryMessenger.ensureInitialized]. BinaryMessenger get binaryMessenger { final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger(); - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( + return kProfilePlatformChannels + ? _profiledBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring result, runtimeType.toString(), codec.runtimeType.toString()) : result; @@ -242,14 +253,15 @@ class BasicMessageChannel { /// {@endtemplate} /// /// See: +@pragma('vm:keep-name') class MethodChannel { /// Creates a [MethodChannel] with the specified [name]. /// /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The default [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null. const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ]) : _binaryMessenger = binaryMessenger; @@ -267,8 +279,8 @@ class MethodChannel { /// [BackgroundIsolateBinaryMessenger.ensureInitialized]. BinaryMessenger get binaryMessenger { final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger(); - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( + return kProfilePlatformChannels + ? _profiledBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring result, runtimeType.toString(), codec.runtimeType.toString()) : result; @@ -298,7 +310,7 @@ class MethodChannel { Future _invokeMethod(String method, { required bool missingOk, dynamic arguments }) async { final ByteData input = codec.encodeMethodCall(MethodCall(method, arguments)); final ByteData? result = - !kReleaseMode && debugProfilePlatformChannels ? + kProfilePlatformChannels ? await (binaryMessenger as _ProfiledBinaryMessenger).sendWithPostfix(name, '#$method', input) : await binaryMessenger.send(name, input); if (result == null) { diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index a024d6a7642eb..bb3c19f756231 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -58,7 +58,7 @@ class PlatformViewsRegistry { /// Callback signature for when a platform view was created. /// -/// `id` is the platform view's unique identifier. +/// The `id` parameter is the platform view's unique identifier. typedef PlatformViewCreatedCallback = void Function(int id); /// Provides access to the platform views service. @@ -92,28 +92,33 @@ class PlatformViewsService { /// {@template flutter.services.PlatformViewsService.initAndroidView} /// Creates a controller for a new Android view. /// - /// `id` is an unused unique identifier generated with [platformViewsRegistry]. + /// The `id` argument is an unused unique identifier generated with + /// [platformViewsRegistry]. /// - /// `viewType` is the identifier of the Android view type to be created, a - /// factory for this view type must have been registered on the platform side. - /// Platform view factories are typically registered by plugin code. - /// Plugins can register a platform view factory with + /// The `viewType` argument is the identifier of the Android view type to be + /// created, a factory for this view type must have been registered on the + /// platform side. Platform view factories are typically registered by plugin + /// code. Plugins can register a platform view factory with /// [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-). /// - /// `creationParams` will be passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-) + /// The `creationParams` argument will be passed as the args argument of + /// [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-) /// - /// `creationParamsCodec` is the codec used to encode `creationParams` before sending it to the - /// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). - /// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec]. + /// The `creationParamsCodec` argument is the codec used to encode + /// `creationParams` before sending it to the platform side. It should match + /// the codec passed to the constructor of + /// [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). + /// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], + /// [StringCodec], or [BinaryCodec]. /// - /// `onFocus` is a callback that will be invoked when the Android View asks to get the - /// input focus. + /// The `onFocus` argument is a callback that will be invoked when the Android + /// View asks to get the input focus. /// - /// The Android view will only be created after [AndroidViewController.setSize] is called for the - /// first time. + /// The Android view will only be created after + /// [AndroidViewController.setSize] is called for the first time. /// - /// The `id, `viewType, and `layoutDirection` parameters must not be null. - /// If `creationParams` is non null then `creationParamsCodec` must not be null. + /// If `creationParams` is non null then `creationParamsCodec` must not be + /// null. /// {@endtemplate} /// /// This attempts to use the newest and most efficient platform view @@ -196,26 +201,21 @@ class PlatformViewsService { return controller; } - /// Whether the render surface of the Android `FlutterView` should be converted to a `FlutterImageView`. - @Deprecated( - 'No longer necessary to improve performance. ' - 'This feature was deprecated after v2.11.0-0.1.pre.', - ) - static Future synchronizeToNativeViewHierarchy(bool yes) async {} - - // TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands. - /// This is work in progress, not yet ready to be used, and requires a custom engine build. Creates a controller for a new iOS UIView. + /// Factory method to create a `UiKitView`. + /// + /// The `id` parameter is an unused unique identifier generated with + /// [platformViewsRegistry]. /// - /// `id` is an unused unique identifier generated with [platformViewsRegistry]. + /// The `viewType` parameter is the identifier of the iOS view type to be + /// created, a factory for this view type must have been registered on the + /// platform side. Platform view factories are typically registered by plugin + /// code. /// - /// `viewType` is the identifier of the iOS view type to be created, a - /// factory for this view type must have been registered on the platform side. - /// Platform view factories are typically registered by plugin code. + /// The `onFocus` parameter is a callback that will be invoked when the UIKit + /// view asks to get the input focus. If `creationParams` is non null then + /// `creationParamsCodec` must not be null. /// - /// `onFocus` is a callback that will be invoked when the UIKit view asks to - /// get the input focus. - /// The `id, `viewType, and `layoutDirection` parameters must not be null. - /// If `creationParams` is non null then `creationParamsCodec` must not be null. + /// See: https://docs.flutter.dev/platform-integration/ios/platform-views static Future initUiKitView({ required int id, required String viewType, @@ -227,6 +227,7 @@ class PlatformViewsService { assert(creationParams == null || creationParamsCodec != null); // TODO(amirh): pass layoutDirection once the system channel supports it. + // https://github.com/flutter/flutter/issues/133682 final Map args = { 'id': id, 'viewType': viewType, @@ -245,6 +246,52 @@ class PlatformViewsService { } return UiKitViewController._(id, layoutDirection); } + + // TODO(cbracken): Write and link website docs. https://github.com/flutter/website/issues/9424. + // + /// Factory method to create an `AppKitView`. + /// + /// The `id` parameter is an unused unique identifier generated with + /// [platformViewsRegistry]. + /// + /// The `viewType` parameter is the identifier of the iOS view type to be + /// created, a factory for this view type must have been registered on the + /// platform side. Platform view factories are typically registered by plugin + /// code. + /// + /// The `onFocus` parameter is a callback that will be invoked when the UIKit + /// view asks to get the input focus. If `creationParams` is non null then + /// `creationParamsCodec` must not be null. + static Future initAppKitView({ + required int id, + required String viewType, + required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec? creationParamsCodec, + VoidCallback? onFocus, + }) async { + assert(creationParams == null || creationParamsCodec != null); + + // TODO(amirh): pass layoutDirection once the system channel supports it. + // https://github.com/flutter/flutter/issues/133682 + final Map args = { + 'id': id, + 'viewType': viewType, + }; + if (creationParams != null) { + final ByteData paramsByteData = creationParamsCodec!.encodeMessage(creationParams)!; + args['params'] = Uint8List.view( + paramsByteData.buffer, + 0, + paramsByteData.lengthInBytes, + ); + } + await SystemChannels.platform_views.invokeMethod('create', args); + if (onFocus != null) { + _instance._focusCallbacks[id] = onFocus; + } + return AppKitViewController._(id, layoutDirection); + } } /// Properties of an Android pointer. @@ -252,8 +299,6 @@ class PlatformViewsService { /// A Dart version of Android's [MotionEvent.PointerProperties](https://developer.android.com/reference/android/view/MotionEvent.PointerProperties). class AndroidPointerProperties { /// Creates an [AndroidPointerProperties] object. - /// - /// All parameters must not be null. const AndroidPointerProperties({ required this.id, required this.toolType, @@ -294,8 +339,6 @@ class AndroidPointerProperties { /// A Dart version of Android's [MotionEvent.PointerCoords](https://developer.android.com/reference/android/view/MotionEvent.PointerCoords). class AndroidPointerCoords { /// Creates an AndroidPointerCoords. - /// - /// All parameters must not be null. const AndroidPointerCoords({ required this.orientation, required this.pressure, @@ -376,8 +419,6 @@ class AndroidPointerCoords { /// * [AndroidViewController.sendMotionEvent], which can be used to send an [AndroidMotionEvent] explicitly. class AndroidMotionEvent { /// Creates an AndroidMotionEvent. - /// - /// All parameters must not be null. AndroidMotionEvent({ required this.downTime, required this.eventTime, @@ -770,8 +811,8 @@ abstract class AndroidViewController extends PlatformViewController { /// Sizes the Android View. /// - /// [size] is the view's new size in logical pixel, it must not be null and must - /// be bigger than zero. + /// [size] is the view's new size in logical pixel. It must be greater than + /// zero. /// /// The first time a size is set triggers the creation of the Android view. /// @@ -1313,16 +1354,17 @@ class _HybridAndroidViewControllerInternals extends _AndroidViewControllerIntern } } -/// Controls an iOS UIView. +/// Base class for iOS and macOS view controllers. /// -/// Typically created with [PlatformViewsService.initUiKitView]. -class UiKitViewController { - UiKitViewController._( +/// View controllers are used to create and interact with the UIView or NSView +/// underlying a platform view. +abstract class DarwinPlatformViewController { + /// Public default for subclasses to override. + DarwinPlatformViewController( this.id, TextDirection layoutDirection, ) : _layoutDirection = layoutDirection; - /// The unique identifier of the iOS view controlled by this controller. /// /// This identifier is typically generated by @@ -1382,6 +1424,26 @@ class UiKitViewController { } } +/// Controller for an iOS platform view. +/// +/// View controllers create and interact with the underlying UIView. +/// +/// Typically created with [PlatformViewsService.initUiKitView]. +class UiKitViewController extends DarwinPlatformViewController { + UiKitViewController._( + super.id, + super.layoutDirection, + ); +} + +/// Controller for a macOS platform view. +class AppKitViewController extends DarwinPlatformViewController { + AppKitViewController._( + super.id, + super.layoutDirection, + ); +} + /// An interface for controlling a single platform view. /// /// Used by [PlatformViewSurface] to interface with the platform view it embeds. diff --git a/packages/flutter/lib/src/services/raw_keyboard_android.dart b/packages/flutter/lib/src/services/raw_keyboard_android.dart index 4de79dad9567e..c6f07ed11f3bf 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_android.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_android.dart @@ -27,9 +27,6 @@ const int _kCombiningCharacterMask = 0x7fffffff; /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataAndroid extends RawKeyEventData { /// Creates a key event data structure specific for Android. - /// - /// The [flags], [codePoint], [keyCode], [scanCode], and [metaState] arguments - /// must not be null. const RawKeyEventDataAndroid({ this.flags = 0, this.codePoint = 0, diff --git a/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart b/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart index 3f74d08400518..3331a78dd70ca 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_fuchsia.dart @@ -22,8 +22,6 @@ export 'raw_keyboard.dart' show KeyboardSide, ModifierKey; /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataFuchsia extends RawKeyEventData { /// Creates a key event data structure specific for Fuchsia. - /// - /// The [hidUsage], [codePoint], and [modifiers] arguments must not be null. const RawKeyEventDataFuchsia({ this.hidUsage = 0, this.codePoint = 0, diff --git a/packages/flutter/lib/src/services/raw_keyboard_ios.dart b/packages/flutter/lib/src/services/raw_keyboard_ios.dart index 841d8425b6cdf..74cded59a6a9b 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_ios.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_ios.dart @@ -22,9 +22,6 @@ export 'raw_keyboard.dart' show KeyboardSide, ModifierKey; /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataIos extends RawKeyEventData { /// Creates a key event data structure specific for iOS. - /// - /// The [characters], [charactersIgnoringModifiers], and [modifiers], arguments - /// must not be null. const RawKeyEventDataIos({ this.characters = '', this.charactersIgnoringModifiers = '', diff --git a/packages/flutter/lib/src/services/raw_keyboard_linux.dart b/packages/flutter/lib/src/services/raw_keyboard_linux.dart index 81c128d0a3746..d01a9a8e6d1ab 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_linux.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_linux.dart @@ -22,9 +22,6 @@ export 'raw_keyboard.dart' show KeyboardSide, ModifierKey; /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataLinux extends RawKeyEventData { /// Creates a key event data structure specific for Linux. - /// - /// The [keyHelper], [scanCode], [unicodeScalarValues], [keyCode], and [modifiers], - /// arguments must not be null. const RawKeyEventDataLinux({ required this.keyHelper, this.unicodeScalarValues = 0, diff --git a/packages/flutter/lib/src/services/raw_keyboard_macos.dart b/packages/flutter/lib/src/services/raw_keyboard_macos.dart index 4f33c8617b842..1d0476d803224 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_macos.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_macos.dart @@ -33,9 +33,6 @@ int runeToLowerCase(int rune) { /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataMacOs extends RawKeyEventData { /// Creates a key event data structure specific for macOS. - /// - /// The [characters], [charactersIgnoringModifiers], and [modifiers], arguments - /// must not be null. const RawKeyEventDataMacOs({ this.characters = '', this.charactersIgnoringModifiers = '', diff --git a/packages/flutter/lib/src/services/raw_keyboard_web.dart b/packages/flutter/lib/src/services/raw_keyboard_web.dart index 8cc289b79d2ff..c3d114e706d01 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_web.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_web.dart @@ -27,8 +27,6 @@ String? _unicodeChar(String key) { @immutable class RawKeyEventDataWeb extends RawKeyEventData { /// Creates a key event data structure specific for Web. - /// - /// The [code] and [metaState] arguments must not be null. const RawKeyEventDataWeb({ required this.code, required this.key, diff --git a/packages/flutter/lib/src/services/raw_keyboard_windows.dart b/packages/flutter/lib/src/services/raw_keyboard_windows.dart index 2c2376a38ee43..3110b5aebcfe2 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_windows.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_windows.dart @@ -27,9 +27,6 @@ const int _vkProcessKey = 0xe5; /// * [RawKeyboard], which uses this interface to expose key data. class RawKeyEventDataWindows extends RawKeyEventData { /// Creates a key event data structure specific for Windows. - /// - /// The [keyCode], [scanCode], [characterCodePoint], and [modifiers], arguments - /// must not be null. const RawKeyEventDataWindows({ this.keyCode = 0, this.scanCode = 0, diff --git a/packages/flutter/lib/src/services/restoration.dart b/packages/flutter/lib/src/services/restoration.dart index b55c438f44345..694f2f63ce5ba 100644 --- a/packages/flutter/lib/src/services/restoration.dart +++ b/packages/flutter/lib/src/services/restoration.dart @@ -92,7 +92,7 @@ typedef _BucketVisitor = void Function(RestorationBucket bucket); /// ## State Restoration on iOS /// /// To enable state restoration on iOS, a restoration identifier has to be -/// assigned to the [FlutterViewController](https://api.flutter.dev/objcdoc/Classes/FlutterViewController.html). +/// assigned to the [FlutterViewController](/ios-embedder/interface_flutter_view_controller.html). /// If the standard embedding (produced by `flutter create`) is used, this can /// be accomplished with the following steps: /// @@ -154,6 +154,9 @@ class RestorationManager extends ChangeNotifier { /// Construct the restoration manager and set up the communications channels /// with the engine to get restoration messages (by calling [initChannels]). RestorationManager() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } initChannels(); } @@ -492,8 +495,6 @@ class RestorationBucket { /// claiming a child from a parent via [claimChild]. If no parent bucket is /// available, [RestorationManager.rootBucket] may be used as a parent. /// {@endtemplate} - /// - /// The `restorationId` must not be null. RestorationBucket.empty({ required String restorationId, required Object? debugOwner, @@ -526,8 +527,6 @@ class RestorationBucket { /// ``` /// /// {@macro flutter.services.RestorationBucket.empty.bucketCreation} - /// - /// The `manager` argument must not be null. RestorationBucket.root({ required RestorationManager manager, required Map? rawData, @@ -548,8 +547,6 @@ class RestorationBucket { /// [RestorationBucket.empty] and have the parent adopt it via [adoptChild]. /// /// {@macro flutter.services.RestorationBucket.empty.bucketCreation} - /// - /// The `restorationId` and `parent` argument must not be null. RestorationBucket.child({ required String restorationId, required RestorationBucket parent, diff --git a/packages/flutter/lib/src/services/system_channels.dart b/packages/flutter/lib/src/services/system_channels.dart index 3a65302a2d1e1..5311d36c7c4c2 100644 --- a/packages/flutter/lib/src/services/system_channels.dart +++ b/packages/flutter/lib/src/services/system_channels.dart @@ -503,6 +503,10 @@ abstract final class SystemChannels { /// represents a pressed keyboard key. The entry key is the physical /// key ID and the entry value is the logical key ID. /// + /// Both the framework and the engine maintain a state of the current + /// pressed keys. There are edge cases, related to startup and restart, + /// where the framework needs to resynchronize its keyboard state. + /// /// See also: /// /// * [HardwareKeyboard.syncKeyboardState], which uses this channel to synchronize diff --git a/packages/flutter/lib/src/services/system_chrome.dart b/packages/flutter/lib/src/services/system_chrome.dart index a1d4c5395de88..1ed5a0c88c9d8 100644 --- a/packages/flutter/lib/src/services/system_chrome.dart +++ b/packages/flutter/lib/src/services/system_chrome.dart @@ -368,6 +368,74 @@ abstract final class SystemChrome { /// /// ## Limitations /// + /// ### Android + /// + /// Android screens may choose to [letterbox](https://developer.android.com/guide/practices/enhanced-letterboxing) + /// applications that lock orientation, particularly on larger screens. When + /// letterboxing occurs on Android, the [MediaQueryData.size] reports the + /// letterboxed size, not the full screen size. Applications that make + /// decisions about whether to lock orientation based on the screen size + /// must use the `display` property of the current [FlutterView]. + /// + /// ```dart + /// // A widget that locks the screen to portrait if it is less than 600 + /// // logical pixels wide. + /// class MyApp extends StatefulWidget { + /// const MyApp({ super.key }); + /// + /// @override + /// State createState() => _MyAppState(); + /// } + /// + /// class _MyAppState extends State with WidgetsBindingObserver { + /// ui.FlutterView? _view; + /// static const double kOrientationLockBreakpoint = 600; + /// + /// @override + /// void initState() { + /// super.initState(); + /// WidgetsBinding.instance.addObserver(this); + /// } + /// + /// @override + /// void didChangeDependencies() { + /// super.didChangeDependencies(); + /// _view = View.maybeOf(context); + /// } + /// + /// @override + /// void dispose() { + /// WidgetsBinding.instance.removeObserver(this); + /// _view = null; + /// super.dispose(); + /// } + /// + /// @override + /// void didChangeMetrics() { + /// final ui.Display? display = _view?.display; + /// if (display == null) { + /// return; + /// } + /// if (display.size.width / display.devicePixelRatio < kOrientationLockBreakpoint) { + /// SystemChrome.setPreferredOrientations([ + /// DeviceOrientation.portraitUp, + /// ]); + /// } else { + /// SystemChrome.setPreferredOrientations([]); + /// } + /// } + /// + /// @override + /// Widget build(BuildContext context) { + /// return const MaterialApp( + /// home: Placeholder(), + /// ); + /// } + /// } + /// ``` + /// + /// ### iOS + /// /// This setting will only be respected on iPad if multitasking is disabled. /// /// You can decide to opt out of multitasking on iPad, then diff --git a/packages/flutter/lib/src/services/system_navigator.dart b/packages/flutter/lib/src/services/system_navigator.dart index 9edff64b3cdf8..1ea16f921ac91 100644 --- a/packages/flutter/lib/src/services/system_navigator.dart +++ b/packages/flutter/lib/src/services/system_navigator.dart @@ -2,10 +2,44 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; + import 'system_channels.dart'; /// Controls specific aspects of the system navigation stack. abstract final class SystemNavigator { + /// Informs the platform of whether or not the Flutter framework will handle + /// back events. + /// + /// Currently, this is used only on Android to inform its use of the + /// predictive back gesture when exiting the app. When true, predictive back + /// is disabled. + /// + /// See also: + /// + /// * The + /// [migration guide](https://developer.android.com/guide/navigation/predictive-back-gesture) + /// for predictive back in native Android apps. + static Future setFrameworkHandlesBack(bool frameworkHandlesBack) async { + // Currently, this method call is only relevant on Android. + if (kIsWeb) { + return; + } + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return; + case TargetPlatform.android: + return SystemChannels.platform.invokeMethod( + 'SystemNavigator.setFrameworkHandlesBack', + frameworkHandlesBack, + ); + } + } + /// Removes the topmost Flutter instance, presenting what was before /// it. /// diff --git a/packages/flutter/lib/src/services/text_editing.dart b/packages/flutter/lib/src/services/text_editing.dart index 6dae30a85f15f..d1d91c20ef289 100644 --- a/packages/flutter/lib/src/services/text_editing.dart +++ b/packages/flutter/lib/src/services/text_editing.dart @@ -12,8 +12,6 @@ export 'dart:ui' show TextAffinity, TextPosition; @immutable class TextSelection extends TextRange { /// Creates a text selection. - /// - /// The [baseOffset] and [extentOffset] arguments must not be null. const TextSelection({ required this.baseOffset, required this.extentOffset, @@ -29,8 +27,6 @@ class TextSelection extends TextRange { /// A collapsed selection starts and ends at the same offset, which means it /// contains zero characters but instead serves as an insertion point in the /// text. - /// - /// The [offset] argument must not be null. const TextSelection.collapsed({ required int offset, this.affinity = TextAffinity.downstream, diff --git a/packages/flutter/lib/src/services/text_editing_delta.dart b/packages/flutter/lib/src/services/text_editing_delta.dart index a93c46ad49780..aff975414674a 100644 --- a/packages/flutter/lib/src/services/text_editing_delta.dart +++ b/packages/flutter/lib/src/services/text_editing_delta.dart @@ -56,10 +56,6 @@ bool _debugTextRangeIsValid(TextRange range, String text) { /// to true. abstract class TextEditingDelta with Diagnosticable { /// Creates a delta for a given change to the editing state. - /// - /// {@template flutter.services.TextEditingDelta} - /// The [oldText], [selection], and [composing] arguments must not be null. - /// {@endtemplate} const TextEditingDelta({ required this.oldText, required this.selection, @@ -251,8 +247,6 @@ abstract class TextEditingDelta with Diagnosticable { class TextEditingDeltaInsertion extends TextEditingDelta { /// Creates an insertion delta for a given change to the editing state. /// - /// {@macro flutter.services.TextEditingDelta} - /// /// {@template flutter.services.TextEditingDelta.optIn} /// See also: /// @@ -304,8 +298,6 @@ class TextEditingDeltaInsertion extends TextEditingDelta { class TextEditingDeltaDeletion extends TextEditingDelta { /// Creates a deletion delta for a given change to the editing state. /// - /// {@macro flutter.services.TextEditingDelta} - /// /// {@macro flutter.services.TextEditingDelta.optIn} const TextEditingDeltaDeletion({ required super.oldText, @@ -356,8 +348,6 @@ class TextEditingDeltaReplacement extends TextEditingDelta { /// A replacement can occur in cases such as auto-correct, suggestions, and /// when a selection is replaced by a single character. /// - /// {@macro flutter.services.TextEditingDelta} - /// /// {@macro flutter.services.TextEditingDelta.optIn} const TextEditingDeltaReplacement({ required super.oldText, @@ -413,8 +403,6 @@ class TextEditingDeltaNonTextUpdate extends TextEditingDelta { /// handles. There are no changes to the text, but there are updates to the selection /// and potentially the composing region as well. /// - /// {@macro flutter.services.TextEditingDelta} - /// /// {@macro flutter.services.TextEditingDelta.optIn} const TextEditingDeltaNonTextUpdate({ required super.oldText, diff --git a/packages/flutter/lib/src/services/text_formatter.dart b/packages/flutter/lib/src/services/text_formatter.dart index a822b5c8aea88..92d9570a13f9a 100644 --- a/packages/flutter/lib/src/services/text_formatter.dart +++ b/packages/flutter/lib/src/services/text_formatter.dart @@ -251,6 +251,14 @@ class _TextEditingValueAccumulator { /// As an example, [FilteringTextInputFormatter] typically shouldn't be used /// with [RegExp]s that contain positional matchers (`^` or `$`) since these /// patterns are usually meant for matching the whole string. +/// +/// ### Quote characters on iOS +/// +/// When filtering single (`'`) or double (`"`) quote characters, be aware that +/// the default iOS keyboard actually inserts special directional versions of +/// these characters (`‘` and `’` for single quote, and `“` and `”` for double +/// quote). Consider including all three variants in your regular expressions to +/// support iOS. class FilteringTextInputFormatter extends TextInputFormatter { /// Creates a formatter that replaces banned patterns with the given /// [replacementString]. @@ -263,9 +271,6 @@ class FilteringTextInputFormatter extends TextInputFormatter { /// If [allow] is false, then the filter pattern is a deny list, /// and characters that match the pattern are rejected. See also /// the [FilteringTextInputFormatter.deny] constructor. - /// - /// The [filterPattern], [allow], and [replacementString] arguments - /// must not be null. FilteringTextInputFormatter( this.filterPattern, { required this.allow, @@ -273,18 +278,12 @@ class FilteringTextInputFormatter extends TextInputFormatter { }); /// Creates a formatter that only allows characters matching a pattern. - /// - /// The [filterPattern] and [replacementString] arguments - /// must not be null. FilteringTextInputFormatter.allow( Pattern filterPattern, { String replacementString = '', }) : this(filterPattern, allow: true, replacementString: replacementString); /// Creates a formatter that blocks characters matching a pattern. - /// - /// The [filterPattern] and [replacementString] arguments - /// must not be null. FilteringTextInputFormatter.deny( Pattern filterPattern, { String replacementString = '', diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index f9f475fa1d440..ae7efa8ff006d 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -566,7 +566,7 @@ class TextInputConfiguration { /// [autocorrect], so that suggestions are only shown when [autocorrect] is /// true. On Android autocorrection and suggestion are controlled separately. /// - /// Defaults to true. Cannot be null. + /// Defaults to true. /// /// See also: /// @@ -580,7 +580,7 @@ class TextInputConfiguration { /// change is sent through semantics actions and is directly disabled from /// the widget side. /// - /// Defaults to true. Cannot be null. + /// Defaults to true. final bool enableInteractiveSelection; /// What text to display in the text input control's action button. @@ -612,7 +612,7 @@ class TextInputConfiguration { /// /// This flag only affects Android. On iOS, there is no equivalent flag. /// - /// Defaults to true. Cannot be null. + /// Defaults to true. /// /// See also: /// @@ -684,7 +684,7 @@ class TextInputConfiguration { /// * If [TextInputClient] is implemented then updates for the editing /// state will come through [TextInputClient.updateEditingValue]. /// - /// Defaults to false. Cannot be null. + /// Defaults to false. final bool enableDeltaModel; /// Returns a representation of this object as a JSON object. @@ -762,9 +762,6 @@ class TextEditingValue { /// The selection and composing range must be within the text. This is not /// checked during construction, and must be guaranteed by the caller. /// - /// The [text], [selection], and [composing] arguments must not be null but - /// each have default values. - /// /// The default value of [selection] is `TextSelection.collapsed(offset: -1)`. /// This indicates that there is no selection at all. const TextEditingValue({ @@ -1038,18 +1035,27 @@ mixin TextSelectionDelegate { /// input. void bringIntoView(TextPosition position); - /// Whether cut is enabled, must not be null. + /// Whether cut is enabled. bool get cutEnabled => true; - /// Whether copy is enabled, must not be null. + /// Whether copy is enabled. bool get copyEnabled => true; - /// Whether paste is enabled, must not be null. + /// Whether paste is enabled. bool get pasteEnabled => true; - /// Whether select all is enabled, must not be null. + /// Whether select all is enabled. bool get selectAllEnabled => true; + /// Whether look up is enabled. + bool get lookUpEnabled => true; + + /// Whether search web is enabled. + bool get searchWebEnabled => true; + + /// Whether share is enabled. + bool get shareEnabled => true; + /// Whether Live Text input is enabled. /// /// See also: @@ -1389,8 +1395,8 @@ class TextInputConnection { /// Send the smallest rect that covers the text in the client that's currently /// being composed. /// - /// The given `rect` can not be null. If any of the 4 coordinates of the given - /// [Rect] is not finite, a [Rect] of size (-1, -1) will be sent instead. + /// If any of the 4 coordinates of the given [Rect] is not finite, a [Rect] of + /// size (-1, -1) will be sent instead. /// /// This information is used for positioning the IME candidates menu on each /// platform. diff --git a/packages/flutter/lib/src/widgets/_html_element_view_io.dart b/packages/flutter/lib/src/widgets/_html_element_view_io.dart new file mode 100644 index 0000000000000..e1c23e2d9ebf0 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_html_element_view_io.dart @@ -0,0 +1,34 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: prefer_const_constructors_in_immutables +// ignore_for_file: avoid_unused_constructor_parameters + +import 'framework.dart'; +import 'platform_view.dart'; + +/// The platform-specific implementation of [HtmlElementView]. +extension HtmlElementViewImpl on HtmlElementView { + /// Creates an [HtmlElementView] that renders a DOM element with the given + /// [tagName]. + static HtmlElementView createFromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) { + throw UnimplementedError('HtmlElementView is only available on Flutter Web'); + } + + /// Called from [HtmlElementView.build] to build the widget tree. + /// + /// This is not expected to be invoked in non-web environments. It throws if + /// that happens. + /// + /// The implementation on Flutter Web builds a platform view and handles its + /// lifecycle. + Widget buildImpl(BuildContext context) { + throw UnimplementedError('HtmlElementView is only available on Flutter Web'); + } +} diff --git a/packages/flutter/lib/src/widgets/_html_element_view_web.dart b/packages/flutter/lib/src/widgets/_html_element_view_web.dart new file mode 100644 index 0000000000000..fa7060e6adfe7 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_html_element_view_web.dart @@ -0,0 +1,136 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui_web' as ui_web; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'framework.dart'; +import 'platform_view.dart'; + +/// The platform-specific implementation of [HtmlElementView]. +extension HtmlElementViewImpl on HtmlElementView { + /// Creates an [HtmlElementView] that renders a DOM element with the given + /// [tagName]. + static HtmlElementView createFromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) { + return HtmlElementView( + key: key, + viewType: isVisible ? ui_web.PlatformViewRegistry.defaultVisibleViewType : ui_web.PlatformViewRegistry.defaultInvisibleViewType, + onPlatformViewCreated: _createPlatformViewCallbackForElementCallback(onElementCreated), + creationParams: {'tagName': tagName}, + ); + } + + /// The implementation of [HtmlElementView.build]. + /// + /// This is not expected to be invoked in non-web environments. It throws if + /// that happens. + /// + /// The implementation on Flutter Web builds an HTML platform view and handles + /// its lifecycle. + Widget buildImpl(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + onCreatePlatformView: _createController, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + controller: controller, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + ); + } + + /// Creates the controller and kicks off its initialization. + _HtmlElementViewController _createController( + PlatformViewCreationParams params, + ) { + final _HtmlElementViewController controller = _HtmlElementViewController( + params.id, + viewType, + creationParams, + ); + controller._initialize().then((_) { + params.onPlatformViewCreated(params.id); + onPlatformViewCreated?.call(params.id); + }); + return controller; + } +} + +PlatformViewCreatedCallback? _createPlatformViewCallbackForElementCallback( + ElementCreatedCallback? onElementCreated, +) { + if (onElementCreated == null) { + return null; + } + return (int id) { + onElementCreated(_platformViewsRegistry.getViewById(id)); + }; +} + +class _HtmlElementViewController extends PlatformViewController { + _HtmlElementViewController( + this.viewId, + this.viewType, + this.creationParams, + ); + + @override + final int viewId; + + /// The unique identifier for the HTML view type to be embedded by this widget. + /// + /// A PlatformViewFactory for this type must have been registered. + final String viewType; + + final dynamic creationParams; + + bool _initialized = false; + + Future _initialize() async { + final Map args = { + 'id': viewId, + 'viewType': viewType, + 'params': creationParams, + }; + await SystemChannels.platform_views.invokeMethod('create', args); + _initialized = true; + } + + @override + Future clearFocus() async { + // Currently this does nothing on Flutter Web. + // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 + } + + @override + Future dispatchPointerEvent(PointerEvent event) async { + // We do not dispatch pointer events to HTML views because they may contain + // cross-origin iframes, which only accept user-generated events. + } + + @override + Future dispose() async { + if (_initialized) { + await SystemChannels.platform_views.invokeMethod('dispose', viewId); + } + } +} + +/// Overrides the [ui_web.PlatformViewRegistry] used by [HtmlElementView]. +/// +/// This is used for testing view factory registration. +@visibleForTesting +ui_web.PlatformViewRegistry? debugOverridePlatformViewRegistry; +ui_web.PlatformViewRegistry get _platformViewsRegistry => debugOverridePlatformViewRegistry ?? ui_web.platformViewRegistry; diff --git a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart index 2a5fa323b81ee..4f06bfc8c60dc 100644 --- a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart +++ b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:js_interop'; import 'dart:ui_web' as ui_web; import 'package:flutter/rendering.dart'; +import 'package:web/web.dart' as web; -import '../services/dom.dart'; import 'basic.dart'; import 'framework.dart'; import 'platform_view.dart'; @@ -27,7 +28,7 @@ const String _kClassRule = ''' '''; const int _kRightClickButton = 2; -typedef _WebSelectionCallBack = void Function(DomHTMLElement, DomMouseEvent); +typedef _WebSelectionCallBack = void Function(web.HTMLElement, web.MouseEvent); /// Function signature for `ui_web.platformViewRegistry.registerViewFactory`. @visibleForTesting @@ -80,7 +81,7 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { // Registers the view factories for the interceptor widgets. static void _register() { assert(_registeredViewType == null); - _registeredViewType = _registerWebSelectionCallback((DomHTMLElement element, DomMouseEvent event) { + _registeredViewType = _registerWebSelectionCallback((web.HTMLElement element, web.MouseEvent event) { final SelectionContainerDelegate? client = _activeClient; if (client != null) { // Converts the html right click event to flutter coordinate. @@ -93,9 +94,9 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { element.innerText = client.getSelectedContent()?.plainText ?? ''; // Programmatically select the dom element in browser. - final DomRange range = domDocument.createRange(); + final web.Range range = web.document.createRange(); range.selectNode(element); - final DomSelection? selection = domWindow.getSelection(); + final web.Selection? selection = web.window.getSelection(); if (selection != null) { selection.removeAllRanges(); selection.addRange(range); @@ -106,26 +107,26 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { static String _registerWebSelectionCallback(_WebSelectionCallBack callback) { _registerViewFactory(_viewType, (int viewId) { - final DomHTMLElement htmlElement = createDomHTMLDivElement(); + final web.HTMLElement htmlElement = web.document.createElement('div') as web.HTMLElement; htmlElement ..style.width = '100%' ..style.height = '100%' ..classList.add(_kClassName); // Create css style for _kClassName. - final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); - domDocument.head!.append(styleElement); - final DomCSSStyleSheet sheet = styleElement.sheet! as DomCSSStyleSheet; + final web.HTMLStyleElement styleElement = web.document.createElement('style') as web.HTMLStyleElement; + web.document.head!.append(styleElement); + final web.CSSStyleSheet sheet = styleElement.sheet!; sheet.insertRule(_kClassRule, 0); sheet.insertRule(_kClassSelectionRule, 1); - htmlElement.addEventListener('mousedown', createDomEventListener((DomEvent event) { - final DomMouseEvent mouseEvent = event as DomMouseEvent; + htmlElement.addEventListener('mousedown', (web.Event event) { + final web.MouseEvent mouseEvent = event as web.MouseEvent; if (mouseEvent.button != _kRightClickButton) { return; } callback(htmlElement, mouseEvent); - })); + }.toJS); return htmlElement; }, isVisible: false); return _viewType; diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart index 72137c0d6fba8..4e31bbd45c32a 100644 --- a/packages/flutter/lib/src/widgets/actions.dart +++ b/packages/flutter/lib/src/widgets/actions.dart @@ -455,8 +455,6 @@ abstract class Action with Diagnosticable { @immutable class ActionListener extends StatefulWidget { /// Create a const [ActionListener]. - /// - /// The [listener], [action], and [child] arguments must not be null. const ActionListener({ super.key, required this.listener, @@ -465,13 +463,9 @@ class ActionListener extends StatefulWidget { }); /// The [ActionListenerCallback] callback to register with the [action]. - /// - /// Must not be null. final ActionListenerCallback listener; /// The [Action] that the callback will be registered with. - /// - /// Must not be null. final Action action; /// {@macro flutter.widgets.ProxyWidget.child} @@ -712,8 +706,6 @@ class ActionDispatcher with Diagnosticable { /// * [ActionDispatcher], the object that this widget uses to manage actions. class Actions extends StatefulWidget { /// Creates an [Actions] widget. - /// - /// The [child], [actions], and [dispatcher] arguments must not be null. const Actions({ super.key, this.dispatcher, @@ -950,8 +942,6 @@ class Actions extends StatefulWidget { /// This method returns the result of invoking the action's [Action.invoke] /// method. /// - /// The `context` and `intent` arguments must not be null. - /// /// If the given `intent` doesn't map to an action, then it will look to the /// next ancestor [Actions] widget in the hierarchy until it reaches the root. /// @@ -1003,8 +993,6 @@ class Actions extends StatefulWidget { /// first action found was disabled, or the action itself returns null /// from [Action.invoke], then this method returns null. /// - /// The `context` and `intent` arguments must not be null. - /// /// If the given `intent` doesn't map to an action, then it will look to the /// next ancestor [Actions] widget in the hierarchy until it reaches the root. /// If a suitable [Action] is found but its [Action.isEnabled] returns false, @@ -1153,8 +1141,6 @@ class _ActionsScope extends InheritedWidget { /// It hosts its own [FocusNode] or uses [focusNode], if given. class FocusableActionDetector extends StatefulWidget { /// Create a const [FocusableActionDetector]. - /// - /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null. const FocusableActionDetector({ super.key, this.enabled = true, diff --git a/packages/flutter/lib/src/widgets/adapter.dart b/packages/flutter/lib/src/widgets/adapter.dart new file mode 100644 index 0000000000000..1948312f7de41 --- /dev/null +++ b/packages/flutter/lib/src/widgets/adapter.dart @@ -0,0 +1,177 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; + +/// A bridge from a [RenderObject] to an [Element] tree. +/// +/// The given container is the [RenderObject] that the [Element] tree should be +/// inserted into. It must be a [RenderObject] that implements the +/// [RenderObjectWithChildMixin] protocol. The type argument `T` is the kind of +/// [RenderObject] that the container expects as its child. +/// +/// The [RenderObjectToWidgetAdapter] is an alternative to [RootWidget] for +/// bootstrapping an element tree. Unlike [RootWidget] it requires the +/// existence of a render tree (the [container]) to attach the element tree to. +class RenderObjectToWidgetAdapter extends RenderObjectWidget { + /// Creates a bridge from a [RenderObject] to an [Element] tree. + RenderObjectToWidgetAdapter({ + this.child, + required this.container, + this.debugShortDescription, + }) : super(key: GlobalObjectKey(container)); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget? child; + + /// The [RenderObject] that is the parent of the [Element] created by this widget. + final RenderObjectWithChildMixin container; + + /// A short description of this widget used by debugging aids. + final String? debugShortDescription; + + @override + RenderObjectToWidgetElement createElement() => RenderObjectToWidgetElement(this); + + @override + RenderObjectWithChildMixin createRenderObject(BuildContext context) => container; + + @override + void updateRenderObject(BuildContext context, RenderObject renderObject) { } + + /// Inflate this widget and actually set the resulting [RenderObject] as the + /// child of [container]. + /// + /// If `element` is null, this function will create a new element. Otherwise, + /// the given element will have an update scheduled to switch to this widget. + RenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement? element ]) { + if (element == null) { + owner.lockState(() { + element = createElement(); + assert(element != null); + element!.assignOwner(owner); + }); + owner.buildScope(element!, () { + element!.mount(null, null); + }); + } else { + element._newWidget = this; + element.markNeedsBuild(); + } + return element!; + } + + @override + String toStringShort() => debugShortDescription ?? super.toStringShort(); +} + +/// The root of an element tree that is hosted by a [RenderObject]. +/// +/// This element class is the instantiation of a [RenderObjectToWidgetAdapter] +/// widget. It can be used only as the root of an [Element] tree (it cannot be +/// mounted into another [Element]; it's parent must be null). +/// +/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter] +/// whose container is the [RenderView]. +class RenderObjectToWidgetElement extends RenderTreeRootElement with RootElementMixin { + /// Creates an element that is hosted by a [RenderObject]. + /// + /// The [RenderObject] created by this element is not automatically set as a + /// child of the hosting [RenderObject]. To actually attach this element to + /// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree]. + RenderObjectToWidgetElement(RenderObjectToWidgetAdapter super.widget); + + Element? _child; + + static const Object _rootChildSlot = Object(); + + @override + void visitChildren(ElementVisitor visitor) { + if (_child != null) { + visitor(_child!); + } + } + + @override + void forgetChild(Element child) { + assert(child == _child); + _child = null; + super.forgetChild(child); + } + + @override + void mount(Element? parent, Object? newSlot) { + assert(parent == null); + super.mount(parent, newSlot); + _rebuild(); + assert(_child != null); + } + + @override + void update(RenderObjectToWidgetAdapter newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _rebuild(); + } + + // When we are assigned a new widget, we store it here + // until we are ready to update to it. + Widget? _newWidget; + + @override + void performRebuild() { + if (_newWidget != null) { + // _newWidget can be null if, for instance, we were rebuilt + // due to a reassemble. + final Widget newWidget = _newWidget!; + _newWidget = null; + update(newWidget as RenderObjectToWidgetAdapter); + } + super.performRebuild(); + assert(_newWidget == null); + } + + @pragma('vm:notify-debugger-on-exception') + void _rebuild() { + try { + _child = updateChild(_child, (widget as RenderObjectToWidgetAdapter).child, _rootChildSlot); + } catch (exception, stack) { + final FlutterErrorDetails details = FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'widgets library', + context: ErrorDescription('attaching to the render tree'), + ); + FlutterError.reportError(details); + final Widget error = ErrorWidget.builder(details); + _child = updateChild(null, error, _rootChildSlot); + } + } + + @override + RenderObjectWithChildMixin get renderObject => super.renderObject as RenderObjectWithChildMixin; + + @override + void insertRenderObjectChild(RenderObject child, Object? slot) { + assert(slot == _rootChildSlot); + assert(renderObject.debugValidateChild(child)); + renderObject.child = child as T; + } + + @override + void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { + assert(false); + } + + @override + void removeRenderObjectChild(RenderObject child, Object? slot) { + assert(renderObject.child == child); + renderObject.child = null; + } +} diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 6d0e928af11d5..116d61bc3fee1 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -115,8 +115,6 @@ class AnimatedCrossFade extends StatefulWidget { /// The [duration] of the animation is the same for all components (fade in, /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in /// order to have finer control, e.g., creating an overlap between the fades. - /// - /// All the arguments other than [key] must be non-null. const AnimatedCrossFade({ super.key, required this.firstChild, diff --git a/packages/flutter/lib/src/widgets/animated_size.dart b/packages/flutter/lib/src/widgets/animated_size.dart index b136fc6aef77f..3deb0d7660288 100644 --- a/packages/flutter/lib/src/widgets/animated_size.dart +++ b/packages/flutter/lib/src/widgets/animated_size.dart @@ -23,8 +23,6 @@ import 'ticker_provider.dart'; /// * [SizeTransition], which changes its size based on an [Animation]. class AnimatedSize extends StatefulWidget { /// Creates a widget that animates its size to match that of its child. - /// - /// The [curve] and [duration] arguments must not be null. const AnimatedSize({ super.key, this.child, @@ -77,7 +75,7 @@ class AnimatedSize extends StatefulWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; @override diff --git a/packages/flutter/lib/src/widgets/animated_switcher.dart b/packages/flutter/lib/src/widgets/animated_switcher.dart index e2d6828a6b376..177ede770629b 100644 --- a/packages/flutter/lib/src/widgets/animated_switcher.dart +++ b/packages/flutter/lib/src/widgets/animated_switcher.dart @@ -103,9 +103,6 @@ typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, Li /// * [FadeTransition], which [AnimatedSwitcher] uses to perform the transition. class AnimatedSwitcher extends StatefulWidget { /// Creates an [AnimatedSwitcher]. - /// - /// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and - /// [switchOutCurve] parameters must not be null. const AnimatedSwitcher({ super.key, this.child, diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index c641566c384fe..b6b4b9647bfcd 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -19,6 +19,7 @@ import 'framework.dart'; import 'localizations.dart'; import 'media_query.dart'; import 'navigator.dart'; +import 'notification_listener.dart'; import 'pages.dart'; import 'performance_overlay.dart'; import 'restoration.dart'; @@ -263,8 +264,6 @@ class WidgetsApp extends StatefulWidget { /// Creates a widget that wraps a number of widgets that are commonly /// required for an application. /// - /// The boolean arguments, [color], and [navigatorObservers] must not be null. - /// /// Most callers will want to use the [home] or [routes] parameters, or both. /// The [home] parameter is a convenience for the following [routes] map: /// @@ -313,6 +312,7 @@ class WidgetsApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, + this.onNavigationNotification, List this.navigatorObservers = const [], this.initialRoute, this.pageRouteBuilder, @@ -420,6 +420,7 @@ class WidgetsApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, + this.onNavigationNotification, this.textStyle, required this.color, this.locale, @@ -701,6 +702,13 @@ class WidgetsApp extends StatefulWidget { /// {@endtemplate} final RouteFactory? onUnknownRoute; + /// {@template flutter.widgets.widgetsApp.onNavigationNotification} + /// The callback to use when receiving a [NavigationNotification]. + /// + /// By default this updates the engine with the navigation status. + /// {@endtemplate} + final NotificationListenerCallback? onNavigationNotification; + /// {@template flutter.widgets.widgetsApp.initialRoute} /// The name of the first route to show, if a [Navigator] is built. /// @@ -725,6 +733,10 @@ class WidgetsApp extends StatefulWidget { /// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not, /// [initialRoute] must be null and [builder] must not be null. /// + /// Changing the [initialRoute] will have no effect, as it only controls the + /// _initial_ route. To change the route while the application is running, use + /// the [Navigator] or [Router] APIs. + /// /// See also: /// /// * [Navigator.initialRoute], which is used to implement this property. @@ -1324,12 +1336,41 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { ? WidgetsBinding.instance.platformDispatcher.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.platformDispatcher.defaultRouteName; + AppLifecycleState? _appLifecycleState; + + /// The default value for [onNavigationNotification]. + /// + /// Does nothing and stops bubbling if the app is detached. Otherwise, updates + /// the platform with [NavigationNotification.canHandlePop] and stops + /// bubbling. + bool _defaultOnNavigationNotification(NavigationNotification notification) { + switch (_appLifecycleState) { + case null: + case AppLifecycleState.detached: + case AppLifecycleState.inactive: + // Avoid updating the engine when the app isn't ready. + return true; + case AppLifecycleState.resumed: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + SystemNavigator.setFrameworkHandlesBack(notification.canHandlePop); + return true; + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _appLifecycleState = state; + super.didChangeAppLifecycleState(state); + } + @override void initState() { super.initState(); _updateRouting(); _locale = _resolveLocales(WidgetsBinding.instance.platformDispatcher.locales, widget.supportedLocales); WidgetsBinding.instance.addObserver(this); + _appLifecycleState = WidgetsBinding.instance.lifecycleState; } @override @@ -1747,25 +1788,28 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { return RootRestorationScope( restorationId: widget.restorationScopeId, child: SharedAppData( - child: Shortcuts( - debugLabel: '', - shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, - // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can - // fall through to the defaultShortcuts. - child: DefaultTextEditingShortcuts( - child: Actions( - actions: widget.actions ?? >{ - ...WidgetsApp.defaultActions, - ScrollIntent: Action.overridable(context: context, defaultAction: ScrollAction()), - }, - child: FocusTraversalGroup( - policy: ReadingOrderTraversalPolicy(), - child: TapRegionSurface( - child: ShortcutRegistrar( - child: Localizations( - locale: appLocale, - delegates: _localizationsDelegates.toList(), - child: title, + child: NotificationListener( + onNotification: widget.onNavigationNotification ?? _defaultOnNavigationNotification, + child: Shortcuts( + debugLabel: '', + shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, + // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can + // fall through to the defaultShortcuts. + child: DefaultTextEditingShortcuts( + child: Actions( + actions: widget.actions ?? >{ + ...WidgetsApp.defaultActions, + ScrollIntent: Action.overridable(context: context, defaultAction: ScrollAction()), + }, + child: FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: TapRegionSurface( + child: ShortcutRegistrar( + child: Localizations( + locale: appLocale, + delegates: _localizationsDelegates.toList(), + child: title, + ), ), ), ), diff --git a/packages/flutter/lib/src/widgets/async.dart b/packages/flutter/lib/src/widgets/async.dart index 7e755524c2c2d..b8aed1f94fc8d 100644 --- a/packages/flutter/lib/src/widgets/async.dart +++ b/packages/flutter/lib/src/widgets/async.dart @@ -386,8 +386,6 @@ class StreamBuilder extends StreamBuilderBase> { /// strategy is given by [builder]. /// /// The [initialData] is used to create the initial snapshot. - /// - /// The [builder] must not be null. const StreamBuilder({ super.key, this.initialData, @@ -517,8 +515,6 @@ class StreamBuilder extends StreamBuilderBase> { class FutureBuilder extends StatefulWidget { /// Creates a widget that builds itself based on the latest snapshot of /// interaction with a [Future]. - /// - /// The [builder] must not be null. const FutureBuilder({ super.key, required this.future, @@ -599,13 +595,14 @@ class _FutureBuilderState extends State> { @override void didUpdateWidget(FutureBuilder oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.future != widget.future) { - if (_activeCallbackIdentity != null) { - _unsubscribe(); - _snapshot = _snapshot.inState(ConnectionState.none); - } - _subscribe(); + if (oldWidget.future == widget.future) { + return; } + if (_activeCallbackIdentity != null) { + _unsubscribe(); + _snapshot = _snapshot.inState(ConnectionState.none); + } + _subscribe(); } @override @@ -618,33 +615,35 @@ class _FutureBuilderState extends State> { } void _subscribe() { - if (widget.future != null) { - final Object callbackIdentity = Object(); - _activeCallbackIdentity = callbackIdentity; - widget.future!.then((T data) { - if (_activeCallbackIdentity == callbackIdentity) { - setState(() { - _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); - }); - } - }, onError: (Object error, StackTrace stackTrace) { - if (_activeCallbackIdentity == callbackIdentity) { - setState(() { - _snapshot = AsyncSnapshot.withError(ConnectionState.done, error, stackTrace); - }); - } - assert(() { - if (FutureBuilder.debugRethrowError) { - Future.error(error, stackTrace); - } - return true; - }()); - }); - // An implementation like `SynchronousFuture` may have already called the - // .then closure. Do not overwrite it in that case. - if (_snapshot.connectionState != ConnectionState.done) { - _snapshot = _snapshot.inState(ConnectionState.waiting); + if (widget.future == null) { + // There is no future to subscribe to, do nothing. + return; + } + final Object callbackIdentity = Object(); + _activeCallbackIdentity = callbackIdentity; + widget.future!.then((T data) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withData(ConnectionState.done, data); + }); } + }, onError: (Object error, StackTrace stackTrace) { + if (_activeCallbackIdentity == callbackIdentity) { + setState(() { + _snapshot = AsyncSnapshot.withError(ConnectionState.done, error, stackTrace); + }); + } + assert(() { + if (FutureBuilder.debugRethrowError) { + Future.error(error, stackTrace); + } + return true; + }()); + }); + // An implementation like `SynchronousFuture` may have already called the + // .then closure. Do not overwrite it in that case. + if (_snapshot.connectionState != ConnectionState.done) { + _snapshot = _snapshot.inState(ConnectionState.waiting); } } diff --git a/packages/flutter/lib/src/widgets/autocomplete.dart b/packages/flutter/lib/src/widgets/autocomplete.dart index 967c1160a2f82..d44ec4d414818 100644 --- a/packages/flutter/lib/src/widgets/autocomplete.dart +++ b/packages/flutter/lib/src/widgets/autocomplete.dart @@ -77,6 +77,29 @@ typedef AutocompleteFieldViewBuilder = Widget Function( /// * [RawAutocomplete.displayStringForOption], which is of this type. typedef AutocompleteOptionToString = String Function(T option); +/// A direction in which to open the options-view overlay. +/// +/// See also: +/// +/// * [RawAutocomplete.optionsViewOpenDirection], which is of this type. +/// * [RawAutocomplete.optionsViewBuilder] to specify how to build the +/// selectable-options widget. +/// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the +/// corresponding field widget. +enum OptionsViewOpenDirection { + /// Open upward. + /// + /// The bottom edge of the options view will align with the top edge + /// of the text field built by [RawAutocomplete.fieldViewBuilder]. + up, + + /// Open downward. + /// + /// The top edge of the options view will align with the bottom edge + /// of the text field built by [RawAutocomplete.fieldViewBuilder]. + down, +} + // TODO(justinmc): Mention AutocompleteCupertino when it is implemented. /// {@template flutter.widgets.RawAutocomplete.RawAutocomplete} /// A widget for helping the user make a selection by entering some text and @@ -128,6 +151,7 @@ class RawAutocomplete extends StatefulWidget { super.key, required this.optionsViewBuilder, required this.optionsBuilder, + this.optionsViewOpenDirection = OptionsViewOpenDirection.down, this.displayStringForOption = defaultStringForOption, this.fieldViewBuilder, this.focusNode, @@ -151,6 +175,9 @@ class RawAutocomplete extends StatefulWidget { /// Pass the provided [TextEditingController] to the field built here so that /// RawAutocomplete can listen for changes. /// {@endtemplate} + /// + /// If this parameter is null, then a [SizedBox.shrink] is built instead. + /// For how that pattern can be useful, see [textEditingController]. final AutocompleteFieldViewBuilder? fieldViewBuilder; /// The [FocusNode] that is used for the text field. @@ -161,9 +188,9 @@ class RawAutocomplete extends StatefulWidget { /// field built by [fieldViewBuilder]. For example, it may be desirable to /// place the text field in the AppBar and the options below in the main body. /// - /// When following this pattern, [fieldViewBuilder] can return - /// `SizedBox.shrink()` so that nothing is drawn where the text field would - /// normally be. A separate text field can be created elsewhere, and a + /// When following this pattern, [fieldViewBuilder] can be omitted, + /// so that a text field is not drawn where it would normally be. + /// A separate text field can be created elsewhere, and a /// FocusNode and TextEditingController can be passed both to that text field /// and to RawAutocomplete. /// @@ -182,9 +209,10 @@ class RawAutocomplete extends StatefulWidget { /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} /// Builds the selectable options widgets from a list of options objects. /// - /// The options are displayed floating below the field using a + /// The options are displayed floating below or above the field using a /// [CompositedTransformFollower] inside of an [Overlay], not at the same - /// place in the widget tree as [RawAutocomplete]. + /// place in the widget tree as [RawAutocomplete]. To control whether it opens + /// upward or downward, use [optionsViewOpenDirection]. /// /// In order to track which item is highlighted by keyboard navigation, the /// resulting options will be wrapped in an inherited @@ -197,6 +225,13 @@ class RawAutocomplete extends StatefulWidget { /// {@endtemplate} final AutocompleteOptionsViewBuilder optionsViewBuilder; + /// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection} + /// The direction in which to open the options-view overlay. + /// + /// Defaults to [OptionsViewOpenDirection.down]. + /// {@endtemplate} + final OptionsViewOpenDirection optionsViewOpenDirection; + /// {@template flutter.widgets.RawAutocomplete.displayStringForOption} /// Returns the string to display in the field when the option is selected. /// @@ -209,10 +244,6 @@ class RawAutocomplete extends StatefulWidget { /// {@template flutter.widgets.RawAutocomplete.onSelected} /// Called when an option is selected by the user. - /// - /// Any [TextEditingController] listeners will not be called when the user - /// selects an option, even though the field will update with the selected - /// value, so use this to be informed of selection. /// {@endtemplate} final AutocompleteOnSelected? onSelected; @@ -415,13 +446,21 @@ class _RawAutocompleteState extends State> } _floatingOptions?.remove(); + _floatingOptions?.dispose(); if (_shouldShowOptions) { final OverlayEntry newFloatingOptions = OverlayEntry( builder: (BuildContext context) { return CompositedTransformFollower( link: _optionsLayerLink, showWhenUnlinked: false, - targetAnchor: Alignment.bottomLeft, + targetAnchor: switch (widget.optionsViewOpenDirection) { + OptionsViewOpenDirection.up => Alignment.topLeft, + OptionsViewOpenDirection.down => Alignment.bottomLeft, + }, + followerAnchor: switch (widget.optionsViewOpenDirection) { + OptionsViewOpenDirection.up => Alignment.bottomLeft, + OptionsViewOpenDirection.down => Alignment.topLeft, + }, child: TextFieldTapRegion( child: AutocompleteHighlightedOption( highlightIndexNotifier: _highlightedOptionIndex, @@ -524,7 +563,9 @@ class _RawAutocompleteState extends State> _focusNode.dispose(); } _floatingOptions?.remove(); + _floatingOptions?.dispose(); _floatingOptions = null; + _highlightedOptionIndex.dispose(); super.dispose(); } diff --git a/packages/flutter/lib/src/widgets/autofill.dart b/packages/flutter/lib/src/widgets/autofill.dart index c184081944b1c..0d72987d3490e 100644 --- a/packages/flutter/lib/src/widgets/autofill.dart +++ b/packages/flutter/lib/src/widgets/autofill.dart @@ -64,8 +64,6 @@ enum AutofillContextAction { /// clean up actions to be run when a topmost [AutofillGroup] is disposed. class AutofillGroup extends StatefulWidget { /// Creates a scope for autofillable input fields. - /// - /// The [child] argument must not be null. const AutofillGroup({ super.key, required this.child, diff --git a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart index 4b3b7cb6ccc28..a8292b1487d9e 100644 --- a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart +++ b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart @@ -294,8 +294,6 @@ class _AutomaticKeepAliveState extends State { /// [KeepAliveNotification] internally. class KeepAliveNotification extends Notification { /// Creates a notification to indicate that a subtree must be kept alive. - /// - /// The [handle] must not be null. const KeepAliveNotification(this.handle); /// A [Listenable] that will inform its clients when the widget that fired the diff --git a/packages/flutter/lib/src/widgets/banner.dart b/packages/flutter/lib/src/widgets/banner.dart index 55e6ed5620958..33a11fd828c46 100644 --- a/packages/flutter/lib/src/widgets/banner.dart +++ b/packages/flutter/lib/src/widgets/banner.dart @@ -54,9 +54,6 @@ enum BannerLocation { /// Paints a [Banner]. class BannerPainter extends CustomPainter { /// Creates a banner painter. - /// - /// The [message], [textDirection], [location], and [layoutDirection] - /// arguments must not be null. BannerPainter({ required this.message, required this.textDirection, @@ -236,8 +233,6 @@ class BannerPainter extends CustomPainter { /// debug mode, to show a banner that says "DEBUG". class Banner extends StatelessWidget { /// Creates a banner. - /// - /// The [message] and [location] arguments must not be null. const Banner({ super.key, this.child, diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index cd6c205aa9ccf..3e0a8942886a2 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -147,8 +147,6 @@ abstract class _UbiquitousInheritedWidget extends InheritedWidget { class Directionality extends _UbiquitousInheritedWidget { /// Creates a widget that determines the directionality of text and /// text-direction-sensitive render objects. - /// - /// The [textDirection] and [child] arguments must not be null. const Directionality({ super.key, required this.textDirection, @@ -290,6 +288,22 @@ class Directionality extends _UbiquitousInheritedWidget { /// Drawing content into the offscreen buffer may also trigger render target /// switches and such switching is particularly slow in older GPUs. /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being applied +/// to the descendants of the [Opacity] widget. This can be confusing for the +/// user, who may not see anything, and may believe the area of the interface +/// where the [Opacity] is hiding a widget to be non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the [Opacity] +/// widget at all when the [opacity] is zero. +/// +/// To avoid such problems, it is generally a good idea to use an +/// [IgnorePointer] widget when setting the [opacity] to zero. This prevents +/// interactions with any children in the subtree. +/// /// See also: /// /// * [Visibility], which can hide a child more efficiently (albeit less @@ -304,8 +318,7 @@ class Directionality extends _UbiquitousInheritedWidget { class Opacity extends SingleChildRenderObjectWidget { /// Creates a widget that makes its child partially transparent. /// - /// The [opacity] argument must not be null and must be between 0.0 and 1.0 - /// (inclusive). + /// The [opacity] argument must be between zero and one, inclusive. const Opacity({ super.key, required this.opacity, @@ -315,14 +328,11 @@ class Opacity extends SingleChildRenderObjectWidget { /// The fraction to scale the child's alpha value. /// - /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent + /// An opacity of one is fully opaque. An opacity of zero is fully transparent /// (i.e., invisible). /// - /// The opacity must not be null. - /// - /// Values 1.0 and 0.0 are painted with a fast path. Other values - /// require painting the child into an intermediate buffer, which is - /// expensive. + /// Values one and zero are painted with a fast path. Other values require + /// painting the child into an intermediate buffer, which is expensive. final double opacity; /// Whether the semantic information of the children is always included. @@ -395,8 +405,6 @@ class Opacity extends SingleChildRenderObjectWidget { /// * [BackdropFilter], which applies an image filter to the background. class ShaderMask extends SingleChildRenderObjectWidget { /// Creates a widget that applies a mask generated by a [Shader] to its child. - /// - /// The [shaderCallback] and [blendMode] arguments must not be null. const ShaderMask({ super.key, required this.shaderCallback, @@ -544,7 +552,6 @@ class ShaderMask extends SingleChildRenderObjectWidget { class BackdropFilter extends SingleChildRenderObjectWidget { /// Creates a backdrop filter. /// - /// The [filter] argument must not be null. /// The [blendMode] argument will default to [BlendMode.srcOver] and must not be /// null if provided. const BackdropFilter({ @@ -600,11 +607,13 @@ class BackdropFilter extends SingleChildRenderObjectWidget { /// `setState` or `markNeedsLayout` during the callback (the layout for this /// frame has already happened). /// -/// Custom painters normally size themselves to their child. If they do not have -/// a child, they attempt to size themselves to the [size], which defaults to -/// [Size.zero]. [size] must not be null. +/// Custom painters normally size themselves to their [child]. If they do not +/// have a child, they attempt to size themselves to the specified [size], which +/// defaults to [Size.zero]. The parent [may enforce constraints on this +/// size](https://docs.flutter.dev/ui/layout/constraints). /// -/// [isComplex] and [willChange] are hints to the compositor's raster cache. +/// The [isComplex] and [willChange] properties are hints to the compositor's +/// raster cache. /// /// {@tool snippet} /// @@ -758,8 +767,7 @@ class ClipRect extends SingleChildRenderObjectWidget { /// If [clipper] is null, the clip will match the layout size and position of /// the child. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. const ClipRect({ super.key, this.clipper, @@ -842,8 +850,7 @@ class ClipRRect extends SingleChildRenderObjectWidget { /// /// If [clipper] is non-null, then [borderRadius] is ignored. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. const ClipRRect({ super.key, this.borderRadius = BorderRadius.zero, @@ -915,8 +922,7 @@ class ClipOval extends SingleChildRenderObjectWidget { /// If [clipper] is null, the oval will be inscribed into the layout size and /// position of the child. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. const ClipOval({ super.key, this.clipper, @@ -990,8 +996,7 @@ class ClipPath extends SingleChildRenderObjectWidget { /// consider using a [ClipRect], which can achieve the same effect more /// efficiently. /// - /// The [clipBehavior] argument must not be null. If [clipBehavior] is - /// [Clip.none], no clipping will be applied. + /// If [clipBehavior] is [Clip.none], no clipping will be applied. const ClipPath({ super.key, this.clipper, @@ -1181,8 +1186,7 @@ class PhysicalShape extends SingleChildRenderObjectWidget { /// /// The [color] is required; physical things have a color. /// - /// The [clipper], [elevation], [color], [clipBehavior], and [shadowColor] - /// must not be null. Additionally, the [elevation] must be non-negative. + /// The [elevation] must be non-negative. const PhysicalShape({ super.key, required this.clipper, @@ -1248,6 +1252,7 @@ class PhysicalShape extends SingleChildRenderObjectWidget { } } + // POSITIONING AND SIZING NODES /// A widget that applies a transformation before painting its child. @@ -1291,8 +1296,6 @@ class PhysicalShape extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Transform extends SingleChildRenderObjectWidget { /// Creates a widget that transforms its child. - /// - /// The [transform] argument must not be null. const Transform({ super.key, required this.transform, @@ -1306,8 +1309,7 @@ class Transform extends SingleChildRenderObjectWidget { /// Creates a widget that transforms its child using a rotation around the /// center. /// - /// The `angle` argument must not be null. It gives the rotation in clockwise - /// radians. + /// The `angle` argument gives the rotation in clockwise radians. /// /// {@tool snippet} /// @@ -1342,7 +1344,7 @@ class Transform extends SingleChildRenderObjectWidget { /// Creates a widget that transforms its child using a translation. /// - /// The `offset` argument must not be null. It specifies the translation. + /// The `offset` argument specifies the translation. /// /// {@tool snippet} /// @@ -1371,19 +1373,25 @@ class Transform extends SingleChildRenderObjectWidget { /// Creates a widget that scales its child along the 2D plane. /// - /// The `scaleX` argument provides the scalar by which to multiply the `x` axis, and the `scaleY` argument provides the scalar by which to multiply the `y` axis. Either may be omitted, in which case the scaling factor for that axis defaults to 1.0. + /// The `scaleX` argument provides the scalar by which to multiply the `x` + /// axis, and the `scaleY` argument provides the scalar by which to multiply + /// the `y` axis. Either may be omitted, in which case the scaling factor for + /// that axis defaults to 1.0. /// - /// For convenience, to scale the child uniformly, instead of providing `scaleX` and `scaleY`, the `scale` parameter may be used. + /// For convenience, to scale the child uniformly, instead of providing + /// `scaleX` and `scaleY`, the `scale` parameter may be used. /// - /// At least one of `scale`, `scaleX`, and `scaleY` must be non-null. If `scale` is provided, the other two must be null; similarly, if it is not provided, one of the other two must be provided. + /// At least one of `scale`, `scaleX`, and `scaleY` must be non-null. If + /// `scale` is provided, the other two must be null; similarly, if it is not + /// provided, one of the other two must be provided. /// - /// The [alignment] controls the origin of the scale; by default, this is - /// the center of the box. + /// The [alignment] controls the origin of the scale; by default, this is the + /// center of the box. /// /// {@tool snippet} /// - /// This example shrinks an orange box containing text such that each dimension - /// is half the size it would otherwise be. + /// This example shrinks an orange box containing text such that each + /// dimension is half the size it would otherwise be. /// /// ```dart /// Transform.scale( @@ -1399,8 +1407,8 @@ class Transform extends SingleChildRenderObjectWidget { /// /// See also: /// - /// * [ScaleTransition], which animates changes in scale smoothly - /// over a given duration. + /// * [ScaleTransition], which animates changes in scale smoothly over a given + /// duration. Transform.scale({ super.key, double? scale, @@ -1558,8 +1566,8 @@ class Transform extends SingleChildRenderObjectWidget { class CompositedTransformTarget extends SingleChildRenderObjectWidget { /// Creates a composited transform target widget. /// - /// The [link] property must not be null, and must not be currently being used - /// by any other [CompositedTransformTarget] object that is in the tree. + /// The [link] property must not be currently used by any other + /// [CompositedTransformTarget] object that is in the tree. const CompositedTransformTarget({ super.key, required this.link, @@ -1569,8 +1577,8 @@ class CompositedTransformTarget extends SingleChildRenderObjectWidget { /// The link object that connects this [CompositedTransformTarget] with one or /// more [CompositedTransformFollower]s. /// - /// This property must not be null. The object must not be associated with - /// another [CompositedTransformTarget] that is also being painted. + /// The link must not be associated with another [CompositedTransformTarget] + /// that is also being painted. final LayerLink link; @override @@ -1616,9 +1624,8 @@ class CompositedTransformTarget extends SingleChildRenderObjectWidget { class CompositedTransformFollower extends SingleChildRenderObjectWidget { /// Creates a composited transform target widget. /// - /// The [link] property must not be null. If it was also provided to a - /// [CompositedTransformTarget], that widget must come earlier in the paint - /// order. + /// If the [link] property was also provided to a [CompositedTransformTarget], + /// that widget must come earlier in the paint order. /// /// The [showWhenUnlinked] and [offset] properties must also not be null. const CompositedTransformFollower({ @@ -1633,8 +1640,6 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { /// The link object that connects this [CompositedTransformFollower] with a /// [CompositedTransformTarget]. - /// - /// This property must not be null. final LayerLink link; /// Whether to show the widget's contents when there is no corresponding @@ -1718,8 +1723,6 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class FittedBox extends SingleChildRenderObjectWidget { /// Creates a widget that scales and positions its child within itself according to [fit]. - /// - /// The [fit] and [alignment] arguments must not be null. const FittedBox({ super.key, this.fit = BoxFit.contain, @@ -1798,8 +1801,6 @@ class FittedBox extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class FractionalTranslation extends SingleChildRenderObjectWidget { /// Creates a widget that translates its child's painting. - /// - /// The [translation] argument must not be null. const FractionalTranslation({ super.key, required this.translation, @@ -1861,8 +1862,6 @@ class FractionalTranslation extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class RotatedBox extends SingleChildRenderObjectWidget { /// A widget that rotates its child. - /// - /// The [quarterTurns] argument must not be null. const RotatedBox({ super.key, required this.quarterTurns, @@ -1936,8 +1935,6 @@ class RotatedBox extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Padding extends SingleChildRenderObjectWidget { /// Creates a widget that insets its child. - /// - /// The [padding] argument must not be null. const Padding({ super.key, required this.padding, @@ -2214,8 +2211,6 @@ class Center extends Align { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class CustomSingleChildLayout extends SingleChildRenderObjectWidget { /// Creates a custom single child layout. - /// - /// The [delegate] argument must not be null. const CustomSingleChildLayout({ super.key, required this.delegate, @@ -2243,8 +2238,6 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget { /// [MultiChildLayoutDelegate.positionChild] methods use these identifiers. class LayoutId extends ParentDataWidget { /// Marks a child with a layout identifier. - /// - /// Both the child and the id arguments must not be null. LayoutId({ Key? key, required this.id, @@ -2317,8 +2310,6 @@ class LayoutId extends ParentDataWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class CustomMultiChildLayout extends MultiChildRenderObjectWidget { /// Creates a custom multi-child layout. - /// - /// The [delegate] argument must not be null. const CustomMultiChildLayout({ super.key, required this.delegate, @@ -2505,8 +2496,6 @@ class SizedBox extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class ConstrainedBox extends SingleChildRenderObjectWidget { /// Creates a widget that imposes additional constraints on its child. - /// - /// The [constraints] argument must not be null. ConstrainedBox({ super.key, required this.constraints, @@ -2992,8 +2981,7 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget { class LimitedBox extends SingleChildRenderObjectWidget { /// Creates a box that limits its size only when it's unconstrained. /// - /// The [maxWidth] and [maxHeight] arguments must not be null and must not be - /// negative. + /// The [maxWidth] and [maxHeight] arguments must not be negative. const LimitedBox({ super.key, this.maxWidth = double.infinity, @@ -3036,6 +3024,12 @@ class LimitedBox extends SingleChildRenderObjectWidget { /// A widget that imposes different constraints on its child than it gets /// from its parent, possibly allowing the child to overflow the parent. /// +/// {@tool dartpad} +/// This example shows how an [OverflowBox] is used, and what its effect is. +/// +/// ** See code in examples/api/lib/widgets/basic/overflowbox.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [RenderConstrainedOverflowBox] for details about how [OverflowBox] is @@ -3145,8 +3139,6 @@ class OverflowBox extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class SizedOverflowBox extends SingleChildRenderObjectWidget { /// Creates a widget of a given size that lets its child overflow. - /// - /// The [size] argument must not be null. const SizedOverflowBox({ super.key, required this.size, @@ -3524,8 +3516,6 @@ class IntrinsicHeight extends SingleChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Baseline extends SingleChildRenderObjectWidget { /// Creates a widget that positions its child according to the child's baseline. - /// - /// The [baseline] and [baselineType] arguments must not be null. const Baseline({ super.key, required this.baseline, @@ -3553,6 +3543,25 @@ class Baseline extends SingleChildRenderObjectWidget { } } +/// A widget that causes the parent to ignore the [child] for the purposes +/// of baseline alignment. +/// +/// See also: +/// +/// * [Baseline], a widget that positions a child relative to a baseline. +class IgnoreBaseline extends SingleChildRenderObjectWidget { + /// Creates a widget that ignores the child for baseline alignment purposes. + const IgnoreBaseline({ + super.key, + super.child, + }); + + @override + RenderIgnoreBaseline createRenderObject(BuildContext context) { + return RenderIgnoreBaseline(); + } +} + // SLIVERS @@ -3606,8 +3615,6 @@ class SliverToBoxAdapter extends SingleChildRenderObjectWidget { /// * [Padding], the box version of this widget. class SliverPadding extends SingleChildRenderObjectWidget { /// Creates a sliver that applies padding on each side of another sliver. - /// - /// The [padding] argument must not be null. const SliverPadding({ super.key, required this.padding, @@ -3906,6 +3913,17 @@ class Stack extends MultiChildRenderObjectWidget { /// {@macro flutter.material.Material.clipBehavior} /// + /// Stacks only clip children whose _geometry_ overflows the stack. A child + /// that paints outside its bounds (e.g. a box with a shadow) will not be + /// clipped, regardless of the value of this property. Similarly, a child that + /// itself has a descendant that overflows the stack will not be clipped, as + /// only the geometry of the stack's direct children are considered. + /// [Transform] is an example of a widget that can cause its children to paint + /// outside its geometry. + /// + /// To clip children whose geometry does not overflow the stack, consider + /// using a [ClipRect] widget. + /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; @@ -3974,8 +3992,6 @@ class Stack extends MultiChildRenderObjectWidget { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class IndexedStack extends StatelessWidget { /// Creates a [Stack] widget that paints a single child. - /// - /// The [index] argument must not be null. const IndexedStack({ super.key, this.alignment = AlignmentDirectional.topStart, @@ -4219,8 +4235,6 @@ class Positioned extends ParentDataWidget { /// then the `start` argument is used for the [left] property and the `end` /// argument is used for the [right] property. /// - /// The `textDirection` argument must not be null. - /// /// See also: /// /// * [PositionedDirectional], which adapts to the ambient [Directionality]. @@ -4559,9 +4573,8 @@ class Flex extends MultiChildRenderObjectWidget { /// /// The [direction] is required. /// - /// The [direction], [mainAxisAlignment], [crossAxisAlignment], and - /// [verticalDirection] arguments must not be null. If [crossAxisAlignment] is - /// [CrossAxisAlignment.baseline], then [textBaseline] must not be null. + /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then + /// [textBaseline] must not be null. /// /// The [textDirection] argument defaults to the ambient [Directionality], if /// any. If there is no ambient directionality, and a text direction is going @@ -4925,8 +4938,6 @@ class Flex extends MultiChildRenderObjectWidget { class Row extends Flex { /// Creates a horizontal array of children. /// - /// The [mainAxisAlignment], [mainAxisSize], [crossAxisAlignment], and - /// [verticalDirection] arguments must not be null. /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then /// [textBaseline] must not be null. /// @@ -5120,8 +5131,6 @@ class Row extends Flex { class Column extends Flex { /// Creates a vertical array of children. /// - /// The [mainAxisAlignment], [mainAxisSize], [crossAxisAlignment], and - /// [verticalDirection] arguments must not be null. /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then /// [textBaseline] must not be null. /// @@ -5555,34 +5564,51 @@ class Wrap extends MultiChildRenderObjectWidget { /// this animation and repaint whenever the animation ticks, avoiding both the /// build and layout phases of the pipeline. /// +/// {@tool dartpad} +/// This example uses the [Flow] widget to create a menu that opens and closes +/// as it is interacted with, shown above. The color of the button in the menu +/// changes to indicate which one has been selected. +/// +/// ** See code in examples/api/lib/widgets/basic/flow.0.dart ** +/// {@end-tool} +/// +/// ## Hit testing and hidden [Flow] widgets +/// +/// The [Flow] widget recomputers its children's positions (as used by hit +/// testing) during the _paint_ phase rather than during the _layout_ phase. +/// +/// Widgets like [Opacity] avoid painting their children when those children +/// would be invisible due to their opacity being zero. +/// +/// Unfortunately, this means that hiding a [Flow] widget using an [Opacity] +/// widget will cause bugs when the user attempts to interact with the hidden +/// region, for example, by tapping it or clicking it. +/// +/// Such bugs will manifest either as out-of-date geometry (taps going to +/// different widgets than might be expected by the currently-specified +/// [FlowDelegate]s), or exceptions (e.g. if the last time the [Flow] was +/// painted, a different set of children was specified). +/// +/// To avoid this, when hiding a [Flow] widget with an [Opacity] widget (or +/// [AnimatedOpacity] or similar), it is wise to also disable hit testing on the +/// widget by using [IgnorePointer]. This is generally good advice anyway as +/// hit-testing invisible widgets is often confusing for the user. +/// /// See also: /// /// * [Wrap], which provides the layout model that some other frameworks call /// "flow", and is otherwise unrelated to [Flow]. -/// * [FlowDelegate], which controls the visual presentation of the children. /// * [Stack], which arranges children relative to the edges of the container. /// * [CustomSingleChildLayout], which uses a delegate to control the layout of /// a single child. /// * [CustomMultiChildLayout], which uses a delegate to position multiple /// children. /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). -/// -/// -/// {@tool dartpad} -/// This example uses the [Flow] widget to create a menu that opens and closes -/// as it is interacted with, shown above. The color of the button in the menu -/// changes to indicate which one has been selected. -/// -/// ** See code in examples/api/lib/widgets/basic/flow.0.dart ** -/// {@end-tool} -/// class Flow extends MultiChildRenderObjectWidget { /// Creates a flow layout. /// /// Wraps each of the given children in a [RepaintBoundary] to avoid /// repainting the children when the flow repaints. - /// - /// The [delegate] argument must not be null. Flow({ super.key, required this.delegate, @@ -5596,8 +5622,6 @@ class Flow extends MultiChildRenderObjectWidget { /// Does not wrap the given children in repaint boundaries, unlike the default /// constructor. Useful when the child is trivial to paint or already contains /// a repaint boundary. - /// - /// The [delegate] argument must not be null. const Flow.unwrapped({ super.key, required this.delegate, @@ -5610,7 +5634,7 @@ class Flow extends MultiChildRenderObjectWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; @override @@ -5707,9 +5731,6 @@ class Flow extends MultiChildRenderObjectWidget { class RichText extends MultiChildRenderObjectWidget { /// Creates a paragraph of rich text. /// - /// The [text], [textAlign], [softWrap], [overflow], and [textScaleFactor] - /// arguments must not be null. - /// /// The [maxLines] property may be null (and indeed defaults to null), but if /// it is not null, it must be greater than zero. /// @@ -5722,7 +5743,13 @@ class RichText extends MultiChildRenderObjectWidget { this.textDirection, this.softWrap = true, this.overflow = TextOverflow.clip, - this.textScaleFactor = 1.0, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, this.maxLines, this.locale, this.strutStyle, @@ -5732,7 +5759,17 @@ class RichText extends MultiChildRenderObjectWidget { this.selectionColor, }) : assert(maxLines == null || maxLines > 0), assert(selectionRegistrar == null || selectionColor != null), - super(children: WidgetSpan.extractFromInlineSpan(text, textScaleFactor)); + assert(textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), 'Use textScaler instead.'), + textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor), + super(children: WidgetSpan.extractFromInlineSpan(text, _effectiveTextScalerFrom(textScaler, textScaleFactor))); + + static TextScaler _effectiveTextScalerFrom(TextScaler textScaler, double textScaleFactor) { + return switch ((textScaler, textScaleFactor)) { + (final TextScaler scaler, 1.0) => scaler, + (TextScaler.noScaling, final double textScaleFactor) => TextScaler.linear(textScaleFactor), + (final TextScaler scaler, _) => scaler, + }; + } /// The text to display in this widget. final InlineSpan text; @@ -5764,11 +5801,22 @@ class RichText extends MultiChildRenderObjectWidget { /// How visual overflow should be handled. final TextOverflow overflow; + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than /// the specified font size. - final double textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double get textScaleFactor => textScaler.textScaleFactor; + + /// {@macro flutter.painting.textPainter.textScaler} + final TextScaler textScaler; /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according @@ -5818,7 +5866,7 @@ class RichText extends MultiChildRenderObjectWidget { textDirection: textDirection ?? Directionality.of(context), softWrap: softWrap, overflow: overflow, - textScaleFactor: textScaleFactor, + textScaler: textScaler, maxLines: maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis, @@ -5838,7 +5886,7 @@ class RichText extends MultiChildRenderObjectWidget { ..textDirection = textDirection ?? Directionality.of(context) ..softWrap = softWrap ..overflow = overflow - ..textScaleFactor = textScaleFactor + ..textScaler = textScaler ..maxLines = maxLines ..strutStyle = strutStyle ..textWidthBasis = textWidthBasis @@ -5855,7 +5903,7 @@ class RichText extends MultiChildRenderObjectWidget { properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true)); properties.add(EnumProperty('overflow', overflow, defaultValue: TextOverflow.clip)); - properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0)); + properties.add(DiagnosticsProperty('textScaler', textScaler, defaultValue: TextScaler.noScaling)); properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); properties.add(EnumProperty('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent)); properties.add(StringProperty('text', text.toPlainText())); @@ -6130,7 +6178,7 @@ class RawImage extends LeafRenderObjectWidget { /// @override /// Future load(String key) async { /// if (key == 'resources/test') { -/// return ByteData.view(Uint8List.fromList(utf8.encode('Hello World!')).buffer); +/// return ByteData.sublistView(utf8.encode('Hello World!')); /// } /// return ByteData(0); /// } @@ -6165,8 +6213,6 @@ class RawImage extends LeafRenderObjectWidget { /// * [rootBundle], the default asset bundle. class DefaultAssetBundle extends InheritedWidget { /// Creates a widget that determines the default asset bundle for its descendants. - /// - /// The [bundle] and [child] arguments must not be null. const DefaultAssetBundle({ super.key, required this.bundle, @@ -6208,8 +6254,6 @@ class DefaultAssetBundle extends InheritedWidget { /// [onUnmount] callback. class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget { /// Creates an adapter for placing a specific [RenderBox] in the widget tree. - /// - /// The [renderBox] argument must not be null. WidgetToRenderBoxAdapter({ required this.renderBox, this.onBuild, @@ -6436,7 +6480,7 @@ class MouseRegion extends SingleChildRenderObjectWidget { /// Creates a widget that forwards mouse events to callbacks. /// /// By default, all callbacks are empty, [cursor] is [MouseCursor.defer], and - /// [opaque] is true. The [cursor] must not be null. + /// [opaque] is true. const MouseRegion({ super.key, this.onEnter, @@ -6740,20 +6784,33 @@ class RepaintBoundary extends SingleChildRenderObjectWidget { /// /// ## Semantics /// -/// Using this widget may also affect how the semantics subtree underneath this -/// widget is collected. +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@template flutter.widgets.IgnorePointer.semantics} +/// If [ignoring] is true, pointer-related [SemanticsAction]s are removed from +/// the semantics subtree. Otherwise, the subtree remains untouched. +/// {@endtemplate} +/// +/// {@template flutter.widgets.IgnorePointer.ignoringSemantics} +/// The usages of [ignoringSemantics] are deprecated and not recommended. This +/// property was introduced to workaround the semantics behavior of the +/// [IgnorePointer] and its friends before v3.8.0-12.0.pre. +/// +/// Before that version, entire semantics subtree is dropped if [ignoring] is +/// true. Developers can only use [ignoringSemantics] to preserver the semantics +/// subtrees. +/// +/// After that version, with [ignoring] set to true, it only prevents semantics +/// user actions in the semantics subtree but leaves the other +/// [SemanticsProperties] intact. Therefore, the [ignoringSemantics] is no +/// longer needed. /// -/// {@template flutter.widgets.IgnorePointer.Semantics} /// If [ignoringSemantics] is true, the semantics subtree is dropped. Therefore, /// the subtree will be invisible to assistive technologies. /// /// If [ignoringSemantics] is false, the semantics subtree is collected as /// usual. -/// -/// If [ignoringSemantics] is not set, then [ignoring] decides how the -/// semantics subtree is collected. If [ignoring] is true, pointer-related -/// [SemanticsAction]s are removed from the semantics subtree. Otherwise, the -/// subtree remains untouched. /// {@endtemplate} /// /// See also: @@ -6763,8 +6820,6 @@ class RepaintBoundary extends SingleChildRenderObjectWidget { /// * [SliverIgnorePointer], the sliver version of this widget. class IgnorePointer extends SingleChildRenderObjectWidget { /// Creates a widget that is invisible to hit testing. - /// - /// The [ignoring] argument must not be null. const IgnorePointer({ super.key, this.ignoring = true, @@ -6781,7 +6836,7 @@ class IgnorePointer extends SingleChildRenderObjectWidget { /// Regardless of whether this widget is ignored during hit testing, it will /// still consume space during layout and be visible during painting. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.semantics} /// /// Defaults to true. final bool ignoring; @@ -6789,7 +6844,7 @@ class IgnorePointer extends SingleChildRenderObjectWidget { /// Whether the semantics of this widget is ignored when compiling the /// semantics subtree. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} /// /// See [SemanticsNode] for additional information about the semantics tree. @Deprecated( @@ -6844,19 +6899,33 @@ class IgnorePointer extends SingleChildRenderObjectWidget { /// /// ## Semantics /// -/// Using this widget may also affect how the semantics subtree underneath this -/// widget is collected. +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@template flutter.widgets.AbsorbPointer.semantics} +/// If [absorbing] is true, pointer-related [SemanticsAction]s are removed from +/// the semantics subtree. Otherwise, the subtree remains untouched. +/// {@endtemplate} +/// +/// {@template flutter.widgets.AbsorbPointer.ignoringSemantics} +/// The usages of [ignoringSemantics] are deprecated and not recommended. This +/// property was introduced to workaround the semantics behavior of the +/// [IgnorePointer] and its friends before v3.8.0-12.0.pre. /// -/// {@template flutter.widgets.AbsorbPointer.Semantics} -/// If [ignoringSemantics] is true, the semantics subtree is dropped. +/// Before that version, entire semantics subtree is dropped if [absorbing] is +/// true. Developers can only use [ignoringSemantics] to preserver the semantics +/// subtrees. +/// +/// After that version, with [absorbing] set to true, it only prevents semantics +/// user actions in the semantics subtree but leaves the other +/// [SemanticsProperties] intact. Therefore, the [ignoringSemantics] is no +/// longer needed. +/// +/// If [ignoringSemantics] is true, the semantics subtree is dropped. Therefore, +/// the subtree will be invisible to assistive technologies. /// /// If [ignoringSemantics] is false, the semantics subtree is collected as /// usual. -/// -/// If [ignoringSemantics] is not set, then [absorbing] decides how the -/// semantics subtree is collected. If [absorbing] is true, pointer-related -/// [SemanticsAction]s are removed from the semantics subtree. Otherwise, the -/// subtree remains untouched. /// {@endtemplate} /// /// See also: @@ -6865,8 +6934,6 @@ class IgnorePointer extends SingleChildRenderObjectWidget { /// events but is itself invisible to hit testing. class AbsorbPointer extends SingleChildRenderObjectWidget { /// Creates a widget that absorbs pointers during hit testing. - /// - /// The [absorbing] argument must not be null. const AbsorbPointer({ super.key, this.absorbing = true, @@ -6884,7 +6951,7 @@ class AbsorbPointer extends SingleChildRenderObjectWidget { /// testing, it will still consume space during layout and be visible during /// painting. /// - /// {@macro flutter.widgets.AbsorbPointer.Semantics} + /// {@macro flutter.widgets.AbsorbPointer.semantics} /// /// Defaults to true. final bool absorbing; @@ -6892,7 +6959,7 @@ class AbsorbPointer extends SingleChildRenderObjectWidget { /// Whether the semantics of this render object is ignored when compiling the /// semantics tree. /// - /// {@macro flutter.widgets.AbsorbPointer.Semantics} + /// {@macro flutter.widgets.AbsorbPointer.ignoringSemantics} /// /// See [SemanticsNode] for additional information about the semantics tree. @Deprecated( @@ -7001,8 +7068,8 @@ class MetaData extends SingleChildRenderObjectWidget { class Semantics extends SingleChildRenderObjectWidget { /// Creates a semantic annotation. /// - /// The [container] argument must not be null. To create a `const` instance - /// of [Semantics], use the [Semantics.fromProperties] constructor. + /// To create a `const` instance of [Semantics], use the + /// [Semantics.fromProperties] constructor. /// /// See also: /// @@ -7039,6 +7106,7 @@ class Semantics extends SingleChildRenderObjectWidget { bool? hidden, bool? image, bool? liveRegion, + bool? expanded, int? maxValueLength, int? currentValueLength, String? label, @@ -7087,6 +7155,7 @@ class Semantics extends SingleChildRenderObjectWidget { enabled: enabled, checked: checked, mixed: mixed, + expanded: expanded, toggled: toggled, selected: selected, button: button, @@ -7150,8 +7219,6 @@ class Semantics extends SingleChildRenderObjectWidget { ); /// Creates a semantic annotation using [SemanticsProperties]. - /// - /// The [container] and [properties] arguments must not be null. const Semantics.fromProperties({ super.key, super.child, @@ -7442,8 +7509,6 @@ class ExcludeSemantics extends SingleChildRenderObjectWidget { /// * [CustomScrollView], for an explanation of index semantics. class IndexedSemantics extends SingleChildRenderObjectWidget { /// Creates a widget that annotated the first child semantics node with an index. - /// - /// [index] must not be null. const IndexedSemantics({ super.key, required this.index, @@ -7599,8 +7664,6 @@ class KeyedSubtree extends StatelessWidget { /// [builder] callback to create the widget's child. class Builder extends StatelessWidget { /// Creates a widget that delegates its build to a callback. - /// - /// The [builder] argument must not be null. const Builder({ super.key, required this.builder, @@ -7672,8 +7735,6 @@ typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSette /// * [Builder], the platonic stateless widget. class StatefulBuilder extends StatefulWidget { /// Creates a widget that both has state and delegates its build to a callback. - /// - /// The [builder] argument must not be null. const StatefulBuilder({ super.key, required this.builder, @@ -7701,8 +7762,6 @@ class _StatefulBuilderState extends State { /// child on top of that color. class ColoredBox extends SingleChildRenderObjectWidget { /// Creates a widget that paints its area with the specified [Color]. - /// - /// The [color] parameter must not be null. const ColoredBox({ required this.color, super.child, super.key }); /// The color to paint the background area with. @@ -7731,8 +7790,6 @@ class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior { super(behavior: HitTestBehavior.opaque); /// The fill color for this render object. - /// - /// This parameter must not be null. Color get color => _color; Color _color; set color(Color value) { diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index e085542bc077d..d0fdbcf9ca00e 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -26,29 +26,36 @@ export 'dart:ui' show AppLifecycleState, Locale; /// Interface for classes that register with the Widgets layer binding. /// -/// When used as a mixin, provides no-op method implementations. +/// This can be used by any class, not just widgets. It provides an interface +/// which is used by [WidgetsBinding.addObserver] and +/// [WidgetsBinding.removeObserver] to notify objects of changes in the +/// environment, such as changes to the device metrics or accessibility +/// settings. It is used to implement features such as [MediaQuery]. /// -/// See [WidgetsBinding.addObserver] and [WidgetsBinding.removeObserver]. +/// This class can be extended directly, or mixed in, to get default behaviors +/// for all of the handlers. Alternatively it can can be used with the +/// `implements` keyword, in which case all the handlers must be implemented +/// (and the analyzer will list those that have been omitted). /// -/// This class can be extended directly, to get default behaviors for all of the -/// handlers, or can used with the `implements` keyword, in which case all the -/// handlers must be implemented (and the analyzer will list those that have -/// been omitted). +/// To start receiving notifications, call `WidgetsBinding.instance.addObserver` +/// with a reference to the object implementing the [WidgetsBindingObserver] +/// interface. To avoid memory leaks, call +/// `WidgetsBinding.instance.removeObserver` to unregister the object when it +/// reaches the end of its lifecycle. /// /// {@tool dartpad} /// This sample shows how to implement parts of the [State] and /// [WidgetsBindingObserver] protocols necessary to react to application /// lifecycle messages. See [didChangeAppLifecycleState]. /// +/// To respond to other notifications, replace the [didChangeAppLifecycleState] +/// method in this example with other methods from this class. +/// /// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart ** /// {@end-tool} -/// -/// To respond to other notifications, replace the [didChangeAppLifecycleState] -/// method above with other methods from this class. abstract mixin class WidgetsBindingObserver { - /// Called when the system tells the app to pop the current route. - /// For example, on Android, this is called when the user presses - /// the back button. + /// Called when the system tells the app to pop the current route, such as + /// after a system back button press or back gesture. /// /// Observers are notified in registration order until one returns /// true. If none return true, the application quits. @@ -61,6 +68,8 @@ abstract mixin class WidgetsBindingObserver { /// /// This method exposes the `popRoute` notification from /// [SystemChannels.navigation]. + /// + /// {@macro flutter.widgets.AndroidPredictiveBack} Future didPopRoute() => Future.value(false); /// Called when the host tells the application to push a new route onto the @@ -131,10 +140,15 @@ abstract mixin class WidgetsBindingObserver { /// @override /// void initState() { /// super.initState(); + /// WidgetsBinding.instance.addObserver(this); + /// } + /// + /// @override + /// void didChangeDependencies() { + /// super.didChangeDependencies(); /// // [View.of] exposes the view from `WidgetsBinding.instance.platformDispatcher.views` /// // into which this widget is drawn. /// _lastSize = View.of(context).physicalSize; - /// WidgetsBinding.instance.addObserver(this); /// } /// /// @override @@ -240,6 +254,11 @@ abstract mixin class WidgetsBindingObserver { /// documentation for the [WidgetsBindingObserver] class. /// /// This method exposes notifications from [SystemChannels.lifecycle]. + /// + /// See also: + /// + /// * [AppLifecycleListener], an alternative API for responding to + /// application lifecycle changes. void didChangeAppLifecycleState(AppLifecycleState state) { } /// Called when a request is received from the system to exit the application. @@ -272,6 +291,48 @@ abstract mixin class WidgetsBindingObserver { } /// The glue between the widgets layer and the Flutter engine. +/// +/// The [WidgetsBinding] manages a single [Element] tree rooted at [rootElement]. +/// Calling [runApp] (which indirectly calls [attachRootWidget]) bootstraps that +/// element tree. +/// +/// ## Relationship to render trees +/// +/// Multiple render trees may be associated with the element tree. Those are +/// managed by the underlying [RendererBinding]. +/// +/// The element tree is segmented into two types of zones: rendering zones and +/// non-rendering zones. +/// +/// A rendering zone is a part of the element tree that is backed by a render +/// tree and it describes the pixels that are drawn on screen. For elements in +/// this zone, [Element.renderObject] never returns null because the elements +/// are all associated with [RenderObject]s. Almost all widgets can be placed in +/// a rendering zone; notable exceptions are the [View] widget, [ViewCollection] +/// widget, and [RootWidget]. +/// +/// A non-rendering zone is a part of the element tree that is not backed by a +/// render tree. For elements in this zone, [Element.renderObject] returns null +/// because the elements are not associated with any [RenderObject]s. Only +/// widgets that do not produce a [RenderObject] can be used in this zone +/// because there is no render tree to attach the render object to. In other +/// words, [RenderObjectWidget]s cannot be used in this zone. Typically, one +/// would find [InheritedWidget]s, [View]s, and [ViewCollection]s in this zone +/// to inject data across rendering zones into the tree and to organize the +/// rendering zones (and by extension their associated render trees) into a +/// unified element tree. +/// +/// The root of the element tree at [rootElement] starts a non-rendering zone. +/// Within a non-rendering zone, the [View] widget is used to start a rendering +/// zone by bootstrapping a render tree. Within a rendering zone, the +/// [ViewAnchor] can be used to start a new non-rendering zone. +/// +// TODO(goderbauer): Include an example graph showcasing the different zones. +/// +/// To figure out if an element is in a rendering zone it may walk up the tree +/// calling [Element.debugExpectsRenderObjectForSlot] on its ancestors. If it +/// reaches an element that returns false, it is in a non-rendering zone. If it +/// reaches a [RenderObjectElement] ancestor it is in a rendering zone. mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding { @override void initInstances() { @@ -450,23 +511,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }, ); - registerServiceExtension( - name: WidgetsServiceExtensions.fastReassemble.name, - callback: (Map params) async { - // This mirrors the implementation of the 'reassemble' callback registration - // in lib/src/foundation/binding.dart, but with the extra binding config used - // to skip some reassemble work. - final String? className = params['className'] as String?; - BindingBase.debugReassembleConfig = DebugReassembleConfig(widgetName: className); - try { - await reassembleApplication(); - } finally { - BindingBase.debugReassembleConfig = null; - } - return {'type': 'Success'}; - }, - ); - // Expose the ability to send Widget rebuilds as [Timeline] events. registerBoolServiceExtension( name: WidgetsServiceExtensions.profileWidgetBuilds.name, @@ -505,7 +549,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB Future _forceRebuild() { if (rootElement != null) { - buildOwner!.reassemble(rootElement!, null); + buildOwner!.reassemble(rootElement!); return endOfFrame; } return Future.value(); @@ -568,7 +612,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override Future handleRequestAppExit() async { bool didCancel = false; - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) { didCancel = true; // Don't early return. For the case where someone is just using the @@ -582,7 +626,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handleMetricsChanged() { super.handleMetricsChanged(); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeMetrics(); } } @@ -590,7 +634,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handleTextScaleFactorChanged() { super.handleTextScaleFactorChanged(); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeTextScaleFactor(); } } @@ -598,7 +642,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handlePlatformBrightnessChanged() { super.handlePlatformBrightnessChanged(); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangePlatformBrightness(); } } @@ -606,7 +650,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handleAccessibilityFeaturesChanged() { super.handleAccessibilityFeaturesChanged(); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeAccessibilityFeatures(); } } @@ -618,6 +662,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// See [dart:ui.PlatformDispatcher.onLocaleChanged]. @protected @mustCallSuper + @visibleForTesting void handleLocaleChanged() { dispatchLocalesChanged(platformDispatcher.locales); } @@ -631,7 +676,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @protected @mustCallSuper void dispatchLocalesChanged(List? locales) { - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeLocales(locales); } } @@ -645,7 +690,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @protected @mustCallSuper void dispatchAccessibilityFeaturesChanged() { - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeAccessibilityFeatures(); } } @@ -664,7 +709,29 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// /// This method exposes the `popRoute` notification from /// [SystemChannels.navigation]. + /// + /// {@template flutter.widgets.AndroidPredictiveBack} + /// ## Handling backs ahead of time + /// + /// Not all system backs will result in a call to this method. Some are + /// handled entirely by the system without informing the Flutter framework. + /// + /// Android API 33+ introduced a feature called predictive back, which allows + /// the user to peek behind the current app or route during a back gesture and + /// then decide to cancel or commit the back. Flutter enables or disables this + /// feature ahead of time, before a back gesture occurs, and back gestures + /// that trigger predictive back are handled entirely by the system and do not + /// trigger this method here in the framework. + /// + /// By default, the framework communicates when it would like to handle system + /// back gestures using [SystemNavigator.setFrameworkHandlesBack] in + /// [WidgetsApp]. This is done automatically based on the status of the + /// [Navigator] stack and the state of any [PopScope] widgets present. + /// Developers can manually set this by calling the method directly or by + /// using [NavigationNotification]. + /// {@endtemplate} @protected + @visibleForTesting Future handlePopRoute() async { for (final WidgetsBindingObserver observer in List.of(_observers)) { if (await observer.didPopRoute()) { @@ -686,6 +753,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// [SystemChannels.navigation]. @protected @mustCallSuper + @visibleForTesting Future handlePushRoute(String route) async { final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route)); for (final WidgetsBindingObserver observer in List.of(_observers)) { @@ -722,7 +790,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handleAppLifecycleStateChanged(AppLifecycleState state) { super.handleAppLifecycleStateChanged(state); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didChangeAppLifecycleState(state); } } @@ -730,7 +798,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override void handleMemoryPressure() { super.handleMemoryPressure(); - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.of(_observers)) { observer.didHaveMemoryPressure(); } } @@ -967,6 +1035,8 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB Widget wrapWithDefaultView(Widget rootWidget) { return View( view: platformDispatcher.implicitView!, + deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: pipelineOwner, + deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: renderView, child: rootWidget, ); } @@ -992,13 +1062,25 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a /// widget and attaches it to the render tree. void attachRootWidget(Widget rootWidget) { - final bool isBootstrapFrame = rootElement == null; - _readyToProduceFrames = true; - _rootElement = RenderObjectToWidgetAdapter( - container: renderView, + attachToBuildOwner(RootWidget( debugShortDescription: '[root]', child: rootWidget, - ).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement?); + )); + } + + /// Called by [attachRootWidget] to attach the provided [RootWidget] to the + /// [buildOwner]. + /// + /// This creates the [rootElement], if necessary, or re-uses an existing one. + /// + /// This method is rarely called directly, but it can be useful in tests to + /// restore the element tree to a previous version by providing the + /// [RootWidget] of that version (see [WidgetTester.restartAndRestore] for an + /// exemplary use case). + void attachToBuildOwner(RootWidget widget) { + final bool isBootstrapFrame = rootElement == null; + _readyToProduceFrames = true; + _rootElement = widget.attach(buildOwner!, rootElement as RootElement?); if (isBootstrapFrame) { SchedulerBinding.instance.ensureVisualUpdate(); } @@ -1018,7 +1100,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }()); if (rootElement != null) { - buildOwner!.reassemble(rootElement!, BindingBase.debugReassembleConfig); + buildOwner!.reassemble(rootElement!); } return super.performReassemble(); } @@ -1080,6 +1162,23 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// /// Initializes the binding using [WidgetsFlutterBinding] if necessary. /// +/// ## Application shutdown +/// +/// This widget tree is not torn down when the application shuts down, because +/// there is no way to predict when that will happen. For example, a user could +/// physically remove power from their device, or the application could crash +/// unexpectedly, or the malware on the device could forcibly terminate the +/// process. +/// +/// Applications are responsible for ensuring that they are well-behaved +/// even in the face of a rapid unscheduled termination. +/// +/// To artificially cause the entire widget tree to be disposed, consider +/// calling [runApp] with a widget such as [SizedBox.shrink]. +/// +/// To listen for platform shutdown messages (and other lifecycle changes), +/// consider the [AppLifecycleListener] API. +/// /// See also: /// /// * [WidgetsBinding.attachRootWidget], which creates the root widget for the @@ -1113,52 +1212,40 @@ void debugDumpApp() { debugPrint(_debugDumpAppString()); } -/// A bridge from a [RenderObject] to an [Element] tree. +/// A widget for the root of the widget tree. /// -/// The given container is the [RenderObject] that the [Element] tree should be -/// inserted into. It must be a [RenderObject] that implements the -/// [RenderObjectWithChildMixin] protocol. The type argument `T` is the kind of -/// [RenderObject] that the container expects as its child. +/// Exposes an [attach] method to attach the widget tree to a [BuildOwner]. That +/// method also bootstraps the element tree. /// -/// Used by [runApp] to bootstrap applications. -class RenderObjectToWidgetAdapter extends RenderObjectWidget { - /// Creates a bridge from a [RenderObject] to an [Element] tree. - /// - /// Used by [WidgetsBinding] to attach the root widget to the [RenderView]. - RenderObjectToWidgetAdapter({ +/// Used by [WidgetsBinding.attachRootWidget] (which is indirectly called by +/// [runApp]) to bootstrap applications. +class RootWidget extends Widget { + /// Creates a [RootWidget]. + const RootWidget({ + super.key, this.child, - required this.container, this.debugShortDescription, - }) : super(key: GlobalObjectKey(container)); + }); /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget? child; - /// The [RenderObject] that is the parent of the [Element] created by this widget. - final RenderObjectWithChildMixin container; - /// A short description of this widget used by debugging aids. final String? debugShortDescription; @override - RenderObjectToWidgetElement createElement() => RenderObjectToWidgetElement(this); - - @override - RenderObjectWithChildMixin createRenderObject(BuildContext context) => container; - - @override - void updateRenderObject(BuildContext context, RenderObject renderObject) { } + RootElement createElement() => RootElement(this); - /// Inflate this widget and actually set the resulting [RenderObject] as the - /// child of [container]. + /// Inflate this widget and attaches it to the provided [BuildOwner]. /// /// If `element` is null, this function will create a new element. Otherwise, /// the given element will have an update scheduled to switch to this widget. /// - /// Used by [runApp] to bootstrap applications. - RenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement? element ]) { + /// Used by [WidgetsBinding.attachToBuildOwner] (which is indirectly called by + /// [runApp]) to bootstrap applications. + RootElement attach(BuildOwner owner, [ RootElement? element ]) { if (element == null) { owner.lockState(() { element = createElement(); @@ -1166,7 +1253,7 @@ class RenderObjectToWidgetAdapter extends RenderObjectWi element!.assignOwner(owner); }); owner.buildScope(element!, () { - element!.mount(null, null); + element!.mount(/* parent */ null, /* slot */ null); }); } else { element._newWidget = this; @@ -1179,28 +1266,22 @@ class RenderObjectToWidgetAdapter extends RenderObjectWi String toStringShort() => debugShortDescription ?? super.toStringShort(); } -/// The root of the element tree that is hosted by a [RenderObject]. +/// The root of the element tree. /// -/// This element class is the instantiation of a [RenderObjectToWidgetAdapter] -/// widget. It can be used only as the root of an [Element] tree (it cannot be -/// mounted into another [Element]; it's parent must be null). +/// This element class is the instantiation of a [RootWidget]. It can be used +/// only as the root of an [Element] tree (it cannot be mounted into another +/// [Element]; its parent must be null). /// -/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter] -/// whose container is the [RenderView] that connects to the Flutter engine. In -/// this usage, it is normally instantiated by the bootstrapping logic in the -/// [WidgetsFlutterBinding] singleton created by [runApp]. -class RenderObjectToWidgetElement extends RenderObjectElement with RootElementMixin { - /// Creates an element that is hosted by a [RenderObject]. - /// - /// The [RenderObject] created by this element is not automatically set as a - /// child of the hosting [RenderObject]. To actually attach this element to - /// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree]. - RenderObjectToWidgetElement(RenderObjectToWidgetAdapter super.widget); +/// In typical usage, it will be instantiated for a [RootWidget] by calling +/// [RootWidget.attach]. In this usage, it is normally instantiated by the +/// bootstrapping logic in the [WidgetsFlutterBinding] singleton created by +/// [runApp]. +class RootElement extends Element with RootElementMixin { + /// Creates a [RootElement] for the provided [RootWidget]. + RootElement(RootWidget super.widget); Element? _child; - static const Object _rootChildSlot = Object(); - @override void visitChildren(ElementVisitor visitor) { if (_child != null) { @@ -1217,14 +1298,15 @@ class RenderObjectToWidgetElement extends RenderObjectEl @override void mount(Element? parent, Object? newSlot) { - assert(parent == null); + assert(parent == null); // We are the root! super.mount(parent, newSlot); _rebuild(); assert(_child != null); + super.performRebuild(); // clears the "dirty" flag } @override - void update(RenderObjectToWidgetAdapter newWidget) { + void update(RootWidget newWidget) { super.update(newWidget); assert(widget == newWidget); _rebuild(); @@ -1232,25 +1314,24 @@ class RenderObjectToWidgetElement extends RenderObjectEl // When we are assigned a new widget, we store it here // until we are ready to update to it. - Widget? _newWidget; + RootWidget? _newWidget; @override void performRebuild() { if (_newWidget != null) { // _newWidget can be null if, for instance, we were rebuilt // due to a reassemble. - final Widget newWidget = _newWidget!; + final RootWidget newWidget = _newWidget!; _newWidget = null; - update(newWidget as RenderObjectToWidgetAdapter); + update(newWidget); } super.performRebuild(); assert(_newWidget == null); } - @pragma('vm:notify-debugger-on-exception') void _rebuild() { try { - _child = updateChild(_child, (widget as RenderObjectToWidgetAdapter).child, _rootChildSlot); + _child = updateChild(_child, (widget as RootWidget).child, /* slot */ null); } catch (exception, stack) { final FlutterErrorDetails details = FlutterErrorDetails( exception: exception, @@ -1259,31 +1340,18 @@ class RenderObjectToWidgetElement extends RenderObjectEl context: ErrorDescription('attaching to the render tree'), ); FlutterError.reportError(details); - final Widget error = ErrorWidget.builder(details); - _child = updateChild(null, error, _rootChildSlot); + // No error widget possible here since it wouldn't have a view to render into. + _child = null; } - } - - @override - RenderObjectWithChildMixin get renderObject => super.renderObject as RenderObjectWithChildMixin; - @override - void insertRenderObjectChild(RenderObject child, Object? slot) { - assert(slot == _rootChildSlot); - assert(renderObject.debugValidateChild(child)); - renderObject.child = child as T; } @override - void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { - assert(false); - } + bool get debugDoingBuild => false; // This element doesn't have a build phase. @override - void removeRenderObjectChild(RenderObject child, Object? slot) { - assert(renderObject.child == child); - renderObject.child = null; - } + // There is no ancestor RenderObjectElement that the render object could be attached to. + bool debugExpectsRenderObjectForSlot(Object? slot) => false; } /// A concrete binding for applications based on the Widgets framework. diff --git a/packages/flutter/lib/src/widgets/color_filter.dart b/packages/flutter/lib/src/widgets/color_filter.dart index ea2ef46ed8a48..bd01095792d54 100644 --- a/packages/flutter/lib/src/widgets/color_filter.dart +++ b/packages/flutter/lib/src/widgets/color_filter.dart @@ -32,8 +32,6 @@ import 'framework.dart'; @immutable class ColorFiltered extends SingleChildRenderObjectWidget { /// Creates a widget that applies a [ColorFilter] to its child. - /// - /// The [colorFilter] must not be null. const ColorFiltered({required this.colorFilter, super.child, super.key}); /// The color filter to apply to the child of this widget. diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 486bce79ce7fd..f6967ce1d0745 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -56,8 +56,7 @@ import 'image.dart'; class DecoratedBox extends SingleChildRenderObjectWidget { /// Creates a widget that paints a [Decoration]. /// - /// The [decoration] and [position] arguments must not be null. By default the - /// decoration paints behind the child. + /// By default the decoration paints behind the child. const DecoratedBox({ super.key, required this.decoration, diff --git a/packages/flutter/lib/src/widgets/context_menu_button_item.dart b/packages/flutter/lib/src/widgets/context_menu_button_item.dart index e355ab41e3da0..896cbab9d20ec 100644 --- a/packages/flutter/lib/src/widgets/context_menu_button_item.dart +++ b/packages/flutter/lib/src/widgets/context_menu_button_item.dart @@ -26,6 +26,15 @@ enum ContextMenuButtonType { /// A button that deletes the current text selection. delete, + /// A button that looks up the current text selection. + lookUp, + + /// A button that launches a web search for the current text selection. + searchWeb, + + /// A button that displays the share screen for the current text selection. + share, + /// A button for starting Live Text input. /// /// See also: diff --git a/packages/flutter/lib/src/widgets/context_menu_controller.dart b/packages/flutter/lib/src/widgets/context_menu_controller.dart index df75571d46d44..6e29a2b0ad9bf 100644 --- a/packages/flutter/lib/src/widgets/context_menu_controller.dart +++ b/packages/flutter/lib/src/widgets/context_menu_controller.dart @@ -81,6 +81,7 @@ class ContextMenuController { /// * [remove], which removes only the current instance. static void removeAny() { _menuOverlayEntry?.remove(); + _menuOverlayEntry?.dispose(); _menuOverlayEntry = null; if (_shownInstance != null) { _shownInstance!.onRemove?.call(); diff --git a/packages/flutter/lib/src/widgets/decorated_sliver.dart b/packages/flutter/lib/src/widgets/decorated_sliver.dart index fffe3b4113bee..3cc1b83e87c67 100644 --- a/packages/flutter/lib/src/widgets/decorated_sliver.dart +++ b/packages/flutter/lib/src/widgets/decorated_sliver.dart @@ -40,8 +40,7 @@ import 'image.dart'; class DecoratedSliver extends SingleChildRenderObjectWidget { /// Creates a widget that paints a [Decoration]. /// - /// The [decoration] and [position] arguments must not be null. By default the - /// decoration paints behind the child. + /// By default the decoration paints behind the child. const DecoratedSliver({ super.key, required this.decoration, diff --git a/packages/flutter/lib/src/widgets/dismissible.dart b/packages/flutter/lib/src/widgets/dismissible.dart index 87f9753511a47..0fe4bdfcd51c7 100644 --- a/packages/flutter/lib/src/widgets/dismissible.dart +++ b/packages/flutter/lib/src/widgets/dismissible.dart @@ -90,12 +90,12 @@ enum DismissDirection { class Dismissible extends StatefulWidget { /// Creates a widget that can be dismissed. /// - /// The [key] argument must not be null because [Dismissible]s are commonly - /// used in lists and removed from the list when dismissed. Without keys, the - /// default behavior is to sync widgets based on their index in the list, - /// which means the item after the dismissed item would be synced with the - /// state of the dismissed item. Using keys causes the widgets to sync - /// according to their keys and avoids this pitfall. + /// The [key] argument is required because [Dismissible]s are commonly used in + /// lists and removed from the list when dismissed. Without keys, the default + /// behavior is to sync widgets based on their index in the list, which means + /// the item after the dismissed item would be synced with the state of the + /// dismissed item. Using keys causes the widgets to sync according to their + /// keys and avoids this pitfall. const Dismissible({ required Key key, required this.child, diff --git a/packages/flutter/lib/src/widgets/disposable_build_context.dart b/packages/flutter/lib/src/widgets/disposable_build_context.dart index 2283acaeec478..f3bcf6abd7f30 100644 --- a/packages/flutter/lib/src/widgets/disposable_build_context.dart +++ b/packages/flutter/lib/src/widgets/disposable_build_context.dart @@ -26,7 +26,7 @@ class DisposableBuildContext { /// /// Creators must call [dispose] when the [State] is disposed. /// - /// The [State] must not be null, and [State.mounted] must be true. + /// [State.mounted] must be true. DisposableBuildContext(T this._state) : assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.'); diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index f55806353bd5f..bf323cf6de0ba 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -20,6 +20,12 @@ import 'view.dart'; /// Used by [DragTarget.onWillAccept]. typedef DragTargetWillAccept = bool Function(T? data); +/// Signature for determining whether the given data will be accepted by a [DragTarget], +/// based on provided information. +/// +/// Used by [DragTarget.onWillAcceptWithDetails]. +typedef DragTargetWillAcceptWithDetails = bool Function(DragTargetDetails details); + /// Signature for causing a [DragTarget] to accept the given data. /// /// Used by [DragTarget.onAccept]. @@ -158,8 +164,7 @@ Offset pointerDragAnchorStrategy(Draggable draggable, BuildContext conte class Draggable extends StatefulWidget { /// Creates a widget that can be dragged to a [DragTarget]. /// - /// The [child] and [feedback] arguments must not be null. If - /// [maxSimultaneousDrags] is non-null, it must be non-negative. + /// If [maxSimultaneousDrags] is non-null, it must be non-negative. const Draggable({ super.key, required this.child, @@ -388,8 +393,7 @@ class Draggable extends StatefulWidget { class LongPressDraggable extends Draggable { /// Creates a widget that can be dragged starting from long press. /// - /// The [child] and [feedback] arguments must not be null. If - /// [maxSimultaneousDrags] is non-null, it must be non-negative. + /// If [maxSimultaneousDrags] is non-null, it must be non-negative. const LongPressDraggable({ super.key, required super.child, @@ -580,8 +584,6 @@ class DraggableDetails { /// Represents the details when a pointer event occurred on the [DragTarget]. class DragTargetDetails { /// Creates details for a [DragTarget] callback. - /// - /// The [offset] must not be null. DragTargetDetails({required this.data, required this.offset}); /// The data that was dropped onto this [DragTarget]. @@ -606,18 +608,17 @@ class DragTargetDetails { /// * [LongPressDraggable] class DragTarget extends StatefulWidget { /// Creates a widget that receives drags. - /// - /// The [builder] argument must not be null. const DragTarget({ super.key, required this.builder, this.onWillAccept, + this.onWillAcceptWithDetails, this.onAccept, this.onAcceptWithDetails, this.onLeave, this.onMove, this.hitTestBehavior = HitTestBehavior.translucent, - }); + }) : assert(onWillAccept == null || onWillAcceptWithDetails == null, "Don't pass both onWillAccept and onWillAcceptWithDetails."); /// Called to build the contents of this widget. /// @@ -631,8 +632,25 @@ class DragTarget extends StatefulWidget { /// Called when a piece of data enters the target. This will be followed by /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or /// [onLeave], if the drag leaves the target. + /// + /// Equivalent to [onWillAcceptWithDetails], but only includes the data. + /// + /// Must not be provided if [onWillAcceptWithDetails] is provided. final DragTargetWillAccept? onWillAccept; + /// Called to determine whether this widget is interested in receiving a given + /// piece of data being dragged over this drag target. + /// + /// Called when a piece of data enters the target. This will be followed by + /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or + /// [onLeave], if the drag leaves the target. + /// + /// Equivalent to [onWillAccept], but with information, including the data, + /// in a [DragTargetDetails]. + /// + /// Must not be provided if [onWillAccept] is provided. + final DragTargetWillAcceptWithDetails? onWillAcceptWithDetails; + /// Called when an acceptable piece of data was dropped over this drag target. /// /// Equivalent to [onAcceptWithDetails], but only includes the data. @@ -684,7 +702,13 @@ class _DragTargetState extends State> { bool didEnter(_DragAvatar avatar) { assert(!_candidateAvatars.contains(avatar)); assert(!_rejectedAvatars.contains(avatar)); - if (widget.onWillAccept == null || widget.onWillAccept!(avatar.data as T?)) { + final bool resolvedWillAccept = (widget.onWillAccept == null && + widget.onWillAcceptWithDetails == null) || + (widget.onWillAccept != null && + widget.onWillAccept!(avatar.data as T?)) || + (widget.onWillAcceptWithDetails != null && + widget.onWillAcceptWithDetails!(DragTargetDetails(data: avatar.data! as T, offset: avatar._lastOffset!))); + if (resolvedWillAccept) { setState(() { _candidateAvatars.add(avatar); }); diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart index 0a63839619d86..e8e56fc47ab34 100644 --- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart +++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart @@ -295,9 +295,6 @@ class DraggableScrollableController extends ChangeNotifier { /// {@end-tool} class DraggableScrollableSheet extends StatefulWidget { /// Creates a widget that can be dragged and scrolled in a single gesture. - /// - /// The [builder], [initialChildSize], [minChildSize], [maxChildSize] and - /// [expand] parameters must not be null. const DraggableScrollableSheet({ super.key, this.initialChildSize = 0.5, @@ -718,7 +715,11 @@ class _DraggableScrollableSheetState extends State { @override void dispose() { - widget.controller?._detach(disposeExtent: true); + if (widget.controller == null) { + _extent.dispose(); + } else { + widget.controller!._detach(disposeExtent: true); + } _scrollController.dispose(); super.dispose(); } @@ -1015,23 +1016,20 @@ class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleCo /// in library users' code). Generally, it's easier to control the sheet /// directly by creating a controller and passing the controller to the sheet in /// its constructor (see [DraggableScrollableSheet.controller]). -class DraggableScrollableActuator extends StatelessWidget { +class DraggableScrollableActuator extends StatefulWidget { /// Creates a widget that can notify descendent [DraggableScrollableSheet]s /// to reset to their initial position. /// /// The [child] parameter is required. - DraggableScrollableActuator({ + const DraggableScrollableActuator({ super.key, required this.child, }); /// This child's [DraggableScrollableSheet] descendant will be reset when the /// [reset] method is applied to a context that includes it. - /// - /// Must not be null. final Widget child; - final _ResetNotifier _notifier = _ResetNotifier(); /// Notifies any descendant [DraggableScrollableSheet] that it should reset /// to its initial position. @@ -1047,15 +1045,33 @@ class DraggableScrollableActuator extends StatelessWidget { return notifier._sendReset(); } + @override + State createState() => _DraggableScrollableActuatorState(); +} + +class _DraggableScrollableActuatorState extends State { + final _ResetNotifier _notifier = _ResetNotifier(); + @override Widget build(BuildContext context) { - return _InheritedResetNotifier(notifier: _notifier, child: child); + return _InheritedResetNotifier(notifier: _notifier, child: widget.child); + } + + @override + void dispose() { + _notifier.dispose(); + super.dispose(); } } /// A [ChangeNotifier] to use with [InheritedResetNotifier] to notify /// descendants that they should reset to initial state. class _ResetNotifier extends ChangeNotifier { + _ResetNotifier() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } /// Whether someone called [sendReset] or not. /// /// This flag should be reset after checking it. @@ -1077,8 +1093,6 @@ class _ResetNotifier extends ChangeNotifier { class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { /// Creates an [InheritedNotifier] that the [DraggableScrollableSheet] will /// listen to for an indication that it should reset itself back to [DraggableScrollableSheet.initialChildSize]. - /// - /// The [child] and [notifier] properties must not be null. const _InheritedResetNotifier({ required super.child, required _ResetNotifier super.notifier, diff --git a/packages/flutter/lib/src/widgets/dual_transition_builder.dart b/packages/flutter/lib/src/widgets/dual_transition_builder.dart index bc5b5b3a3be29..5f1a1868e4a55 100644 --- a/packages/flutter/lib/src/widgets/dual_transition_builder.dart +++ b/packages/flutter/lib/src/widgets/dual_transition_builder.dart @@ -32,9 +32,6 @@ typedef AnimatedTransitionBuilder = Widget Function( /// any descendant widget is lost when the transition starts or completes. class DualTransitionBuilder extends StatefulWidget { /// Creates a [DualTransitionBuilder]. - /// - /// The [animation], [forwardBuilder], and [reverseBuilder] arguments are - /// required and must not be null. const DualTransitionBuilder({ super.key, required this.animation, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 859dfa89bd24f..c4c376565aae1 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -206,7 +206,7 @@ class TextEditingController extends ValueNotifier { value == null || !value.composing.isValid || value.isComposingRangeValid, 'New TextEditingValue $value has an invalid non-empty composing range ' '${value.composing}. It is recommended to use a valid composing range, ' - 'even for readonly text fields', + 'even for readonly text fields.', ), super(value ?? TextEditingValue.empty); @@ -235,7 +235,7 @@ class TextEditingController extends ValueNotifier { !newValue.composing.isValid || newValue.isComposingRangeValid, 'New TextEditingValue $newValue has an invalid non-empty composing range ' '${newValue.composing}. It is recommended to use a valid composing range, ' - 'even for readonly text fields', + 'even for readonly text fields.', ); super.value = newValue; } @@ -369,26 +369,26 @@ class ToolbarOptions { /// Whether to show copy option in toolbar. /// - /// Defaults to false. Must not be null. + /// Defaults to false. final bool copy; /// Whether to show cut option in toolbar. /// /// If [EditableText.readOnly] is set to true, cut will be disabled regardless. /// - /// Defaults to false. Must not be null. + /// Defaults to false. final bool cut; /// Whether to show paste option in toolbar. /// /// If [EditableText.readOnly] is set to true, paste will be disabled regardless. /// - /// Defaults to false. Must not be null. + /// Defaults to false. final bool paste; /// Whether to show select all option in toolbar. /// - /// Defaults to false. Must not be null. + /// Defaults to false. final bool selectAll; } @@ -547,8 +547,11 @@ class _DiscreteKeyFrameSimulation extends Simulation { /// /// This widget interacts with the [TextInput] service to let the user edit the /// text it contains. It also provides scrolling, selection, and cursor -/// movement. This widget does not provide any focus management (e.g., -/// tap-to-focus). +/// movement. +/// +/// The [EditableText] widget is a low-level widget that is intended as a +/// building block for custom widget sets. For a complete user experience, +/// consider using a [TextField] or [CupertinoTextField]. /// /// ## Handling User Input /// @@ -662,13 +665,14 @@ class _DiscreteKeyFrameSimulation extends Simulation { /// /// ## Gesture Events Handling /// -/// This widget provides rudimentary, platform-agnostic gesture handling for -/// user actions such as tapping, long-pressing and scrolling when -/// [rendererIgnoresPointer] is false (false by default). To tightly conform -/// to the platform behavior with respect to input gestures in text fields, use -/// [TextField] or [CupertinoTextField]. For custom selection behavior, call -/// methods such as [RenderEditable.selectPosition], -/// [RenderEditable.selectWord], etc. programmatically. +/// When [rendererIgnoresPointer] is false (the default), this widget provides +/// rudimentary, platform-agnostic gesture handling for user actions such as +/// tapping, long-pressing, and scrolling. +/// +/// To provide more complete gesture handling, including double-click to select +/// a word, drag selection, and platform-specific handling of gestures such as +/// long presses, consider setting [rendererIgnoresPointer] to true and using +/// [TextSelectionGestureDetectorBuilder]. /// /// {@template flutter.widgets.editableText.showCaretOnScreen} /// ## Keep the caret visible when focused @@ -696,7 +700,7 @@ class _DiscreteKeyFrameSimulation extends Simulation { /// a currency value text field. The following example demonstrates how to /// suppress the default accessibility announcements by always announcing /// the content of the text field as a US currency value (the `\$` inserts -/// a dollar sign, the `$newText interpolates the `newText` variable): +/// a dollar sign, the `$newText` interpolates the `newText` variable): /// /// ```dart /// onChanged: (String newText) { @@ -726,14 +730,6 @@ class EditableText extends StatefulWidget { /// /// The text cursor is not shown if [showCursor] is false or if [showCursor] /// is null (the default) and [readOnly] is true. - /// - /// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus], - /// [showSelectionHandles], [enableInteractiveSelection], [forceLine], - /// [style], [cursorColor], [cursorOpacityAnimates], [backgroundCursorColor], - /// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle], - /// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding], - /// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer], - /// [readOnly], and [enableIMEPersonalizedLearning] arguments must not be null. EditableText({ super.key, required this.controller, @@ -752,7 +748,13 @@ class EditableText extends StatefulWidget { this.textAlign = TextAlign.start, this.textDirection, this.locale, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) this.textScaleFactor, + this.textScaler, this.maxLines = 1, this.minLines, this.expands = false, @@ -886,7 +888,7 @@ class EditableText extends StatefulWidget { /// copied with copy or cut. If [readOnly] is also true, then the text cannot /// be selected. /// - /// Defaults to false. Cannot be null. + /// Defaults to false. /// {@endtemplate} final bool obscureText; @@ -902,7 +904,7 @@ class EditableText extends StatefulWidget { /// When this is set to true, the text cannot be modified /// by any shortcut or keyboard operation. The text is still selectable. /// - /// Defaults to false. Must not be null. + /// Defaults to false. /// {@endtemplate} final bool readOnly; @@ -911,7 +913,7 @@ class EditableText extends StatefulWidget { /// When this is set to false, the width will be based on text width, which /// will also be affected by [textWidthBasis]. /// - /// Defaults to true. Must not be null. + /// Defaults to true. /// /// See also: /// @@ -951,7 +953,7 @@ class EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.autocorrect} /// Whether to enable autocorrection. /// - /// Defaults to true. Cannot be null. + /// Defaults to true. /// {@endtemplate} final bool autocorrect; @@ -1000,14 +1002,14 @@ class EditableText extends StatefulWidget { if (_strutStyle == null) { return StrutStyle.fromTextStyle(style, forceStrutHeight: true); } - return _strutStyle!.inheritFromTextStyle(style); + return _strutStyle.inheritFromTextStyle(style); } final StrutStyle? _strutStyle; /// {@template flutter.widgets.editableText.textAlign} /// How the text should be aligned horizontally. /// - /// Defaults to [TextAlign.start] and cannot be null. + /// Defaults to [TextAlign.start]. /// {@endtemplate} final TextAlign textAlign; @@ -1035,7 +1037,7 @@ class EditableText extends StatefulWidget { /// Only supports text keyboards, other keyboard types will ignore this /// configuration. Capitalization is locale-aware. /// - /// Defaults to [TextCapitalization.none]. Must not be null. + /// Defaults to [TextCapitalization.none]. /// /// See also: /// @@ -1054,6 +1056,9 @@ class EditableText extends StatefulWidget { final Locale? locale; /// {@template flutter.widgets.editableText.textScaleFactor} + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than @@ -1062,11 +1067,17 @@ class EditableText extends StatefulWidget { /// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. /// {@endtemplate} + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) final double? textScaleFactor; + /// {@macro flutter.painting.textPainter.textScaler} + final TextScaler? textScaler; + /// The color to use when painting the cursor. - /// - /// Cannot be null. final Color cursorColor; /// The color to use when painting the autocorrection Rect. @@ -1084,8 +1095,7 @@ class EditableText extends StatefulWidget { /// The color to use when painting the background cursor aligned with the text /// while rendering the floating cursor. /// - /// Cannot be null. By default it is the disabled grey color from - /// CupertinoColors. + /// Typically this would be set to [CupertinoColors.inactiveGray]. final Color backgroundCursorColor; /// {@template flutter.widgets.editableText.maxLines} @@ -1221,7 +1231,7 @@ class EditableText extends StatefulWidget { /// If true, the keyboard will open as soon as this text field obtains focus. /// Otherwise, the keyboard is only shown after the user taps the text field. /// - /// Defaults to false. Cannot be null. + /// Defaults to false. /// {@endtemplate} // See https://github.com/flutter/flutter/issues/7035 for the rationale for this // keyboard behavior. @@ -1238,11 +1248,14 @@ class EditableText extends StatefulWidget { final Color? selectionColor; /// {@template flutter.widgets.editableText.selectionControls} - /// Optional delegate for building the text selection handles and toolbar. + /// Optional delegate for building the text selection handles. /// - /// The [EditableText] widget used on its own will not trigger the display - /// of the selection toolbar by itself. The toolbar is shown by calling - /// [EditableTextState.showToolbar] in response to an appropriate user event. + /// Historically, this field also controlled the toolbar. This is now handled + /// by [contextMenuBuilder] instead. However, for backwards compatibility, when + /// [selectionControls] is set to an object that does not mix in + /// [TextSelectionHandleControls], [contextMenuBuilder] is ignored and the + /// [TextSelectionControls.buildToolbar] method is used instead. + /// {@endtemplate} /// /// See also: /// @@ -1252,7 +1265,6 @@ class EditableText extends StatefulWidget { /// * [TextField], a Material Design themed wrapper of [EditableText], which /// shows the selection toolbar upon appropriate user events based on the /// user's platform set in [ThemeData.platform]. - /// {@endtemplate} final TextSelectionControls? selectionControls; /// {@template flutter.widgets.editableText.keyboardType} @@ -1454,10 +1466,28 @@ class EditableText extends StatefulWidget { /// the editing position. final MouseCursor? mouseCursor; - /// If true, the [RenderEditable] created by this widget will not handle - /// pointer events, see [RenderEditable] and [RenderEditable.ignorePointer]. + /// Whether the caller will provide gesture handling (true), or if the + /// [EditableText] is expected to handle basic gestures (false). + /// + /// When this is false, the [EditableText] (or more specifically, the + /// [RenderEditable]) enables some rudimentary gestures (tap to position the + /// cursor, long-press to select all, and some scrolling behavior). + /// + /// These behaviors are sufficient for debugging purposes but are inadequate + /// for user-facing applications. To enable platform-specific behaviors, use a + /// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set + /// [rendererIgnoresPointer] to true. + /// + /// When [rendererIgnoresPointer] is true true, the [RenderEditable] created + /// by this widget will not handle pointer events. /// /// This property is false by default. + /// + /// See also: + /// + /// * [RenderEditable.ignorePointer], which implements this feature. + /// * [TextSelectionGestureDetectorBuilder], which implements platform-specific + /// gestures and behaviors. final bool rendererIgnoresPointer; /// {@template flutter.widgets.editableText.cursorWidth} @@ -1761,6 +1791,11 @@ class EditableText extends StatefulWidget { /// `buttonItems` represents the buttons that would be built by default for /// this widget. /// + /// For backwards compatibility, when [selectionControls] is set to an object + /// that does not mix in [TextSelectionHandleControls], [contextMenuBuilder] + /// is ignored and the [TextSelectionControls.buildToolbar] method is used + /// instead. + /// /// {@tool dartpad} /// This example shows how to customize the menu, in this case by keeping the /// default buttons for the platform but modifying their appearance. @@ -1835,6 +1870,9 @@ class EditableText extends StatefulWidget { required final VoidCallback? onCut, required final VoidCallback? onPaste, required final VoidCallback? onSelectAll, + required final VoidCallback? onLookUp, + required final VoidCallback? onSearchWeb, + required final VoidCallback? onShare, required final VoidCallback? onLiveTextInput, }) { final List resultButtonItem = []; @@ -1865,6 +1903,21 @@ class EditableText extends StatefulWidget { onPressed: onSelectAll, type: ContextMenuButtonType.selectAll, ), + if (onLookUp != null) + ContextMenuButtonItem( + onPressed: onLookUp, + type: ContextMenuButtonType.lookUp, + ), + if (onSearchWeb != null) + ContextMenuButtonItem( + onPressed: onSearchWeb, + type: ContextMenuButtonType.searchWeb, + ), + if (onShare != null) + ContextMenuButtonItem( + onPressed: onShare, + type: ContextMenuButtonType.share, + ), ]); } @@ -2039,7 +2092,7 @@ class EditableText extends StatefulWidget { properties.add(EnumProperty('textAlign', textAlign, defaultValue: null)); properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(DiagnosticsProperty('locale', locale, defaultValue: null)); - properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); + properties.add(DiagnosticsProperty('textScaler', textScaler, defaultValue: null)); properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); properties.add(IntProperty('minLines', minLines, defaultValue: null)); properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); @@ -2073,7 +2126,13 @@ class EditableTextState extends State with AutomaticKeepAliveClien final GlobalKey _editableKey = GlobalKey(); /// Detects whether the clipboard can paste. - final ClipboardStatusNotifier clipboardStatus = ClipboardStatusNotifier(); + final ClipboardStatusNotifier clipboardStatus = kIsWeb + // Web browsers will show a permission dialog when Clipboard.hasStrings is + // called. In an EditableText, this will happen before the paste button is + // clicked, often before the context menu is even shown. To avoid this + // poor user experience, always show the paste button on web. + ? _WebClipboardStatusNotifier() + : ClipboardStatusNotifier(); /// Detects whether the Live Text input is enabled. /// @@ -2157,7 +2216,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override bool get wantKeepAlive => widget.focusNode.hasFocus; - Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); + Color get _cursorColor { + final double effectiveOpacity = math.min(widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value); + return widget.cursorColor.withOpacity(effectiveOpacity); + } @override bool get cutEnabled { @@ -2215,6 +2277,38 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + @override + bool get lookUpEnabled { + if (defaultTargetPlatform != TargetPlatform.iOS) { + return false; + } + return !widget.obscureText + && !textEditingValue.selection.isCollapsed + && textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; + } + + @override + bool get searchWebEnabled { + if (defaultTargetPlatform != TargetPlatform.iOS) { + return false; + } + + return !widget.obscureText + && !textEditingValue.selection.isCollapsed + && textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; + } + + @override + bool get shareEnabled { + if (defaultTargetPlatform != TargetPlatform.iOS) { + return false; + } + + return !widget.obscureText + && !textEditingValue.selection.isCollapsed + && textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; + } + @override bool get liveTextInputEnabled { return _liveTextInputStatus?.value == LiveTextInputStatus.enabled && @@ -2380,6 +2474,69 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + /// Look up the current selection, + /// as in the "Look Up" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// Throws an error if the selection is empty or collapsed. + Future lookUpSelection(SelectionChangedCause cause) async { + assert(!widget.obscureText); + + final String text = textEditingValue.selection.textInside(textEditingValue.text); + if (widget.obscureText || text.isEmpty) { + return; + } + await SystemChannels.platform.invokeMethod( + 'LookUp.invoke', + text, + ); + } + + /// Launch a web search on the current selection, + /// as in the "Search Web" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// When 'obscureText' is true or the selection is empty, + /// this function will not do anything + Future searchWebForSelection(SelectionChangedCause cause) async { + assert(!widget.obscureText); + if (widget.obscureText) { + return; + } + + final String text = textEditingValue.selection.textInside(textEditingValue.text); + if (text.isNotEmpty) { + await SystemChannels.platform.invokeMethod( + 'SearchWeb.invoke', + text, + ); + } + } + + /// Launch the share interface for the current selection, + /// as in the "Share" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// When 'obscureText' is true or the selection is empty, + /// this function will not do anything + Future shareSelection(SelectionChangedCause cause) async { + assert(!widget.obscureText); + if (widget.obscureText) { + return; + } + + final String text = textEditingValue.selection.textInside(textEditingValue.text); + if (text.isNotEmpty) { + await SystemChannels.platform.invokeMethod( + 'Share.invoke', + text, + ); + } + } + void _startLiveTextInput(SelectionChangedCause cause) { if (!liveTextInputEnabled) { return; @@ -2606,6 +2763,15 @@ class EditableTextState extends State with AutomaticKeepAliveClien onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, + onLookUp: lookUpEnabled + ? () => lookUpSelection(SelectionChangedCause.toolbar) + : null, + onSearchWeb: searchWebEnabled + ? () => searchWebForSelection(SelectionChangedCause.toolbar) + : null, + onShare: shareEnabled + ? () => shareSelection(SelectionChangedCause.toolbar) + : null, onLiveTextInput: liveTextInputEnabled ? () => _startLiveTextInput(SelectionChangedCause.toolbar) : null, @@ -3322,11 +3488,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _textInputConnection!.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; - if (kIsWeb) { - _finalizeEditing(TextInputAction.done, shouldUnfocus: true); - } else { - widget.focusNode.unfocus(); - } + widget.focusNode.unfocus(); } } @@ -3705,8 +3867,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien } void _onCursorColorTick() { - renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); - _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0; + final double effectiveOpacity = math.min(widget.cursorColor.alpha / 255.0, _cursorBlinkOpacityController.value); + renderEditable.cursorColor = widget.cursorColor.withOpacity(effectiveOpacity); + _cursorVisibilityNotifier.value = widget.showCursor && (EditableText.debugDeterministicCursor || _cursorBlinkOpacityController.value > 0); } bool get _showBlinkingCursor => _hasFocus && _value.selection.isCollapsed && widget.showCursor && _tickersEnabled; @@ -3890,11 +4053,17 @@ class EditableTextState extends State with AutomaticKeepAliveClien } final InlineSpan inlineSpan = renderEditable.text!; + final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) { + (final TextScaler textScaler, _) => textScaler, + (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), + (null, null) => MediaQuery.textScalerOf(context), + }; + final _ScribbleCacheKey newCacheKey = _ScribbleCacheKey( inlineSpan: inlineSpan, textAlign: widget.textAlign, textDirection: _textDirection, - textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), + textScaler: effectiveTextScaler, textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), locale: widget.locale, structStyle: widget.strutStyle, @@ -4588,6 +4757,12 @@ class EditableTextState extends State with AutomaticKeepAliveClien super.build(context); // See AutomaticKeepAliveClientMixin. final TextSelectionControls? controls = widget.selectionControls; + final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) { + (final TextScaler textScaler, _) => textScaler, + (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), + (null, null) => MediaQuery.textScalerOf(context), + }; + return _CompositionCallback( compositeCallback: _compositeCallback, enabled: _hasInputConnection, @@ -4675,9 +4850,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien value: _value, cursorColor: _cursorColor, backgroundCursorColor: widget.backgroundCursorColor, - showCursor: EditableText.debugDeterministicCursor - ? ValueNotifier(widget.showCursor) - : _cursorVisibilityNotifier, + showCursor: _cursorVisibilityNotifier, forceLine: widget.forceLine, readOnly: widget.readOnly, hasFocus: _hasFocus, @@ -4688,7 +4861,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien selectionColor: _selectionOverlay?.spellCheckToolbarIsVisible ?? false ? _spellCheckConfiguration.misspelledSelectionColor ?? widget.selectionColor : widget.selectionColor, - textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), + textScaler: effectiveTextScaler, textAlign: widget.textAlign, textDirection: _textDirection, locale: widget.locale, @@ -4814,7 +4987,7 @@ class _Editable extends MultiChildRenderObjectWidget { required this.expands, this.strutStyle, this.selectionColor, - required this.textScaleFactor, + required this.textScaler, required this.textAlign, required this.textDirection, this.locale, @@ -4835,7 +5008,7 @@ class _Editable extends MultiChildRenderObjectWidget { this.promptRectRange, this.promptRectColor, required this.clipBehavior, - }) : super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaleFactor)); + }) : super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler)); final InlineSpan inlineSpan; final TextEditingValue value; @@ -4852,7 +5025,7 @@ class _Editable extends MultiChildRenderObjectWidget { final bool expands; final StrutStyle? strutStyle; final Color? selectionColor; - final double textScaleFactor; + final TextScaler textScaler; final TextAlign textAlign; final TextDirection textDirection; final Locale? locale; @@ -4893,7 +5066,7 @@ class _Editable extends MultiChildRenderObjectWidget { expands: expands, strutStyle: strutStyle, selectionColor: selectionColor, - textScaleFactor: textScaleFactor, + textScaler: textScaler, textAlign: textAlign, textDirection: textDirection, locale: locale ?? Localizations.maybeLocaleOf(context), @@ -4937,7 +5110,7 @@ class _Editable extends MultiChildRenderObjectWidget { ..expands = expands ..strutStyle = strutStyle ..selectionColor = selectionColor - ..textScaleFactor = textScaleFactor + ..textScaler = textScaler ..textAlign = textAlign ..textDirection = textDirection ..locale = locale ?? Localizations.maybeLocaleOf(context) @@ -4970,7 +5143,7 @@ class _ScribbleCacheKey { required this.inlineSpan, required this.textAlign, required this.textDirection, - required this.textScaleFactor, + required this.textScaler, required this.textHeightBehavior, required this.locale, required this.structStyle, @@ -4980,7 +5153,7 @@ class _ScribbleCacheKey { final TextAlign textAlign; final TextDirection textDirection; - final double textScaleFactor; + final TextScaler textScaler; final TextHeightBehavior? textHeightBehavior; final Locale? locale; final StrutStyle structStyle; @@ -4994,7 +5167,7 @@ class _ScribbleCacheKey { } final bool needsLayout = textAlign != other.textAlign || textDirection != other.textDirection - || textScaleFactor != other.textScaleFactor + || textScaler != other.textScaler || (textHeightBehavior ?? const TextHeightBehavior()) != (other.textHeightBehavior ?? const TextHeightBehavior()) || locale != other.locale || structStyle != other.structStyle @@ -5111,17 +5284,19 @@ class _ScribblePlaceholder extends WidgetSpan { final Size size; @override - void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List? dimensions }) { + void build(ui.ParagraphBuilder builder, { + TextScaler textScaler = TextScaler.noScaling, + List? dimensions, + }) { assert(debugAssertIsValid()); final bool hasStyle = style != null; if (hasStyle) { - builder.pushStyle(style!.getTextStyle(textScaleFactor: textScaleFactor)); + builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); } builder.addPlaceholder( - size.width, - size.height, + size.width * textScaler.textScaleFactor, + size.height * textScaler.textScaleFactor, alignment, - scale: textScaleFactor, ); if (hasStyle) { builder.pop(); @@ -5454,3 +5629,18 @@ class _GlyphHeights { /// The glyph height of the last line. final double end; } + +/// A [ClipboardStatusNotifier] whose [value] is hardcoded to +/// [ClipboardStatus.pasteable]. +/// +/// Useful to avoid showing a permission dialog on web, which happens when +/// [Clipboard.hasStrings] is called. +class _WebClipboardStatusNotifier extends ClipboardStatusNotifier { + @override + ClipboardStatus value = ClipboardStatus.pasteable; + + @override + Future update() { + return Future.value(); + } +} diff --git a/packages/flutter/lib/src/widgets/fade_in_image.dart b/packages/flutter/lib/src/widgets/fade_in_image.dart index b489316ef5595..b16f5d3305b0e 100644 --- a/packages/flutter/lib/src/widgets/fade_in_image.dart +++ b/packages/flutter/lib/src/widgets/fade_in_image.dart @@ -71,10 +71,6 @@ class FadeInImage extends StatefulWidget { /// The [placeholder] and [image] may have their own FilterQuality settings via [filterQuality] /// and [placeholderFilterQuality]. /// - /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve], - /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and - /// [matchTextDirection] arguments must not be null. - /// /// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored. const FadeInImage({ super.key, @@ -178,10 +174,6 @@ class FadeInImage extends StatefulWidget { /// and [height] regardless of these parameters. These parameters are primarily /// intended to reduce the memory usage of [ImageCache]. /// - /// The [placeholder], [image], [imageScale], [fadeOutDuration], - /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat], - /// and [matchTextDirection] arguments must not be null. - /// /// See also: /// /// * [Image.asset], which has more details about loading images from diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index b167ed114d0fb..69705e477c178 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/painting.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'binding.dart'; @@ -416,9 +417,6 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { /// /// The [debugLabel] is ignored on release builds. /// - /// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus] - /// arguments must not be null. - /// /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`. /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated /// in the future. @@ -436,6 +434,10 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { _descendantsAreTraversable = descendantsAreTraversable { // Set it via the setter so that it does nothing on release builds. this.debugLabel = debugLabel; + + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } } /// If true, tells the focus traversal policy to skip over this node for @@ -1462,6 +1464,9 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { /// handlers, callers must call [registerGlobalHandlers]. See the /// documentation in that method for caveats to watch out for. FocusManager() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } rootScope._manager = this; } @@ -1479,6 +1484,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { @override void dispose() { _highlightManager.dispose(); + rootScope.dispose(); super.dispose(); } @@ -1601,10 +1607,32 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { return; } _haveScheduledUpdate = true; - scheduleMicrotask(_applyFocusChange); + scheduleMicrotask(applyFocusChangesIfNeeded); } - void _applyFocusChange() { + /// Applies any pending focus changes and notifies listeners that the focus + /// has changed. + /// + /// Must not be called during the build phase. This method is meant to be + /// called in a post-frame callback or microtask when the pending focus + /// changes need to be resolved before something else occurs. + /// + /// It can't be called during the build phase because not all listeners are + /// safe to be called with an update during a build. + /// + /// Typically, this is called automatically by the [FocusManager], but + /// sometimes it is necessary to ensure that no focus changes are pending + /// before executing an action. For example, the [MenuAnchor] class uses this + /// to make sure that the previous focus has been restored before executing a + /// menu callback when a menu item is selected. + /// + /// It is safe to call this if no focus changes are pending. + void applyFocusChangesIfNeeded() { + assert( + SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks, + 'applyFocusChangesIfNeeded() should not be called during the build phase.' + ); + _haveScheduledUpdate = false; final FocusNode? previousFocus = _primaryFocus; diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart index 497a9419eed21..766bb0b4afdbc 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -111,10 +111,6 @@ import 'inherited_notifier.dart'; /// traversal policy on the [Focus] nodes below it in the widget hierarchy. class Focus extends StatefulWidget { /// Creates a widget that manages a [FocusNode]. - /// - /// The [child] argument is required and must not be null. - /// - /// The [autofocus] argument must not be null. const Focus({ super.key, required this.child, @@ -203,7 +199,7 @@ class Focus extends StatefulWidget { /// If there is more than one widget with autofocus set, then the first one /// added to the tree will get focus. /// - /// Must not be null. Defaults to false. + /// Defaults to false. /// {@endtemplate} final bool autofocus; @@ -350,7 +346,7 @@ class Focus extends StatefulWidget { /// It is not typical to set this to false, as that can affect the semantics /// information available to accessibility systems. /// - /// Must not be null, defaults to true. + /// Defaults to true. /// {@endtemplate} final bool includeSemantics; @@ -746,10 +742,6 @@ class _FocusState extends State { /// policy for a widget subtree. class FocusScope extends Focus { /// Creates a widget that manages a [FocusScopeNode]. - /// - /// The [child] argument is required and must not be null. - /// - /// The [autofocus] argument must not be null. const FocusScope({ super.key, FocusScopeNode? node, @@ -870,10 +862,6 @@ class _FocusInheritedScope extends InheritedNotifier { /// `descendantsAreFocusable` attribute. class ExcludeFocus extends StatelessWidget { /// Const constructor for [ExcludeFocus] widget. - /// - /// The [excluding] argument must not be null. - /// - /// The [child] argument is required, and must not be null. const ExcludeFocus({ super.key, this.excluding = true, diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart index 33a83b0b7a5b1..3f4707c28fe44 100644 --- a/packages/flutter/lib/src/widgets/focus_traversal.dart +++ b/packages/flutter/lib/src/widgets/focus_traversal.dart @@ -193,8 +193,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// traversing forwards (i.e. with [next]), and there is no current focus in /// the nearest [FocusScopeNode] that `currentNode` belongs to. /// - /// The `currentNode` argument must not be null. - /// /// If `ignoreCurrentFocus` is false or not given, this function returns the /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the /// `currentNode`, otherwise, returns the first node from [sortDescendants], @@ -221,8 +219,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// traversing backwards (i.e. with [previous]), and there is no current focus /// in the nearest [FocusScopeNode] that `currentNode` belongs to. /// - /// The `currentNode` argument must not be null. - /// /// If `ignoreCurrentFocus` is false or not given, this function returns the /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the /// `currentNode`, otherwise, returns the last node from [sortDescendants], @@ -245,7 +241,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { final FocusScopeNode scope = currentNode.nearestScope!; FocusNode? candidate = scope.focusedChild; if (ignoreCurrentFocus || candidate == null && scope.descendants.isNotEmpty) { - final Iterable sorted = _sortAllDescendants(scope, currentNode); + final Iterable sorted = _sortAllDescendants(scope, currentNode).where((FocusNode node) => _canRequestTraversalFocus(node)); if (sorted.isEmpty) { candidate = null; } else { @@ -265,8 +261,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// /// This is typically used by [inDirection] to determine which node to focus /// if it is called when no node is currently focused. - /// - /// All arguments must not be null. FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction); /// Clears the data associated with the given [FocusScopeNode] for this object. @@ -298,8 +292,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// the node that has been selected. /// /// Returns true if it successfully found a node and requested focus. - /// - /// The [currentNode] argument must not be null. bool next(FocusNode currentNode) => _moveFocus(currentNode, forward: true); /// Focuses the previous widget in the focus scope that contains the given @@ -310,8 +302,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// the node that has been selected. /// /// Returns true if it successfully found a node and requested focus. - /// - /// The [currentNode] argument must not be null. bool previous(FocusNode currentNode) => _moveFocus(currentNode, forward: false); /// Focuses the next widget in the given [direction] in the focus scope that @@ -322,8 +312,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { /// [FocusNode.requestFocus] on the node that has been selected. /// /// Returns true if it successfully found a node and requested focus. - /// - /// All arguments must not be null. bool inDirection(FocusNode currentNode, TraversalDirection direction); /// Sorts the given `descendants` into focus order. @@ -352,10 +340,25 @@ abstract class FocusTraversalPolicy with Diagnosticable { @protected Iterable sortDescendants(Iterable descendants, FocusNode currentNode); - Map _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode) { + static bool _canRequestTraversalFocus(FocusNode node) { + return node.canRequestFocus && !node.skipTraversal; + } + + static Iterable _getDescendantsWithoutExpandingScope(FocusNode node) { + final List result = []; + for (final FocusNode child in node.children) { + result.add(child); + if (child is! FocusScopeNode) { + result.addAll(_getDescendantsWithoutExpandingScope(child)); + } + } + return result; + } + + static Map _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) { final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy(); final Map groups = {}; - for (final FocusNode node in scope.descendants) { + for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) { final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node); // Group nodes need to be added to their parent's node, or to the "null" // node if no parent is found. This creates the hierarchy of group nodes @@ -374,7 +377,10 @@ abstract class FocusTraversalPolicy with Diagnosticable { } // Skip non-focusable and non-traversable nodes in the same way that // FocusScopeNode.traversalDescendants would. - if (node.canRequestFocus && !node.skipTraversal) { + // + // Current focused node needs to be in the group so that the caller can + // find the next traversable node from the current focused node. + if (node == currentNode || (node.canRequestFocus && !node.skipTraversal)) { groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: [], defaultPolicy: defaultPolicy); assert(!groups[groupNode]!.members.contains(node)); groups[groupNode]!.members.add(node); @@ -388,7 +394,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { List _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) { final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope); // Build the sorting data structure, separating descendants into groups. - final Map groups = _findGroups(scope, scopeGroupNode); + final Map groups = _findGroups(scope, scopeGroupNode, currentNode); // Sort the member lists using the individual policy sorts. for (final FocusNode? key in groups.keys) { @@ -397,6 +403,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { groups[key]!.members.addAll(sortedMembers); } + // Traverse the group tree, adding the children of members in the order they // appear in the member lists. final List sortedDescendants = []; @@ -421,17 +428,31 @@ abstract class FocusTraversalPolicy with Diagnosticable { // They were left in above because they were needed to find their members // during sorting. sortedDescendants.removeWhere((FocusNode node) { - return !node.canRequestFocus || node.skipTraversal; + return node != currentNode && !_canRequestTraversalFocus(node); }); // Sanity check to make sure that the algorithm above doesn't diverge from // the one in FocusScopeNode.traversalDescendants in terms of which nodes it // finds. - assert( - sortedDescendants.length <= scope.traversalDescendants.length && sortedDescendants.toSet().difference(scope.traversalDescendants.toSet()).isEmpty, - 'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. ' - 'These are the different nodes: ${sortedDescendants.toSet().difference(scope.traversalDescendants.toSet())}', - ); + assert((){ + final Set difference = sortedDescendants.toSet().difference(scope.traversalDescendants.toSet()); + if (!_canRequestTraversalFocus(currentNode)) { + // The scope.traversalDescendants will not contain currentNode if it + // skips traversal or not focusable. + assert( + difference.length == 1 && difference.contains(currentNode), + 'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. ' + 'These are the different nodes: ${difference.where((FocusNode node) => node != currentNode)}', + ); + return true; + } + assert( + difference.isEmpty, + 'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. ' + 'These are the different nodes: $difference', + ); + return true; + }()); return sortedDescendants; } @@ -453,7 +474,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { bool _moveFocus(FocusNode currentNode, {required bool forward}) { final FocusScopeNode nearestScope = currentNode.nearestScope!; invalidateScopeData(nearestScope); - final FocusNode? focusedChild = nearestScope.focusedChild; + FocusNode? focusedChild = nearestScope.focusedChild; if (focusedChild == null) { final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode); if (firstFocus != null) { @@ -464,8 +485,11 @@ abstract class FocusTraversalPolicy with Diagnosticable { return true; } } - final List sortedNodes = _sortAllDescendants(nearestScope, currentNode); - if (sortedNodes.isEmpty) { + focusedChild ??= nearestScope; + final List sortedNodes = _sortAllDescendants(nearestScope, focusedChild); + + assert(sortedNodes.contains(focusedChild)); + if (sortedNodes.length < 2) { // If there are no nodes to traverse to, like when descendantsAreTraversable // is false or skipTraversal for all the nodes is true. return false; @@ -473,7 +497,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { if (forward && focusedChild == sortedNodes.last) { switch (nearestScope.traversalEdgeBehavior) { case TraversalEdgeBehavior.leaveFlutterView: - focusedChild!.unfocus(); + focusedChild.unfocus(); return false; case TraversalEdgeBehavior.closedLoop: requestFocusCallback(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd); @@ -483,7 +507,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { if (!forward && focusedChild == sortedNodes.first) { switch (nearestScope.traversalEdgeBehavior) { case TraversalEdgeBehavior.leaveFlutterView: - focusedChild!.unfocus(); + focusedChild.unfocus(); return false; case TraversalEdgeBehavior.closedLoop: requestFocusCallback(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); @@ -1607,8 +1631,6 @@ class FocusTraversalOrder extends InheritedWidget { /// focus traversal in a direction. class FocusTraversalGroup extends StatefulWidget { /// Creates a [FocusTraversalGroup] object. - /// - /// The [child] and [descendantsAreFocusable] arguments must not be null. FocusTraversalGroup({ super.key, FocusTraversalPolicy? policy, @@ -1765,7 +1787,11 @@ class _FocusTraversalGroupNode extends FocusNode { _FocusTraversalGroupNode({ super.debugLabel, required this.policy, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } FocusTraversalPolicy policy; } @@ -1814,7 +1840,6 @@ class _FocusTraversalGroupState extends State { class RequestFocusIntent extends Intent { /// Creates an intent used with [RequestFocusAction]. /// - /// The [focusNode] argument must not be null. /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback} const RequestFocusIntent(this.focusNode, { TraversalRequestFocusCallback? requestFocusCallback @@ -2000,10 +2025,6 @@ class DirectionalFocusAction extends Action { /// `descendantsAreFocusable` attribute. class ExcludeFocusTraversal extends StatelessWidget { /// Const constructor for [ExcludeFocusTraversal] widget. - /// - /// The [excluding] argument must not be null. - /// - /// The [child] argument is required, and must not be null. const ExcludeFocusTraversal({ super.key, this.excluding = true, diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 76e8521ab9196..cb8b5d9c6e8b0 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -10,8 +10,10 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'framework.dart'; import 'navigator.dart'; +import 'pop_scope.dart'; import 'restoration.dart'; import 'restoration_properties.dart'; +import 'routes.dart'; import 'will_pop_scope.dart'; // Duration for delay before announcement in IOS so that the announcement won't be interrupted. @@ -47,15 +49,20 @@ const Duration _kIOSAnnouncementDelayDuration = Duration(seconds: 1); /// * [TextFormField], a convenience widget that wraps a [TextField] widget in a [FormField]. class Form extends StatefulWidget { /// Creates a container for form fields. - /// - /// The [child] argument must not be null. const Form({ super.key, required this.child, + this.canPop, + this.onPopInvoked, + @Deprecated( + 'Use canPop and/or onPopInvoked instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', + ) this.onWillPop, this.onChanged, AutovalidateMode? autovalidateMode, - }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled; + }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled, + assert((onPopInvoked == null && canPop == null) || onWillPop == null, 'onWillPop is deprecated; use canPop and/or onPopInvoked.'); /// Returns the [FormState] of the closest [Form] widget which encloses the /// given context, or null if none is found. @@ -134,8 +141,44 @@ class Form extends StatefulWidget { /// /// * [WillPopScope], another widget that provides a way to intercept the /// back button. + @Deprecated( + 'Use canPop and/or onPopInvoked instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', + ) final WillPopCallback? onWillPop; + /// {@macro flutter.widgets.PopScope.canPop} + /// + /// {@tool dartpad} + /// This sample demonstrates how to use this parameter to show a confirmation + /// dialog when a navigation pop would cause form data to be lost. + /// + /// ** See code in examples/api/lib/widgets/form/form.1.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [onPopInvoked], which also comes from [PopScope] and is often used in + /// conjunction with this parameter. + /// * [PopScope.canPop], which is what [Form] delegates to internally. + final bool? canPop; + + /// {@macro flutter.widgets.navigator.onPopInvoked} + /// + /// {@tool dartpad} + /// This sample demonstrates how to use this parameter to show a confirmation + /// dialog when a navigation pop would cause form data to be lost. + /// + /// ** See code in examples/api/lib/widgets/form/form.1.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [canPop], which also comes from [PopScope] and is often used in + /// conjunction with this parameter. + /// * [PopScope.onPopInvoked], which is what [Form] delegates to internally. + final PopInvokedCallback? onPopInvoked; + /// Called when one of the form fields changes. /// /// In addition to this callback being invoked, all the form fields themselves @@ -200,6 +243,18 @@ class FormState extends State
    { break; } + if (widget.canPop != null || widget.onPopInvoked != null) { + return PopScope( + canPop: widget.canPop ?? true, + onPopInvoked: widget.onPopInvoked, + child: _FormScope( + formState: this, + generation: _generation, + child: widget.child, + ), + ); + } + return WillPopScope( onWillPop: widget.onWillPop, child: _FormScope( @@ -327,8 +382,6 @@ typedef FormFieldBuilder = Widget Function(FormFieldState field); /// * [TextField], which is a commonly used form field for entering text. class FormField extends StatefulWidget { /// Creates a single form field. - /// - /// The [builder] argument must not be null. const FormField({ super.key, required this.builder, @@ -383,7 +436,7 @@ class FormField extends StatefulWidget { /// will auto-validate even without user interaction. If /// [AutovalidateMode.disabled], auto-validation will be disabled. /// - /// Defaults to [AutovalidateMode.disabled], cannot be null. + /// Defaults to [AutovalidateMode.disabled]. /// {@endtemplate} final AutovalidateMode autovalidateMode; @@ -423,6 +476,12 @@ class FormFieldState extends State> with RestorationMixin { /// True if this field has any validation errors. bool get hasError => _errorText.value != null; + /// Returns true if the user has modified the value of this field. + /// + /// This only updates to true once [didChange] has been called and resets to + /// false when [reset] is called. + bool get hasInteractedByUser => _hasInteractedByUser.value; + /// True if the current value is valid. /// /// This will not set [errorText] or [hasError] and it will not update @@ -465,6 +524,8 @@ class FormFieldState extends State> with RestorationMixin { void _validate() { if (widget.validator != null) { _errorText.value = widget.validator!(_value); + } else { + _errorText.value = null; } } @@ -510,6 +571,13 @@ class FormFieldState extends State> with RestorationMixin { super.deactivate(); } + @override + void dispose() { + _errorText.dispose(); + _hasInteractedByUser.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (widget.enabled) { diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index c6d8f056fd430..0d5fd908cde31 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -45,6 +45,7 @@ export 'package:flutter/rendering.dart' show RenderBox, RenderObject, debugDumpL // late Object? _myState, newValue; // int _counter = 0; // Future getApplicationDocumentsDirectory() async => Directory(''); +// late AnimationController animation; // An annotation used by test_analysis package to verify patterns are followed // that allow for tree-shaking of both fields and their initializers. This @@ -993,7 +994,7 @@ abstract class State with Diagnosticable { /// /// {@endtemplate} /// - /// You cannot use [BuildContext.dependOnInheritedWidgetOfExactType] from this + /// You should not use [BuildContext.dependOnInheritedWidgetOfExactType] from this /// method. However, [didChangeDependencies] will be called immediately /// following this method, and [BuildContext.dependOnInheritedWidgetOfExactType] can /// be used there. @@ -1092,9 +1093,73 @@ abstract class State with Diagnosticable { /// } /// ``` /// + /// Sometimes, the changed state is in some other object not owned by the + /// widget [State], but the widget nonetheless needs to be updated to react to + /// the new state. This is especially common with [Listenable]s, such as + /// [AnimationController]s. + /// + /// In such cases, it is good practice to leave a comment in the callback + /// passed to [setState] that explains what state changed: + /// + /// ```dart + /// void _update() { + /// setState(() { /* The animation changed. */ }); + /// } + /// //... + /// animation.addListener(_update); + /// ``` + /// /// It is an error to call this method after the framework calls [dispose]. /// You can determine whether it is legal to call this method by checking - /// whether the [mounted] property is true. + /// whether the [mounted] property is true. That said, it is better practice + /// to cancel whatever work might trigger the [setState] rather than merely + /// checking for [mounted] before calling [setState], as otherwise CPU cycles + /// will be wasted. + /// + /// ## Design discussion + /// + /// The original version of this API was a method called `markNeedsBuild`, for + /// consistency with [RenderObject.markNeedsLayout], + /// [RenderObject.markNeedsPaint], _et al_. + /// + /// However, early user testing of the Flutter framework revealed that people + /// would call `markNeedsBuild()` much more often than necessary. Essentially, + /// people used it like a good luck charm, any time they weren't sure if they + /// needed to call it, they would call it, just in case. + /// + /// Naturally, this led to performance issues in applications. + /// + /// When the API was changed to take a callback instead, this practice was + /// greatly reduced. One hypothesis is that prompting developers to actually + /// update their state in a callback caused developers to think more carefully + /// about what exactly was being updated, and thus improved their understanding + /// of the appropriate times to call the method. + /// + /// In practice, the [setState] method's implementation is trivial: it calls + /// the provided callback synchronously, then calls [Element.markNeedsBuild]. + /// + /// ## Performance considerations + /// + /// There is minimal _direct_ overhead to calling this function, and as it is + /// expected to be called at most once per frame, the overhead is irrelevant + /// anyway. Nonetheless, it is best to avoid calling this function redundantly + /// (e.g. in a tight loop), as it does involve creating a closure and calling + /// it. The method is idempotent, there is no benefit to calling it more than + /// once per [State] per frame. + /// + /// The _indirect_ cost of causing this function, however, is high: it causes + /// the widget to rebuild, possibly triggering rebuilds for the entire subtree + /// rooted at this widget, and further triggering a relayout and repaint of + /// the entire corresponding [RenderObject] subtree. + /// + /// For this reason, this method should only be called when the [build] method + /// will, as a result of whatever state change was detected, change its result + /// meaningfully. + /// + /// See also: + /// + /// * [StatefulWidget], the API documentation for which has a section on + /// performance considerations that are relevant here. @protected void setState(VoidCallback fn) { assert(() { @@ -1238,6 +1303,23 @@ abstract class State with Diagnosticable { /// Implementations of this method should end with a call to the inherited /// method, as in `super.dispose()`. /// + /// ## Application shutdown + /// + /// This method is _not_ invoked when the application shuts down, because + /// there is no way to predict when that will happen. For example, a user's + /// battery could catch fire, or the user could drop the device into a + /// swimming pool, or the operating system could unilaterally terminate the + /// application process due to memory pressure. + /// + /// Applications are responsible for ensuring that they are well-behaved + /// even in the face of a rapid unscheduled termination. + /// + /// To artificially cause the entire widget tree to be disposed, consider + /// calling [runApp] with a widget such as [SizedBox.shrink]. + /// + /// To listen for platform shutdown messages (and other lifecycle changes), + /// consider the [AppLifecycleListener] API. + /// /// See also: /// /// * [deactivate], which is called prior to [dispose]. @@ -1510,13 +1592,40 @@ abstract class ParentDataWidget extends ProxyWidget { return renderObject.parentData is T; } - /// The [RenderObjectWidget] that is typically used to set up the [ParentData] - /// that [applyParentData] will write to. + /// Describes the [RenderObjectWidget] that is typically used to set up the + /// [ParentData] that [applyParentData] will write to. /// /// This is only used in error messages to tell users what widget typically - /// wraps this ParentDataWidget. + /// wraps this [ParentDataWidget] through + /// [debugTypicalAncestorWidgetDescription]. + /// + /// ## Implementations + /// + /// The returned Type should describe a subclass of `RenderObjectWidget`. If + /// more than one Type is supported, use + /// [debugTypicalAncestorWidgetDescription], which typically inserts this + /// value but can be overridden to describe more than one Type. + /// + /// ```dart + /// @override + /// Type get debugTypicalAncestorWidgetClass => FrogJar; + /// ``` + /// + /// If the "typical" parent is generic (`Foo`), consider specifying either + /// a typical type argument (e.g. `Foo` if `int` is typically how the + /// type is specialized), or specifying the upper bound (e.g. `Foo`). Type get debugTypicalAncestorWidgetClass; + /// Describes the [RenderObjectWidget] that is typically used to set up the + /// [ParentData] that [applyParentData] will write to. + /// + /// This is only used in error messages to tell users what widget typically + /// wraps this [ParentDataWidget]. + /// + /// Returns [debugTypicalAncestorWidgetClass] by default as a String. This can + /// be overridden to describe more than one Type of valid parent. + String get debugTypicalAncestorWidgetDescription => '$debugTypicalAncestorWidgetClass'; + Iterable _debugDescribeIncorrectParentDataType({ required ParentData? parentData, RenderObjectWidget? parentDataCreator, @@ -1537,7 +1646,7 @@ abstract class ParentDataWidget extends ProxyWidget { ), ErrorHint( 'Usually, this means that the $runtimeType widget has the wrong ancestor RenderObjectWidget. ' - 'Typically, $runtimeType widgets are placed directly inside $debugTypicalAncestorWidgetClass widgets.', + 'Typically, $runtimeType widgets are placed directly inside $debugTypicalAncestorWidgetDescription widgets.', ), if (parentDataCreator != null) ErrorHint( @@ -1751,16 +1860,20 @@ abstract class InheritedWidget extends ProxyWidget { bool updateShouldNotify(covariant InheritedWidget oldWidget); } -/// RenderObjectWidgets provide the configuration for [RenderObjectElement]s, +/// [RenderObjectWidget]s provide the configuration for [RenderObjectElement]s, /// which wrap [RenderObject]s, which provide the actual rendering of the /// application. /// -/// See also: +/// Usually, rather than subclassing [RenderObjectWidget] directly, render +/// object widgets subclass one of: /// -/// * [MultiChildRenderObjectWidget], which configures a [RenderObject] with -/// a single list of children. -/// * [SlottedMultiChildRenderObjectWidget], which configures a -/// [RenderObject] that organizes its children in different named slots. +/// * [LeafRenderObjectWidget], if the widget has no children. +/// * [SingleChildRenderObjectElement], if the widget has exactly one child. +/// * [MultiChildRenderObjectWidget], if the widget takes a list of children. +/// * [SlottedMultiChildRenderObjectWidget], if the widget organizes its +/// children in different named slots. +/// +/// Subclasses must implement [createRenderObject] and [updateRenderObject]. abstract class RenderObjectWidget extends Widget { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -1803,8 +1916,10 @@ abstract class RenderObjectWidget extends Widget { void didUnmountRenderObject(covariant RenderObject renderObject) { } } -/// A superclass for RenderObjectWidgets that configure RenderObject subclasses +/// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses /// that have no children. +/// +/// Subclasses must implement [createRenderObject] and [updateRenderObject]. abstract class LeafRenderObjectWidget extends RenderObjectWidget { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -1815,13 +1930,14 @@ abstract class LeafRenderObjectWidget extends RenderObjectWidget { } /// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses -/// that have a single child slot. (This superclass only provides the storage -/// for that child, it doesn't actually provide the updating logic.) +/// that have a single child slot. /// -/// Typically, the render object assigned to this widget will make use of +/// The render object assigned to this widget should make use of /// [RenderObjectWithChildMixin] to implement a single-child model. The mixin -/// exposes a [RenderObjectWithChildMixin.child] property that allows -/// retrieving the render object belonging to the [child] widget. +/// exposes a [RenderObjectWithChildMixin.child] property that allows retrieving +/// the render object belonging to the [child] widget. +/// +/// Subclasses must implement [createRenderObject] and [updateRenderObject]. abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -1841,13 +1957,15 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { /// storage for that child list, it doesn't actually provide the updating /// logic.) /// -/// Subclasses must return a [RenderObject] that mixes in +/// Subclasses must use a [RenderObject] that mixes in /// [ContainerRenderObjectMixin], which provides the necessary functionality to /// visit the children of the container render object (the render object -/// belonging to the [children] widgets). Typically, subclasses will return a +/// belonging to the [children] widgets). Typically, subclasses will use a /// [RenderBox] that mixes in both [ContainerRenderObjectMixin] and /// [RenderBoxContainerDefaultsMixin]. /// +/// Subclasses must implement [createRenderObject] and [updateRenderObject]. +/// /// See also: /// /// * [Stack], which uses [MultiChildRenderObjectWidget]. @@ -1858,9 +1976,6 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { /// its children in named slots. abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { /// Initializes fields for subclasses. - /// - /// The [children] argument must not be null and must not contain any null - /// objects. const MultiChildRenderObjectWidget({ super.key, this.children = const [] }); /// The widgets below this widget in the tree. @@ -2240,7 +2355,8 @@ abstract class BuildContext { /// again if the inherited value were to change. To ensure that the widget /// correctly updates itself when the inherited value changes, only call this /// (directly or indirectly) from build methods, layout and paint callbacks, - /// or from [State.didChangeDependencies]. + /// or from [State.didChangeDependencies] (which is called immediately after + /// [State.initState]). /// /// This method should not be called from [State.dispose] because the element /// tree is no longer stable at that time. To refer to an ancestor from that @@ -3005,7 +3121,7 @@ class BuildOwner { assert(_globalKeyRegistry.containsKey(key)); duplicates ??= >{}; // Uses ordered set to produce consistent error message. - final Set elements = duplicates.putIfAbsent(key, () => LinkedHashSet()); + final Set elements = duplicates.putIfAbsent(key, () => {}); elements.add(element); elements.add(_globalKeyRegistry[key]!); } @@ -3150,14 +3266,13 @@ class BuildOwner { /// changed implementations. /// /// This is expensive and should not be called except during development. - void reassemble(Element root, DebugReassembleConfig? reassembleConfig) { + void reassemble(Element root) { if (!kReleaseMode) { FlutterTimeline.startSync('Preparing Hot Reload (widgets)'); } try { assert(root._parent == null); assert(root.owner == this); - root._debugReassembleConfig = reassembleConfig; root.reassemble(); } finally { if (!kReleaseMode) { @@ -3272,7 +3387,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } Element? _parent; - DebugReassembleConfig? _debugReassembleConfig; _NotificationNode? _notificationTree; /// Compare two widgets for equality. @@ -3424,15 +3538,10 @@ abstract class Element extends DiagnosticableTree implements BuildContext { @mustCallSuper @protected void reassemble() { - if (_debugShouldReassemble(_debugReassembleConfig, _widget)) { - markNeedsBuild(); - _debugReassembleConfig = null; - } + markNeedsBuild(); visitChildren((Element child) { - child._debugReassembleConfig = _debugReassembleConfig; child.reassemble(); }); - _debugReassembleConfig = null; } bool _debugIsInScope(Element target) { @@ -3451,6 +3560,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// If this object is a [RenderObjectElement], the render object is the one at /// this location in the tree. Otherwise, this getter will walk down the tree /// until it finds a [RenderObjectElement]. + /// + /// Some locations in the tree are not backed by a render object. In those + /// cases, this getter returns null. This can happen, if the element is + /// located outside of a [View] since only the element subtree rooted in a + /// view has a render tree associated with it. RenderObject? get renderObject { Element? current = this; while (current != null) { @@ -3459,17 +3573,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } else if (current is RenderObjectElement) { return current.renderObject; } else { - Element? next; - current.visitChildren((Element child) { - assert(next == null); // This verifies that there's only one child. - next = child; - }); - current = next; + current = current.renderObjectAttachingChild; } } return null; } + /// Returns the child of this [Element] that will insert a [RenderObject] into + /// an ancestor of this Element to construct the render tree. + /// + /// Returns null if this Element doesn't have any children who need to attach + /// a [RenderObject] to an ancestor of this [Element]. A [RenderObjectElement] + /// will therefore return null because its children insert their + /// [RenderObject]s into the [RenderObjectElement] itself and not into an + /// ancestor of the [RenderObjectElement]. + /// + /// Furthermore, this may return null for [Element]s that hoist their own + /// independent render tree and do not extend the ancestor render tree. + @protected + Element? get renderObjectAttachingChild { + Element? next; + visitChildren((Element child) { + assert(next == null); // This verifies that there's only one child. + next = child; + }); + return next; + } + @override List describeMissingAncestor({ required Type expectedAncestorType }) { final List information = []; @@ -4020,15 +4150,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext { assert(_lifecycleState == _ElementLifecycle.active); assert(child._parent == this); void visit(Element element) { - element._updateSlot(newSlot); - if (element is! RenderObjectElement) { - element.visitChildren(visit); + element.updateSlot(newSlot); + final Element? descendant = element.renderObjectAttachingChild; + if (descendant != null) { + visit(descendant); } } visit(child); } - void _updateSlot(Object? newSlot) { + /// Called by [updateSlotForChild] when the framework needs to change the slot + /// that this [Element] occupies in its ancestor. + @protected + @mustCallSuper + void updateSlot(Object? newSlot) { assert(_lifecycleState == _ElementLifecycle.active); assert(_parent != null); assert(_parent!._lifecycleState == _ElementLifecycle.active); @@ -4069,7 +4204,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// /// The `newSlot` argument specifies the new value for this element's [slot]. void attachRenderObject(Object? newSlot) { - assert(_slot == null); + assert(slot == null); visitChildren((Element child) { child.attachRenderObject(newSlot); }); @@ -4142,7 +4277,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { @protected @pragma('vm:prefer-inline') Element inflateWidget(Widget newWidget, Object? newSlot) { - final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget); if (isTimelineTracked) { Map? debugTimelineArguments; @@ -4168,7 +4302,17 @@ abstract class Element extends DiagnosticableTree implements BuildContext { _debugCheckForCycles(newChild); return true; }()); - newChild._activateWithParent(this, newSlot); + try { + newChild._activateWithParent(this, newSlot); + } catch (_) { + // Attempt to do some clean-up if activation fails to leave tree in a reasonable state. + try { + deactivateChild(newChild); + } catch (_) { + // Clean-up failed. Only surface original exception. + } + rethrow; + } final Element? updatedChild = updateChild(newChild, newWidget, newSlot); assert(newChild == updatedChild); return updatedChild!; @@ -4403,6 +4547,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext { _lifecycleState = _ElementLifecycle.defunct; } + /// Whether the child in the provided `slot` (or one of its descendants) must + /// insert a [RenderObject] into its ancestor [RenderObjectElement] by calling + /// [RenderObjectElement.insertRenderObjectChild] on it. + /// + /// This method is used to define non-rendering zones in the element tree (see + /// [WidgetsBinding] for an explanation of rendering and non-rendering zones): + /// + /// Most branches of the [Element] tree are expected to eventually insert a + /// [RenderObject] into their [RenderObjectElement] ancestor to construct the + /// render tree. However, there is a notable exception: an [Element] may + /// expect that the occupant of a certain child slot creates a new independent + /// render tree and therefore is not allowed to insert a render object into + /// the existing render tree. Those elements must return false from this + /// method for the slot in question to signal to the child in that slot that + /// it must not call [RenderObjectElement.insertRenderObjectChild] on its + /// ancestor. + /// + /// As an example, the element backing the [ViewAnchor] returns false from + /// this method for the [ViewAnchor.view] slot to enforce that it is occupied + /// by e.g. a [View] widget, which will ultimately bootstrap a separate + /// render tree for that view. Another example is the [ViewCollection] widget, + /// which returns false for all its slots for the same reason. + /// + /// Overriding this method is not common, as elements behaving in the way + /// described above are rare. + bool debugExpectsRenderObjectForSlot(Object? slot) => true; + @override RenderObject? findRenderObject() { assert(() { @@ -5185,7 +5356,7 @@ class ErrorWidget extends LeafRenderObjectWidget { if (_flutterError == null) { properties.add(StringProperty('message', message, quoted: false)); } else { - properties.add(_flutterError!.toDiagnosticsNode(style: DiagnosticsTreeStyle.whitespace)); + properties.add(_flutterError.toDiagnosticsNode(style: DiagnosticsTreeStyle.whitespace)); } } } @@ -5265,6 +5436,9 @@ abstract class ComponentElement extends Element { @override bool get debugDoingBuild => _debugDoingBuild; + @override + Element? get renderObjectAttachingChild => _child; + @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); @@ -5418,9 +5592,7 @@ class StatefulElement extends ComponentElement { @override void reassemble() { - if (_debugShouldReassemble(_debugReassembleConfig, _widget)) { - state.reassemble(); - } + state.reassemble(); super.reassemble(); } @@ -5652,12 +5824,28 @@ class ParentDataElement extends ProxyElement { /// Creates an element that uses the given widget as its configuration. ParentDataElement(ParentDataWidget super.widget); + /// Returns the [Type] of [ParentData] that this element has been configured + /// for. + /// + /// This is only available in debug mode. It will throw in profile and + /// release modes. + Type get debugParentDataType { + Type? type; + assert(() { + type = T; + return true; + }()); + if (type != null) { + return type!; + } + throw UnsupportedError('debugParentDataType is only supported in debug builds'); + } + void _applyParentData(ParentDataWidget widget) { void applyParentDataToChild(Element child) { if (child is RenderObjectElement) { child._updateParentData(widget); } else { - assert(child is! ParentDataElement); child.visitChildren(applyParentDataToChild); } } @@ -6072,6 +6260,9 @@ abstract class RenderObjectElement extends Element { } RenderObject? _renderObject; + @override + Element? get renderObjectAttachingChild => null; + bool _debugDoingBuild = false; @override bool get debugDoingBuild => _debugDoingBuild; @@ -6081,55 +6272,146 @@ abstract class RenderObjectElement extends Element { RenderObjectElement? _findAncestorRenderObjectElement() { Element? ancestor = _parent; while (ancestor != null && ancestor is! RenderObjectElement) { - ancestor = ancestor._parent; + // In debug mode we check whether the ancestor accepts RenderObjects to + // produce a better error message in attachRenderObject. In release mode, + // we assume only correct trees are built (i.e. + // debugExpectsRenderObjectForSlot always returns true) and don't check + // explicitly. + assert(() { + if (!ancestor!.debugExpectsRenderObjectForSlot(slot)) { + ancestor = null; + } + return true; + }()); + ancestor = ancestor?._parent; } + assert(() { + if (ancestor?.debugExpectsRenderObjectForSlot(slot) == false) { + ancestor = null; + } + return true; + }()); return ancestor as RenderObjectElement?; } - ParentDataElement? _findAncestorParentDataElement() { - Element? ancestor = _parent; - ParentDataElement? result; - while (ancestor != null && ancestor is! RenderObjectElement) { - if (ancestor is ParentDataElement) { - result = ancestor; - break; - } - ancestor = ancestor._parent; - } + void _debugCheckCompetingAncestors( + List> result, + Set debugAncestorTypes, + Set debugParentDataTypes, + List debugAncestorCulprits, + ) { assert(() { - if (result == null || ancestor == null) { - return true; - } - // Check that no other ParentDataWidgets want to provide parent data. - final List> badAncestors = >[]; - ancestor = ancestor!._parent; - while (ancestor != null && ancestor is! RenderObjectElement) { - if (ancestor is ParentDataElement) { - badAncestors.add(ancestor! as ParentDataElement); - } - ancestor = ancestor!._parent; - } - if (badAncestors.isNotEmpty) { - badAncestors.insert(0, result); + // Check that no other ParentDataWidgets of the same + // type want to provide parent data. + if (debugAncestorTypes.length != result.length || debugParentDataTypes.length != result.length) { + // This can only occur if the Sets of ancestors and parent data types was + // provided a dupe and did not add it. + assert(debugAncestorTypes.length < result.length || debugParentDataTypes.length < result.length); try { // We explicitly throw here (even though we immediately redirect the // exception elsewhere) so that debuggers will notice it when they // have "break on exception" enabled. throw FlutterError.fromParts([ ErrorSummary('Incorrect use of ParentDataWidget.'), - ErrorDescription('The following ParentDataWidgets are providing parent data to the same RenderObject:'), - for (final ParentDataElement ancestor in badAncestors) - ErrorDescription('- ${ancestor.widget} (typically placed directly inside a ${(ancestor.widget as ParentDataWidget).debugTypicalAncestorWidgetClass} widget)'), - ErrorDescription('However, a RenderObject can only receive parent data from at most one ParentDataWidget.'), - ErrorHint('Usually, this indicates that at least one of the offending ParentDataWidgets listed above is not placed directly inside a compatible ancestor widget.'), - ErrorDescription('The ownership chain for the RenderObject that received the parent data was:\n ${debugGetCreatorChain(10)}'), + ErrorDescription( + 'Competing ParentDataWidgets are providing parent data to the ' + 'same RenderObject:' + ), + for (final ParentDataElement ancestor in result.where((ParentDataElement ancestor) { + return debugAncestorCulprits.contains(ancestor.runtimeType); + })) + ErrorDescription( + '- ${ancestor.widget}, which writes ParentData of type ' + '${ancestor.debugParentDataType}, (typically placed directly ' + 'inside a ' + '${(ancestor.widget as ParentDataWidget).debugTypicalAncestorWidgetClass} ' + 'widget)' + ), + ErrorDescription( + 'A RenderObject can receive parent data from multiple ' + 'ParentDataWidgets, but the Type of ParentData must be unique to ' + 'prevent one overwriting another.' + ), + ErrorHint( + 'Usually, this indicates that one or more of the offending ' + "ParentDataWidgets listed above isn't placed inside a dedicated " + "compatible ancestor widget that it isn't sharing with another " + 'ParentDataWidget of the same type.' + ), + ErrorHint( + 'Otherwise, separating aspects of ParentData to prevent ' + 'conflicts can be done using mixins, mixing them all in on the ' + 'full ParentData Object, such as KeepAlive does with ' + 'KeepAliveParentDataMixin.' + ), + ErrorDescription( + 'The ownership chain for the RenderObject that received the ' + 'parent data was:\n ${debugGetCreatorChain(10)}' + ), ]); - } on FlutterError catch (e) { - _reportException(ErrorSummary('while looking for parent data.'), e, e.stackTrace); + } on FlutterError catch (error) { + _reportException( + ErrorSummary('while looking for parent data.'), + error, + error.stackTrace, + ); } } return true; }()); + } + + List> _findAncestorParentDataElements() { + Element? ancestor = _parent; + final List> result = >[]; + final Set debugAncestorTypes = {}; + final Set debugParentDataTypes = {}; + final List debugAncestorCulprits = []; + + // More than one ParentDataWidget can contribute ParentData, but there are + // some constraints. + // 1. ParentData can only be written by unique ParentDataWidget types. + // For example, two KeepAlive ParentDataWidgets trying to write to the + // same child is not allowed. + // 2. Each contributing ParentDataWidget must contribute to a unique + // ParentData type, less ParentData be overwritten. + // For example, there cannot be two ParentDataWidgets that both write + // ParentData of type KeepAliveParentDataMixin, if the first check was + // subverted by a subclassing of the KeepAlive ParentDataWidget. + // 3. The ParentData itself must be compatible with all ParentDataWidgets + // writing to it. + // For example, TwoDimensionalViewportParentData uses the + // KeepAliveParentDataMixin, so it could be compatible with both + // KeepAlive, and another ParentDataWidget with ParentData type + // TwoDimensionalViewportParentData or a subclass thereof. + // The first and second cases are verified here. The third is verified in + // debugIsValidRenderObject. + + while (ancestor != null && ancestor is! RenderObjectElement) { + if (ancestor is ParentDataElement) { + assert((ParentDataElement ancestor) { + if (!debugAncestorTypes.add(ancestor.runtimeType) || !debugParentDataTypes.add(ancestor.debugParentDataType)) { + debugAncestorCulprits.add(ancestor.runtimeType); + } + return true; + }(ancestor)); + result.add(ancestor); + } + ancestor = ancestor._parent; + } + assert(() { + if (result.isEmpty || ancestor == null) { + return true; + } + // Validate points 1 and 2 from above. + _debugCheckCompetingAncestors( + result, + debugAncestorTypes, + debugParentDataTypes, + debugAncestorCulprits, + ); + return true; + }()); return result; } @@ -6150,7 +6432,7 @@ abstract class RenderObjectElement extends Element { _debugUpdateRenderObjectOwner(); return true; }()); - assert(_slot == newSlot); + assert(slot == newSlot); attachRenderObject(newSlot); super.performRebuild(); // clears the "dirty" flag } @@ -6251,12 +6533,13 @@ abstract class RenderObjectElement extends Element { } @override - void _updateSlot(Object? newSlot) { + void updateSlot(Object? newSlot) { final Object? oldSlot = slot; assert(oldSlot != newSlot); - super._updateSlot(newSlot); + super.updateSlot(newSlot); assert(slot == newSlot); - _ancestorRenderObjectElement!.moveRenderObjectChild(renderObject, oldSlot, slot); + assert(_ancestorRenderObjectElement == _findAncestorRenderObjectElement()); + _ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot); } @override @@ -6264,9 +6547,28 @@ abstract class RenderObjectElement extends Element { assert(_ancestorRenderObjectElement == null); _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); + assert(() { + if (_ancestorRenderObjectElement == null) { + FlutterError.reportError(FlutterErrorDetails(exception: FlutterError.fromParts( + [ + ErrorSummary( + 'The render object for ${toStringShort()} cannot find ancestor render object to attach to.', + ), + ErrorDescription( + 'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}', + ), + ErrorHint( + 'Try wrapping your widget in a View widget or any other widget that is backed by ' + 'a $RenderTreeRootElement to serve as the root of the render tree.', + ), + ] + ))); + } + return true; + }()); _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot); - final ParentDataElement? parentDataElement = _findAncestorParentDataElement(); - if (parentDataElement != null) { + final List> parentDataElements = _findAncestorParentDataElements(); + for (final ParentDataElement parentDataElement in parentDataElements) { _updateParentData(parentDataElement.widget as ParentDataWidget); } } @@ -6405,8 +6707,8 @@ class LeafRenderObjectElement extends RenderObjectElement { /// /// The child is optional. /// -/// This element subclass can be used for RenderObjectWidgets whose -/// RenderObjects use the [RenderObjectWithChildMixin] mixin. Such widgets are +/// This element subclass can be used for [RenderObjectWidget]s whose +/// [RenderObject]s use the [RenderObjectWithChildMixin] mixin. Such widgets are /// expected to inherit from [SingleChildRenderObjectWidget]. class SingleChildRenderObjectElement extends RenderObjectElement { /// Creates an element that uses the given widget as its configuration. @@ -6467,8 +6769,8 @@ class SingleChildRenderObjectElement extends RenderObjectElement { /// An [Element] that uses a [MultiChildRenderObjectWidget] as its configuration. /// -/// This element subclass can be used for RenderObjectWidgets whose -/// RenderObjects use the [ContainerRenderObjectMixin] mixin with a parent data +/// This element subclass can be used for [RenderObjectWidget]s whose +/// [RenderObject]s use the [ContainerRenderObjectMixin] mixin with a parent data /// type that implements [ContainerParentDataMixin]. Such widgets /// are expected to inherit from [MultiChildRenderObjectWidget]. /// @@ -6596,6 +6898,67 @@ class MultiChildRenderObjectElement extends RenderObjectElement { } } +/// A [RenderObjectElement] used to manage the root of a render tree. +/// +/// Unlike any other render object element this element does not attempt to +/// attach its [renderObject] to the closest ancestor [RenderObjectElement]. +/// Instead, subclasses must override [attachRenderObject] and +/// [detachRenderObject] to attach/detach the [renderObject] to whatever +/// instance manages the render tree (e.g. by assigning it to +/// [PipelineOwner.rootNode]). +abstract class RenderTreeRootElement extends RenderObjectElement { + /// Creates an element that uses the given widget as its configuration. + RenderTreeRootElement(super.widget); + + @override + @mustCallSuper + void attachRenderObject(Object? newSlot) { + _slot = newSlot; + assert(_debugCheckMustNotAttachRenderObjectToAncestor()); + } + + @override + @mustCallSuper + void detachRenderObject() { + _slot = null; + } + + @override + void updateSlot(Object? newSlot) { + super.updateSlot(newSlot); + assert(_debugCheckMustNotAttachRenderObjectToAncestor()); + } + + bool _debugCheckMustNotAttachRenderObjectToAncestor() { + if (!kDebugMode) { + return true; + } + if (_findAncestorRenderObjectElement() != null) { + throw FlutterError.fromParts( + [ + ErrorSummary( + 'The RenderObject for ${toStringShort()} cannot maintain an independent render tree at its current location.', + ), + ErrorDescription( + 'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}', + ), + ErrorDescription( + 'This RenderObject is the root of an independent render tree and it cannot ' + 'attach itself to an ancestor in an existing tree. The ancestor RenderObject, ' + 'however, expects that a child will be attached.', + ), + ErrorHint( + 'Try moving the subtree that contains the ${toStringShort()} widget into the ' + 'view property of a ViewAnchor widget or to the root of the widget tree, where ' + 'it is not expected to attach its RenderObject to a parent.', + ), + ], + ); + } + return true; + } +} + /// A wrapper class for the [Element] that is the creator of a [RenderObject]. /// /// Setting a [DebugCreator] as [RenderObject.debugCreator] will lead to better @@ -6684,9 +7047,3 @@ class _NullWidget extends Widget { @override Element createElement() => throw UnimplementedError(); } - -// Whether a [DebugReassembleConfig] indicates that an element holding [widget] can skip -// a reassemble. -bool _debugShouldReassemble(DebugReassembleConfig? config, Widget? widget) { - return config == null || config.widgetName == null || widget?.runtimeType.toString() == config.widgetName; -} diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index eb686b97a552d..43c7caa764683 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -91,8 +91,6 @@ typedef GestureRecognizerFactoryInitializer = void /// Used by [RawGestureDetector.gestures]. class GestureRecognizerFactoryWithHandlers extends GestureRecognizerFactory { /// Creates a gesture recognizer factory with the given callbacks. - /// - /// The arguments must not be null. const GestureRecognizerFactoryWithHandlers(this._constructor, this._initializer); final GestureRecognizerFactoryConstructor _constructor; diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index ca5e6d5f23063..1edac3cea47c6 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -170,7 +170,6 @@ enum HeroFlightDirection { class Hero extends StatefulWidget { /// Create a hero. /// - /// The [tag] and [child] parameters must not be null. /// The [child] parameter and all of the its descendants must not be [Hero]es. const Hero({ super.key, @@ -259,7 +258,7 @@ class Hero extends StatefulWidget { /// [PageRoute.maintainState] set to true for a gesture triggered hero /// transition to work. /// - /// Defaults to false and cannot be null. + /// Defaults to false. final bool transitionOnUserGestures; // Returns a map of all of the heroes in `context` indexed by hero tag that @@ -568,6 +567,7 @@ class _HeroFlight { assert(overlayEntry != null); overlayEntry!.remove(); + overlayEntry!.dispose(); overlayEntry = null; // We want to keep the hero underneath the current page hidden. If // [AnimationStatus.completed], toHero will be the one on top and we keep @@ -611,6 +611,19 @@ class _HeroFlight { navigator.userGestureInProgressNotifier.addListener(delayedPerformAnimationUpdate); } + /// Releases resources. + @mustCallSuper + void dispose() { + if (overlayEntry != null) { + overlayEntry!.remove(); + overlayEntry!.dispose(); + overlayEntry = null; + _proxyAnimation.parent = null; + _proxyAnimation.removeListener(onTick); + _proxyAnimation.removeStatusListener(_handleAnimationUpdate); + } + } + void onTick() { final RenderBox? toHeroBox = (!_aborted && manifest.toHero.mounted) ? manifest.toHero.context.findRenderObject() as RenderBox? @@ -1026,6 +1039,14 @@ class HeroController extends NavigatorObserver { }, ); } + + /// Releases resources. + @mustCallSuper + void dispose() { + for (final _HeroFlight flight in _flights.values) { + flight.dispose(); + } + } } /// Enables or disables [Hero]es in the widget subtree. @@ -1039,8 +1060,6 @@ class HeroController extends NavigatorObserver { /// hero animations, as usual. class HeroMode extends StatelessWidget { /// Creates a widget that enables or disables [Hero]es. - /// - /// The [child] and [enabled] arguments must not be null. const HeroMode({ super.key, required this.child, @@ -1055,7 +1074,7 @@ class HeroMode extends StatelessWidget { /// If this property is false, the [Hero]es in this subtree will not animate /// on route changes. Otherwise, they will animate as usual. /// - /// Defaults to true and must not be null. + /// Defaults to true. final bool enabled; @override diff --git a/packages/flutter/lib/src/widgets/icon_data.dart b/packages/flutter/lib/src/widgets/icon_data.dart index 214cadcfc5ef2..865d5d10801cd 100644 --- a/packages/flutter/lib/src/widgets/icon_data.dart +++ b/packages/flutter/lib/src/widgets/icon_data.dart @@ -93,8 +93,6 @@ class IconData { /// [DiagnosticsProperty] that has an [IconData] as value. class IconDataProperty extends DiagnosticsProperty { /// Create a diagnostics property for [IconData]. - /// - /// The [showName], [style], and [level] arguments must not be null. IconDataProperty( String super.name, super.value, { diff --git a/packages/flutter/lib/src/widgets/icon_theme.dart b/packages/flutter/lib/src/widgets/icon_theme.dart index d27ccf475bb2c..404baaaf0b40d 100644 --- a/packages/flutter/lib/src/widgets/icon_theme.dart +++ b/packages/flutter/lib/src/widgets/icon_theme.dart @@ -17,8 +17,6 @@ import 'inherited_theme.dart'; /// The icon theme is honored by [Icon] and [ImageIcon] widgets. class IconTheme extends InheritedTheme { /// Creates an icon theme that controls properties of descendant widgets. - /// - /// Both [data] and [child] arguments must not be null. const IconTheme({ super.key, required this.data, @@ -27,8 +25,6 @@ class IconTheme extends InheritedTheme { /// Creates an icon theme that controls the properties of /// descendant widgets, and merges in the current icon theme, if any. - /// - /// The [data] and [child] arguments must not be null. static Widget merge({ Key? key, required IconThemeData data, diff --git a/packages/flutter/lib/src/widgets/icon_theme_data.dart b/packages/flutter/lib/src/widgets/icon_theme_data.dart index 6c27c54bb3bfd..3c890b9554cc6 100644 --- a/packages/flutter/lib/src/widgets/icon_theme_data.dart +++ b/packages/flutter/lib/src/widgets/icon_theme_data.dart @@ -157,7 +157,7 @@ class IconThemeData with Diagnosticable { /// An opacity to apply to both explicit and default icon colors. /// /// Falls back to 1.0. - double? get opacity => _opacity == null ? null : clampDouble(_opacity!, 0.0, 1.0); + double? get opacity => _opacity == null ? null : clampDouble(_opacity, 0.0, 1.0); final double? _opacity; /// The default for [Icon.shadows]. diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 4c1ec7321aed6..e2871e5e2279f 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -309,6 +309,16 @@ typedef ImageErrorWidgetBuilder = Widget Function( /// using the HTML renderer, the web engine delegates image decoding of network /// images to the Web, which does not support custom decode sizes. /// +/// ## Custom image providers +/// +/// {@tool dartpad} +/// In this example, a variant of [NetworkImage] is created that passes all the +/// [ImageConfiguration] information (locale, platform, size, etc) to the server +/// using query arguments in the image URL. +/// +/// ** See code in examples/api/lib/painting/image_provider/image_provider.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [Icon], which shows an image from a font. @@ -325,9 +335,6 @@ class Image extends StatefulWidget { /// To show an image from the network or from an asset bundle, consider using /// [Image.network] and [Image.asset] respectively. /// - /// The [image], [alignment], [repeat], and [matchTextDirection] arguments - /// must not be null. - /// /// Either the [width] and [height] arguments should be specified, or the /// widget should be placed in a context that sets tight layout constraints. /// Otherwise, the image dimensions will change as the image is loaded, which @@ -363,8 +370,6 @@ class Image extends StatefulWidget { /// Creates a widget that displays an [ImageStream] obtained from the network. /// - /// The [src], [scale], and [repeat] arguments must not be null. - /// /// Either the [width] and [height] arguments should be specified, or the /// widget should be placed in a context that sets tight layout constraints. /// Otherwise, the image dimensions will change as the image is loaded, which @@ -419,8 +424,6 @@ class Image extends StatefulWidget { /// Creates a widget that displays an [ImageStream] obtained from a [File]. /// - /// The [file], [scale], and [repeat] arguments must not be null. - /// /// Either the [width] and [height] arguments should be specified, or the /// widget should be placed in a context that sets tight layout constraints. /// Otherwise, the image dimensions will change as the image is loaded, which @@ -516,8 +519,6 @@ class Image extends StatefulWidget { /// regardless of these parameters. These parameters are primarily intended /// to reduce the memory usage of [ImageCache]. /// - /// The [name] and [repeat] arguments must not be null. - /// /// Either the [width] and [height] arguments should be specified, or the /// widget should be placed in a context that sets tight layout constraints. /// Otherwise, the image dimensions will change as the image is loaded, which @@ -653,8 +654,6 @@ class Image extends StatefulWidget { /// image at its intended size and applies to both the width and the height. /// {@macro flutter.painting.imageInfo.scale} /// - /// The `bytes`, `scale`, and [repeat] arguments must not be null. - /// /// This only accepts compressed image formats (e.g. PNG). Uncompressed /// formats like rawRgba (the default format of [dart:ui.Image.toByteData]) /// will lead to exceptions. @@ -819,7 +818,7 @@ class Image extends StatefulWidget { /// {@end-tool} final ImageErrorWidgetBuilder? errorBuilder; - /// If non-null, require the image to have this width. + /// If non-null, require the image to have this width (in logical pixels). /// /// If null, the image will pick a size that best preserves its intrinsic /// aspect ratio. @@ -831,7 +830,7 @@ class Image extends StatefulWidget { /// and height if the exact image dimensions are not known in advance. final double? width; - /// If non-null, require the image to have this height. + /// If non-null, require the image to have this height (in logical pixels). /// /// If null, the image will pick a size that best preserves its intrinsic /// aspect ratio. diff --git a/packages/flutter/lib/src/widgets/image_filter.dart b/packages/flutter/lib/src/widgets/image_filter.dart index f7a1785744924..ada948a6ae47c 100644 --- a/packages/flutter/lib/src/widgets/image_filter.dart +++ b/packages/flutter/lib/src/widgets/image_filter.dart @@ -34,8 +34,6 @@ import 'framework.dart'; @immutable class ImageFiltered extends SingleChildRenderObjectWidget { /// Creates a widget that applies an [ImageFilter] to its child. - /// - /// The [imageFilter] must not be null. const ImageFiltered({ super.key, required this.imageFilter, diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index f9fd981bb4c34..de234ceab574e 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -274,8 +274,6 @@ class TextStyleTween extends Tween { /// * [AnimatedSwitcher], which fades from one widget to another. abstract class ImplicitlyAnimatedWidget extends StatefulWidget { /// Initializes fields for subclasses. - /// - /// The [curve] and [duration] arguments must not be null. const ImplicitlyAnimatedWidget({ super.key, this.curve = Curves.linear, @@ -598,8 +596,6 @@ abstract class AnimatedWidgetBaseState exten /// * [AnimatedCrossFade], which fades between two children and interpolates their sizes. class AnimatedContainer extends ImplicitlyAnimatedWidget { /// Creates a container that animates its parameters implicitly. - /// - /// The [curve] and [duration] arguments must not be null. AnimatedContainer({ super.key, this.alignment, @@ -805,8 +801,6 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState class AnimatedPadding extends ImplicitlyAnimatedWidget { /// Creates a widget that insets its child by a value that animates /// implicitly. - /// - /// The [padding], [curve], and [duration] arguments must not be null. AnimatedPadding({ super.key, required this.padding, @@ -893,8 +887,6 @@ class _AnimatedPaddingState extends AnimatedWidgetBaseState { class AnimatedAlign extends ImplicitlyAnimatedWidget { /// Creates a widget that positions its child by an alignment that animates /// implicitly. - /// - /// The [alignment], [curve], and [duration] arguments must not be null. const AnimatedAlign({ super.key, required this.alignment, @@ -1031,8 +1023,6 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { /// [width]), and only two out of the three vertical values ([top], /// [bottom], [height]), can be set. In each case, at least one of /// the three must be null. - /// - /// The [curve] and [duration] arguments must not be null. const AnimatedPositioned({ super.key, required this.child, @@ -1049,8 +1039,6 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { assert(top == null || bottom == null || height == null); /// Creates a widget that animates the rectangle it occupies implicitly. - /// - /// The [curve] and [duration] arguments must not be null. AnimatedPositioned.fromRect({ super.key, required this.child, @@ -1182,8 +1170,6 @@ class AnimatedPositionedDirectional extends ImplicitlyAnimatedWidget { /// Only two out of the three horizontal values ([start], [end], [width]), and /// only two out of the three vertical values ([top], [bottom], [height]), can /// be set. In each case, at least one of the three must be null. - /// - /// The [curve] and [duration] arguments must not be null. const AnimatedPositionedDirectional({ super.key, required this.child, @@ -1345,9 +1331,6 @@ class _AnimatedPositionedDirectionalState extends AnimatedWidgetBaseState { /// an [Animation] is provided by the caller instead of being built in. class AnimatedRotation extends ImplicitlyAnimatedWidget { /// Creates a widget that animates its rotation implicitly. - /// - /// The [turns] argument must not be null. - /// The [curve] and [duration] arguments must not be null. const AnimatedRotation({ super.key, this.child, @@ -1569,8 +1547,6 @@ class _AnimatedRotationState extends ImplicitlyAnimatedWidgetState { /// ``` /// {@end-tool} /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being +/// applied to the descendants of the [AnimatedOpacity] widget. This can be +/// confusing for the user, who may not see anything, and may believe the area +/// of the interface where the [AnimatedOpacity] is hiding a widget to be +/// non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the [AnimatedOpacity] +/// widget at all when the [opacity] animation reaches zero. +/// +/// To avoid such problems, it is generally a good idea to use an +/// [IgnorePointer] widget when setting the [opacity] to zero. This prevents +/// interactions with any children in the subtree when the [child] is animating +/// away. +/// /// See also: /// /// * [AnimatedCrossFade], for fading between two children. @@ -1685,8 +1677,7 @@ class _AnimatedSlideState extends ImplicitlyAnimatedWidgetState { class AnimatedOpacity extends ImplicitlyAnimatedWidget { /// Creates a widget that animates its opacity implicitly. /// - /// The [opacity] argument must not be null and must be between 0.0 and 1.0, - /// inclusive. The [curve] and [duration] arguments must not be null. + /// The [opacity] argument must be between zero and one, inclusive. const AnimatedOpacity({ super.key, this.child, @@ -1706,8 +1697,6 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget { /// /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent /// (i.e., invisible). - /// - /// The opacity must not be null. final double opacity; /// Whether the semantic information of the children is always included. @@ -1771,6 +1760,25 @@ class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState extends InheritedWidget { /// Create an inherited widget that updates its dependents when [notifier] /// sends notifications. - /// - /// The [child] argument must not be null. const InheritedNotifier({ super.key, this.notifier, diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index a0ee8fafa6fa7..9ed589af7bb52 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -44,8 +44,6 @@ typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Q /// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the /// size of the area that should be interactive. /// -/// The [child] must not be null. -/// /// See also: /// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart), /// which includes the use of InteractiveViewer. @@ -62,8 +60,6 @@ typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Q @immutable class InteractiveViewer extends StatefulWidget { /// Create an InteractiveViewer. - /// - /// The [child] parameter must not be null. InteractiveViewer({ super.key, this.clipBehavior = Clip.hardEdge, @@ -111,8 +107,8 @@ class InteractiveViewer extends StatefulWidget { /// Can be used to render a child that changes in response to the current /// transformation. /// - /// The [builder] parameter must not be null. See its docs for an example of - /// using it to optimize a large child. + /// See the [builder] attribute docs for an example of using it to optimize a + /// large child. InteractiveViewer.builder({ super.key, this.clipBehavior = Clip.hardEdge, @@ -307,7 +303,7 @@ class InteractiveViewer extends StatefulWidget { /// /// Defaults to 2.5. /// - /// Cannot be null, and must be greater than zero and greater than minScale. + /// Must be greater than zero and greater than [minScale]. final double maxScale; /// The minimum allowed scale. @@ -321,15 +317,14 @@ class InteractiveViewer extends StatefulWidget { /// /// Defaults to 0.8. /// - /// Cannot be null, and must be a finite number greater than zero and less - /// than maxScale. + /// Must be a finite number greater than zero and less than [maxScale]. final double minScale; /// Changes the deceleration behavior after a gesture. /// /// Defaults to 0.0000135. /// - /// Cannot be null, and must be a finite number greater than zero. + /// Must be a finite number greater than zero. final double interactionEndFrictionCoefficient; /// Called when the user ends a pan or scale gesture on the widget. diff --git a/packages/flutter/lib/src/widgets/keyboard_listener.dart b/packages/flutter/lib/src/widgets/keyboard_listener.dart index 0f43379c49819..e339807076193 100644 --- a/packages/flutter/lib/src/widgets/keyboard_listener.dart +++ b/packages/flutter/lib/src/widgets/keyboard_listener.dart @@ -37,10 +37,6 @@ class KeyboardListener extends StatelessWidget { /// For text entry, consider using a [EditableText], which integrates with /// on-screen keyboards and input method editors (IMEs). /// - /// The [focusNode] and [child] arguments are required and must not be null. - /// - /// The [autofocus] argument must not be null. - /// /// The `key` is an identifier for widgets, and is unrelated to keyboards. /// See [Widget.key]. const KeyboardListener({ diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index b2c8638a197d8..d7e489bc052f4 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -34,9 +34,6 @@ typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstrain /// [RenderConstrainedLayoutBuilder]. abstract class ConstrainedLayoutBuilder extends RenderObjectWidget { /// Creates a widget that defers its building until layout. - /// - /// The [builder] argument must not be null, and the returned widget should not - /// be null. const ConstrainedLayoutBuilder({ super.key, required this.builder, @@ -262,8 +259,6 @@ mixin RenderConstrainedLayoutBuilder { /// Creates a widget that defers its building until layout. - /// - /// The [builder] argument must not be null. const LayoutBuilder({ super.key, required super.builder, diff --git a/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart b/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart index d8d43ef7bfc48..6a20487ebe216 100644 --- a/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart @@ -7,7 +7,6 @@ import 'dart:math' as math; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; import 'basic.dart'; import 'framework.dart'; @@ -215,14 +214,14 @@ class ListWheelChildBuilderDelegate extends ListWheelChildDelegate { class FixedExtentScrollController extends ScrollController { /// Creates a scroll controller for scrollables whose items have the same size. /// - /// [initialItem] defaults to 0 and must not be null. + /// [initialItem] defaults to zero. FixedExtentScrollController({ this.initialItem = 0, }); /// The page to show when first creating the scroll view. /// - /// Defaults to 0 and must not be null. + /// Defaults to zero. final int initialItem; /// The currently selected item index that's closest to the center of the viewport. @@ -255,8 +254,6 @@ class FixedExtentScrollController extends ScrollController { /// /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. - /// - /// The `duration` and `curve` arguments must not be null. Future animateToItem( int itemIndex, { required Duration duration, @@ -666,8 +663,9 @@ class ListWheelScrollView extends StatefulWidget { /// {@macro flutter.rendering.RenderListWheelViewport.overAndUnderCenterOpacity} final double overAndUnderCenterOpacity; - /// Size of each child in the main axis. Must not be null and must be - /// positive. + /// Size of each child in the main axis. + /// + /// Must be positive. final double itemExtent; /// {@macro flutter.rendering.RenderListWheelViewport.squeeze} @@ -709,12 +707,14 @@ class ListWheelScrollView extends StatefulWidget { class _ListWheelScrollViewState extends State { int _lastReportedItemIndex = 0; - ScrollController? scrollController; + ScrollController? _backupController; + + ScrollController get _effectiveController => + widget.controller ?? (_backupController ??= FixedExtentScrollController()); @override void initState() { super.initState(); - scrollController = widget.controller ?? FixedExtentScrollController(); if (widget.controller is FixedExtentScrollController) { final FixedExtentScrollController controller = widget.controller! as FixedExtentScrollController; _lastReportedItemIndex = controller.initialItem; @@ -722,15 +722,9 @@ class _ListWheelScrollViewState extends State { } @override - void didUpdateWidget(ListWheelScrollView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller != null && widget.controller != scrollController) { - final ScrollController? oldScrollController = scrollController; - SchedulerBinding.instance.addPostFrameCallback((_) { - oldScrollController!.dispose(); - }); - scrollController = widget.controller; - } + void dispose() { + _backupController?.dispose(); + super.dispose(); } bool _handleScrollNotification(ScrollNotification notification) { @@ -754,7 +748,7 @@ class _ListWheelScrollViewState extends State { return NotificationListener( onNotification: _handleScrollNotification, child: _FixedExtentScrollable( - controller: scrollController, + controller: _effectiveController, physics: widget.physics, itemExtent: widget.itemExtent, restorationId: widget.restorationId, @@ -945,18 +939,18 @@ class ListWheelElement extends RenderObjectElement implements ListWheelChildMana class ListWheelViewport extends RenderObjectWidget { /// Creates a viewport where children are rendered onto a wheel. /// - /// The [diameterRatio] argument defaults to 2.0 and must not be null. + /// The [diameterRatio] argument defaults to 2. /// - /// The [perspective] argument defaults to 0.003 and must not be null. + /// The [perspective] argument defaults to 0.003. /// /// The [itemExtent] argument in pixels must be provided and must be positive. /// - /// The [clipBehavior] argument defaults to [Clip.hardEdge] and must not be null. + /// The [clipBehavior] argument defaults to [Clip.hardEdge]. /// /// The [renderChildrenOutsideViewport] argument defaults to false and must /// not be null. /// - /// The [offset] argument must be provided and must not be null. + /// The [offset] argument must be provided. const ListWheelViewport({ super.key, this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio, diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 72f58ad1dfc91..499e21bdf96f9 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -96,7 +96,7 @@ Future> _loadAll(Locale locale, Iterable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. diff --git a/packages/flutter/lib/src/widgets/magnifier.dart b/packages/flutter/lib/src/widgets/magnifier.dart index 74ef957a3b78e..ea598e90a903a 100644 --- a/packages/flutter/lib/src/widgets/magnifier.dart +++ b/packages/flutter/lib/src/widgets/magnifier.dart @@ -242,9 +242,8 @@ class MagnifierController { Widget? debugRequiredFor, OverlayEntry? below, }) async { - if (overlayEntry != null) { - overlayEntry!.remove(); - } + _overlayEntry?.remove(); + _overlayEntry?.dispose(); final OverlayState overlayState = Overlay.of( context, @@ -257,7 +256,7 @@ class MagnifierController { to: Navigator.maybeOf(context)?.context, ); - _overlayEntry = OverlayEntry( + _overlayEntry = OverlayEntry( builder: (BuildContext context) => capturedThemes.wrap(builder(context)), ); overlayState.insert(overlayEntry!, below: below); @@ -307,6 +306,7 @@ class MagnifierController { @visibleForTesting void removeFromOverlay() { _overlayEntry?.remove(); + _overlayEntry?.dispose(); _overlayEntry = null; } diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 8cd8ee3c2a379..3a899096b6761 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -30,8 +30,8 @@ enum Orientation { /// /// [MediaQuery] contains a large number of related properties. Widgets frequently /// depend on only a few of these attributes. For example, a widget that needs to -/// rebuild when the [MediaQueryData.textScaleFactor] changes does not need to -/// be notified when the [MediaQueryData.size] changes. Specifying an aspect avoids +/// rebuild when the [MediaQueryData.textScaler] changes does not need to be +/// notified when the [MediaQueryData.size] changes. Specifying an aspect avoids /// unnecessary rebuilds. enum _MediaQueryAspect { /// Specifies the aspect corresponding to [MediaQueryData.size]. @@ -42,6 +42,8 @@ enum _MediaQueryAspect { devicePixelRatio, /// Specifies the aspect corresponding to [MediaQueryData.textScaleFactor]. textScaleFactor, + /// Specifies the aspect corresponding to [MediaQueryData.textScaler]. + textScaler, /// Specifies the aspect corresponding to [MediaQueryData.platformBrightness]. platformBrightness, /// Specifies the aspect corresponding to [MediaQueryData.padding]. @@ -138,12 +140,20 @@ enum _MediaQueryAspect { class MediaQueryData { /// Creates data for a media query with explicit values. /// - /// Consider using [MediaQueryData.fromView] to create data based on a - /// [dart:ui.FlutterView]. + /// In a typical application, calling this constructor directly is rarely + /// needed. Consider using [MediaQueryData.fromView] to create data based on a + /// [dart:ui.FlutterView], or [MediaQueryData.copyWith] to create a new copy + /// of [MediaQueryData] with updated properties from a base [MediaQueryData]. const MediaQueryData({ this.size = Size.zero, this.devicePixelRatio = 1.0, - this.textScaleFactor = 1.0, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double textScaleFactor = 1.0, + TextScaler textScaler = _kUnspecifiedTextScaler, this.platformBrightness = Brightness.light, this.padding = EdgeInsets.zero, this.viewInsets = EdgeInsets.zero, @@ -158,7 +168,12 @@ class MediaQueryData { this.navigationMode = NavigationMode.traditional, this.gestureSettings = const DeviceGestureSettings(touchSlop: kTouchSlop), this.displayFeatures = const [], - }); + }) : _textScaleFactor = textScaleFactor, + _textScaler = textScaler, + assert( + identical(textScaler, _kUnspecifiedTextScaler) || textScaleFactor == 1.0, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ); /// Deprecated. Use [MediaQueryData.fromView] instead. /// @@ -201,6 +216,10 @@ class MediaQueryData { /// this method again when it changes to keep the constructed [MediaQueryData] /// updated. /// + /// In general, [MediaQuery.of] is the appropriate way to obtain + /// [MediaQueryData] from a widget. This `fromView` constructor is primarily + /// for use in the implementation of the framework itself. + /// /// See also: /// /// * [MediaQuery.fromView], which constructs [MediaQueryData] from a provided @@ -209,7 +228,8 @@ class MediaQueryData { MediaQueryData.fromView(ui.FlutterView view, {MediaQueryData? platformData}) : size = view.physicalSize / view.devicePixelRatio, devicePixelRatio = view.devicePixelRatio, - textScaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor, + _textScaleFactor = 1.0, // _textScaler is the source of truth. + _textScaler = _textScalerFromView(view, platformData), platformBrightness = platformData?.platformBrightness ?? view.platformDispatcher.platformBrightness, padding = EdgeInsets.fromViewPadding(view.padding, view.devicePixelRatio), viewPadding = EdgeInsets.fromViewPadding(view.viewPadding, view.devicePixelRatio), @@ -225,6 +245,11 @@ class MediaQueryData { gestureSettings = DeviceGestureSettings.fromView(view), displayFeatures = view.displayFeatures; + static TextScaler _textScalerFromView(ui.FlutterView view, MediaQueryData? platformData) { + final double scaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor; + return scaleFactor == 1.0 ? TextScaler.noScaling : TextScaler.linear(scaleFactor); + } + /// The size of the media in logical pixels (e.g, the size of the screen). /// /// Logical pixels are roughly the same visual size across devices. Physical @@ -237,9 +262,12 @@ class MediaQueryData { /// It is considered bad practice to cache and later use the size returned /// by `MediaQuery.of(context).size`. It will make the application non responsive /// and might lead to unexpected behaviors. - /// For instance, during startup, especially in release mode, the first returned - /// size might be (0,0). The size will be updated when the native platform - /// reports the actual resolution. + /// + /// For instance, during startup, especially in release mode, the first + /// returned size might be (0,0). The size will be updated when the native + /// platform reports the actual resolution. Using [MediaQuery.of] will ensure + /// that when the size changes, any widgets depending on the size are + /// automatically rebuilt. /// /// See the article on [Creating responsive and adaptive /// apps](https://docs.flutter.dev/development/ui/layout/adaptive-responsive) @@ -257,6 +285,9 @@ class MediaQueryData { /// the Nexus 6 has a device pixel ratio of 3.5. final double devicePixelRatio; + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than @@ -266,7 +297,44 @@ class MediaQueryData { /// /// * [MediaQuery.textScaleFactorOf], a method to find and depend on the /// textScaleFactor defined for a [BuildContext]. - final double textScaleFactor; + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) + double get textScaleFactor => textScaler.textScaleFactor; + // TODO(LongCatIsLooong): remove this after textScaleFactor is removed. To + // maintain backward compatibility and also keep the const constructor this + // has to be kept as a private field. + // https://github.com/flutter/flutter/issues/128825 + final double _textScaleFactor; + + /// The font scaling strategy to use for laying out textual contents. + /// + /// If this [MediaQueryData] is created by the [MediaQueryData.fromView] + /// constructor, this property reflects the platform's preferred text scaling + /// strategy, and may change as the user changes the scaling factor in the + /// operating system's accessibility settings. + /// + /// See also: + /// + /// * [MediaQuery.textScalerOf], a method to find and depend on the + /// [textScaler] defined for a [BuildContext]. + /// * [TextPainter], a class that lays out and paints text. + TextScaler get textScaler { + // The constructor was called with an explicitly specified textScaler value, + // we assume the caller is migrated and ignore _textScaleFactor. + if (!identical(_kUnspecifiedTextScaler, _textScaler)) { + return _textScaler; + } + return _textScaleFactor == 1.0 + // textScaleFactor and textScaler from the constructor are consistent. + ? TextScaler.noScaling + // The constructor was called with an explicitly specified textScaleFactor, + // we assume the caller is unmigrated and ignore _textScaler. + : TextScaler.linear(_textScaleFactor); + } + final TextScaler _textScaler; /// The current brightness mode of the host platform. /// @@ -477,10 +545,19 @@ class MediaQueryData { /// Creates a copy of this media query data but with the given fields replaced /// with the new values. + /// + /// The `textScaler` parameter and `textScaleFactor` parameter must not be + /// both specified. MediaQueryData copyWith({ Size? size, double? devicePixelRatio, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) double? textScaleFactor, + TextScaler? textScaler, Brightness? platformBrightness, EdgeInsets? padding, EdgeInsets? viewPadding, @@ -496,10 +573,14 @@ class MediaQueryData { DeviceGestureSettings? gestureSettings, List? displayFeatures, }) { + assert(textScaleFactor == null || textScaler == null); + if (textScaleFactor != null) { + textScaler ??= TextScaler.linear(textScaleFactor); + } return MediaQueryData( size: size ?? this.size, devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, - textScaleFactor: textScaleFactor ?? this.textScaleFactor, + textScaler: textScaler ?? this.textScaler, platformBrightness: platformBrightness ?? this.platformBrightness, padding: padding ?? this.padding, viewPadding: viewPadding ?? this.viewPadding, @@ -520,8 +601,8 @@ class MediaQueryData { /// Creates a copy of this media query data but with the given [padding]s /// replaced with zero. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then this + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then this /// [MediaQueryData] is returned unmodified. /// /// See also: @@ -560,8 +641,8 @@ class MediaQueryData { /// Creates a copy of this media query data but with the given [viewInsets] /// replaced with zero. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then this + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then this /// [MediaQueryData] is returned unmodified. /// /// See also: @@ -598,8 +679,8 @@ class MediaQueryData { /// Creates a copy of this media query data but with the given [viewPadding] /// replaced with zero. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then this + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then this /// [MediaQueryData] is returned unmodified. /// /// See also: @@ -733,7 +814,7 @@ class MediaQueryData { final List properties = [ 'size: $size', 'devicePixelRatio: ${devicePixelRatio.toStringAsFixed(1)}', - 'textScaleFactor: ${textScaleFactor.toStringAsFixed(1)}', + 'textScaler: $textScaler', 'platformBrightness: $platformBrightness', 'padding: $padding', 'viewPadding: $viewPadding', @@ -785,8 +866,6 @@ class MediaQueryData { /// * [MediaQueryData], the data structure that represents the metrics. class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// Creates a widget that provides [MediaQueryData] to its descendants. - /// - /// The [data] and [child] arguments must not be null. const MediaQuery({ super.key, required this.data, @@ -800,16 +879,13 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// is consumed by a widget in such a way that the padding is no longer /// exposed to the widget's descendants or siblings. /// - /// The [context] argument is required, must not be null, and must have a - /// [MediaQuery] in scope. + /// The [context] argument must have a [MediaQuery] in scope. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then the returned + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then the returned /// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not /// particularly useful. /// - /// The [child] argument is required and must not be null. - /// /// See also: /// /// * [SafeArea], which both removes the padding from the [MediaQuery] and @@ -847,16 +923,13 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// insets are consumed by a widget in such a way that the view insets are no /// longer exposed to the widget's descendants or siblings. /// - /// The [context] argument is required, must not be null, and must have a - /// [MediaQuery] in scope. + /// The [context] argument must have a [MediaQuery] in scope. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then the returned + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then the returned /// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not /// particularly useful. /// - /// The [child] argument is required and must not be null. - /// /// See also: /// /// * [MediaQueryData.viewInsets], the affected property of the @@ -892,16 +965,13 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// padding is consumed by a widget in such a way that the view padding is no /// longer exposed to the widget's descendants or siblings. /// - /// The [context] argument is required, must not be null, and must have a - /// [MediaQuery] in scope. + /// The [context] argument must have a [MediaQuery] in scope. /// - /// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments - /// must not be null. If all four are false (the default) then the returned + /// If all four of the `removeLeft`, `removeTop`, `removeRight`, and + /// `removeBottom` arguments are false (the default), then the returned /// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not /// particularly useful. /// - /// The [child] argument is required and must not be null. - /// /// See also: /// /// * [MediaQueryData.viewPadding], the affected property of the @@ -970,8 +1040,6 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// /// The injected [MediaQuery] automatically updates when any of the data used /// to construct it changes. - /// - /// The [view] and [child] arguments are required and must not be null. static Widget fromView({ Key? key, required FlutterView view, @@ -984,6 +1052,64 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { ); } + /// Wraps the `child` in a [MediaQuery] with its [MediaQueryData.textScaler] + /// set to [TextScaler.noScaling]. + /// + /// The returned widget must be inserted in a widget tree below an existing + /// [MediaQuery] widget. + /// + /// This can be used to prevent, for example, icon fonts from scaling as the + /// user adjusts the platform's text scaling value. + static Widget withNoTextScaling({ + Key? key, + required Widget child, + }) { + return Builder( + key: key, + builder: (BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), + child: child, + ); + }, + ); + } + + /// Wraps the `child` in a [MediaQuery] and applies [TextScaler.clamp] on the + /// current [MediaQueryData.textScaler]. + /// + /// The returned widget must be inserted in a widget tree below an existing + /// [MediaQuery] widget. + /// + /// This is a convenience function to restrict the range of the scaled text + /// size to `[minScaleFactor * fontSize, maxScaleFactor * fontSize]` (to + /// prevent excessive text scaling that would break the UI, for example). When + /// `minScaleFactor` equals `maxScaleFactor`, the scaler becomes + /// `TextScaler.linear(minScaleFactor)`. + static Widget withClampedTextScaling({ + Key? key, + double minScaleFactor = 0.0, + double maxScaleFactor = double.infinity, + required Widget child, + }) { + assert(maxScaleFactor >= minScaleFactor); + assert(!maxScaleFactor.isNaN); + assert(minScaleFactor.isFinite); + assert(minScaleFactor >= 0); + + return Builder(builder: (BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + final MediaQueryData data = MediaQuery.of(context); + return MediaQuery( + data: data.copyWith( + textScaler: data.textScaler.clamp(minScaleFactor: minScaleFactor, maxScaleFactor: maxScaleFactor), + ), + child: child, + ); + }); + } + /// Contains information about the current media. /// /// For example, the [MediaQueryData.size] property contains the width and @@ -1108,20 +1234,53 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { /// the [MediaQueryData.devicePixelRatio] property of the ancestor [MediaQuery] changes. static double? maybeDevicePixelRatioOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.devicePixelRatio)?.devicePixelRatio; + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [maybeTextScalerOf] instead. + /// /// Returns textScaleFactor for the nearest MediaQuery ancestor or /// 1.0, if no such ancestor exists. /// /// Use of this method will cause the given [context] to rebuild any time that /// the [MediaQueryData.textScaleFactor] property of the ancestor [MediaQuery] changes. + @Deprecated( + 'Use textScalerOf instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) static double textScaleFactorOf(BuildContext context) => maybeTextScaleFactorOf(context) ?? 1.0; + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [maybeTextScalerOf] instead. + /// /// Returns textScaleFactor for the nearest MediaQuery ancestor or /// null, if no such ancestor exists. /// /// Use of this method will cause the given [context] to rebuild any time that - /// the [MediaQueryData.textScaleFactor] property of the ancestor [MediaQuery] changes. + /// the [MediaQueryData.textScaleFactor] property of the ancestor [MediaQuery] + /// changes. + @Deprecated( + 'Use maybeTextScalerOf instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) static double? maybeTextScaleFactorOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.textScaleFactor)?.textScaleFactor; + /// Returns the [TextScaler] for the nearest [MediaQuery] ancestor or null if + /// no such ancestor exists. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [MediaQueryData.textScaler] property of the ancestor [MediaQuery] + /// changes. + static TextScaler textScalerOf(BuildContext context) => maybeTextScalerOf(context) ?? TextScaler.noScaling; + + /// Returns the [TextScaler] for the nearest [MediaQuery] ancestor or + /// [TextScaler.noScaling] if no such ancestor exists. + /// + /// Use of this method will cause the given [context] to rebuild any time that + /// the [MediaQueryData.textScaler] property of the ancestor [MediaQuery] + /// changes. + static TextScaler? maybeTextScalerOf(BuildContext context) => _maybeOf(context, _MediaQueryAspect.textScaler)?.textScaler; + /// Returns platformBrightness for the nearest MediaQuery ancestor or /// [Brightness.light], if no such ancestor exists. /// @@ -1370,6 +1529,10 @@ class MediaQuery extends InheritedModel<_MediaQueryAspect> { if (data.textScaleFactor != oldWidget.data.textScaleFactor) { return true; } + case _MediaQueryAspect.textScaler: + if (data.textScaler != oldWidget.data.textScaler) { + return true; + } case _MediaQueryAspect.platformBrightness: if (data.platformBrightness != oldWidget.data.platformBrightness) { return true; @@ -1579,3 +1742,19 @@ class _MediaQueryFromViewState extends State<_MediaQueryFromView> with WidgetsBi ); } } + +const TextScaler _kUnspecifiedTextScaler = _UnspecifiedTextScaler(); +// TODO(LongCatIsLooong): Remove once `MediaQueryData.textScaleFactor` is +// removed: https://github.com/flutter/flutter/issues/128825. +class _UnspecifiedTextScaler implements TextScaler { + const _UnspecifiedTextScaler(); + + @override + TextScaler clamp({double minScaleFactor = 0, double maxScaleFactor = double.infinity}) => throw UnimplementedError(); + + @override + double scale(double fontSize) => throw UnimplementedError(); + + @override + double get textScaleFactor => throw UnimplementedError(); +} diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 353b0614fccb1..3b78bd7387d7b 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -20,6 +20,7 @@ import 'focus_scope.dart'; import 'focus_traversal.dart'; import 'framework.dart'; import 'heroes.dart'; +import 'notification_listener.dart'; import 'overlay.dart'; import 'restoration.dart'; import 'restoration_properties.dart'; @@ -67,6 +68,10 @@ typedef RoutePredicate = bool Function(Route route); /// /// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback], /// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope]. +@Deprecated( + 'Use PopInvokedCallback instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', +) typedef WillPopCallback = Future Function(); /// Signature for the [Navigator.onPopPage] callback. @@ -89,19 +94,21 @@ typedef PopPageCallback = bool Function(Route route, dynamic result); enum RoutePopDisposition { /// Pop the route. /// - /// If [Route.willPop] returns [pop] then the back button will actually pop - /// the current route. + /// If [Route.willPop] or [Route.popDisposition] return [pop] then the back + /// button will actually pop the current route. pop, /// Do not pop the route. /// - /// If [Route.willPop] returns [doNotPop] then the back button will be ignored. + /// If [Route.willPop] or [Route.popDisposition] return [doNotPop] then the + /// back button will be ignored. doNotPop, /// Delegate this to the next level of navigation. /// - /// If [Route.willPop] returns [bubble] then the back button will be handled - /// by the [SystemNavigator], which will usually close the application. + /// If [Route.willPop] or [Route.popDisposition] return [bubble] then the back + /// button will be handled by the [SystemNavigator], which will usually close + /// the application. bubble, } @@ -133,7 +140,15 @@ abstract class Route { /// /// If the [settings] are not provided, an empty [RouteSettings] object is /// used instead. - Route({ RouteSettings? settings }) : _settings = settings ?? const RouteSettings(); + Route({ RouteSettings? settings }) : _settings = settings ?? const RouteSettings() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/widgets.dart', + className: '$Route<$T>', + object: this, + ); + } + } /// The navigator that the route is in, if any. NavigatorState? get navigator => _navigator; @@ -294,10 +309,51 @@ abstract class Route { /// mechanism. /// * [WillPopScope], another widget that provides a way to intercept the /// back button. + @Deprecated( + 'Use popDisposition instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', + ) Future willPop() async { return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop; } + /// Returns whether calling [Navigator.maybePop] when this [Route] is current + /// ([isCurrent]) should do anything. + /// + /// [Navigator.maybePop] is usually used instead of [Navigator.pop] to handle + /// the system back button, when it hasn't been disabled via + /// [SystemNavigator.setFrameworkHandlesBack]. + /// + /// By default, if a [Route] is the first route in the history (i.e., if + /// [isFirst]), it reports that pops should be bubbled + /// ([RoutePopDisposition.bubble]). This behavior prevents the user from + /// popping the first route off the history and being stranded at a blank + /// screen; instead, the larger scope is popped (e.g. the application quits, + /// so that the user returns to the previous application). + /// + /// In other cases, the default behavior is to accept the pop + /// ([RoutePopDisposition.pop]). + /// + /// The third possible value is [RoutePopDisposition.doNotPop], which causes + /// the pop request to be ignored entirely. + /// + /// See also: + /// + /// * [Form], which provides a [Form.canPop] boolean that is similar. + /// * [PopScope], a widget that provides a way to intercept the back button. + RoutePopDisposition get popDisposition { + return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop; + } + + /// {@template flutter.widgets.navigator.onPopInvoked} + /// Called after a route pop was handled. + /// + /// Even when the pop is canceled, for example by a [PopScope] widget, this + /// will still be called. The `didPop` parameter indicates whether or not the + /// back navigation actually happened successfully. + /// {@endtemplate} + void onPopInvoked(bool didPop) {} + /// Whether calling [didPop] would return false. bool get willHandlePopInternally => false; @@ -455,6 +511,9 @@ abstract class Route { void dispose() { _navigator = null; _restorationScopeId.dispose(); + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } } /// Whether this route is the top-most route on the navigator. @@ -554,8 +613,6 @@ class RouteSettings { /// history. abstract class Page extends RouteSettings { /// Creates a page and initializes [key] for subclasses. - /// - /// The [arguments] argument must not be null. const Page({ this.key, super.name, @@ -1392,9 +1449,6 @@ const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = kIsWeb class Navigator extends StatefulWidget { /// Creates a widget that maintains a stack-based history of child widgets. /// - /// The [onGenerateRoute], [pages], [onGenerateInitialRoutes], - /// [transitionDelegate], [observers] arguments must not be null. - /// /// If the [pages] is not empty, the [onPopPage] must not be null. const Navigator({ super.key, @@ -1461,7 +1515,7 @@ class Navigator extends StatefulWidget { /// The delegate used for deciding how routes transition in or off the screen /// during the [pages] updates. /// - /// Defaults to [DefaultTransitionDelegate] if not specified, cannot be null. + /// Defaults to [DefaultTransitionDelegate]. final TransitionDelegate transitionDelegate; /// The name of the first route to show. @@ -1470,6 +1524,10 @@ class Navigator extends StatefulWidget { /// /// The value is interpreted according to [onGenerateInitialRoutes], which /// defaults to [defaultGenerateInitialRoutes]. + /// + /// Changing the [initialRoute] will have no effect, as it only controls the + /// _initial_ route. To change the route while the application is running, use + /// the static functions on this class, such as [push] or [replace]. final String? initialRoute; /// Called to generate a route for a given [RouteSettings]. @@ -1589,7 +1647,7 @@ class Navigator extends StatefulWidget { /// In cases where clipping is not desired, consider setting this property to /// [Clip.none]. /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// Whether or not the navigator and it's new topmost route should request focus @@ -2411,6 +2469,9 @@ class Navigator extends StatefulWidget { /// the initial route. /// /// If there is no [Navigator] in scope, returns false. + /// + /// Does not consider anything that might externally prevent popping, such as + /// [PopEntry]. /// {@endtemplate} /// /// See also: @@ -2422,21 +2483,22 @@ class Navigator extends StatefulWidget { return navigator != null && navigator.canPop(); } - /// Consults the current route's [Route.willPop] method, and acts accordingly, - /// potentially popping the route as a result; returns whether the pop request - /// should be considered handled. + /// Consults the current route's [Route.popDisposition] getter or + /// [Route.willPop] method, and acts accordingly, potentially popping the + /// route as a result; returns whether the pop request should be considered + /// handled. /// /// {@template flutter.widgets.navigator.maybePop} - /// If [Route.willPop] returns [RoutePopDisposition.pop], then the [pop] + /// If the [RoutePopDisposition] is [RoutePopDisposition.pop], then the [pop] /// method is called, and this method returns true, indicating that it handled /// the pop request. /// - /// If [Route.willPop] returns [RoutePopDisposition.doNotPop], then this + /// If the [RoutePopDisposition] is [RoutePopDisposition.doNotPop], then this /// method returns true, but does not do anything beyond that. /// - /// If [Route.willPop] returns [RoutePopDisposition.bubble], then this method - /// returns false, and the caller is responsible for sending the request to - /// the containing scope (e.g. by closing the application). + /// If the [RoutePopDisposition] is [RoutePopDisposition.bubble], then this + /// method returns false, and the caller is responsible for sending the + /// request to the containing scope (e.g. by closing the application). /// /// This method is typically called for a user-initiated [pop]. For example on /// Android it's called by the binding for the system's back button. @@ -2725,6 +2787,9 @@ class Navigator extends StatefulWidget { ); return true; }()); + for (final Route? route in result) { + route?.dispose(); + } result.clear(); } } else if (initialRouteName != Navigator.defaultRouteName) { @@ -2857,11 +2922,15 @@ class _RouteEntry extends RouteTransitionRecord { final _RestorationInformation? restorationInformation; final bool pageBased; - static Route notAnnounced = _NotAnnounced(); + /// The limit this route entry will attempt to pop in the case of route being + /// remove as a result of a page update. + static const int kDebugPopAttemptLimit = 100; + + static final Route notAnnounced = _NotAnnounced(); _RouteLifecycle currentState; Route? lastAnnouncedPreviousRoute = notAnnounced; // last argument to Route.didChangePrevious - Route lastAnnouncedPoppedNextRoute = notAnnounced; // last argument to Route.didPopNext + WeakReference> lastAnnouncedPoppedNextRoute = WeakReference>(notAnnounced); // last argument to Route.didPopNext Route? lastAnnouncedNextRoute = notAnnounced; // last argument to Route.didChangeNext /// Restoration ID to be used for the encapsulating route when restoration is @@ -2950,7 +3019,7 @@ class _RouteEntry extends RouteTransitionRecord { void handleDidPopNext(Route poppedRoute) { route.didPopNext(poppedRoute); - lastAnnouncedPoppedNextRoute = poppedRoute; + lastAnnouncedPoppedNextRoute = WeakReference>(poppedRoute); } /// Process the to-be-popped route. @@ -3011,6 +3080,7 @@ class _RouteEntry extends RouteTransitionRecord { assert(isPresent); pendingResult = result; currentState = _RouteLifecycle.pop; + route.onPopInvoked(true); } bool _reportRemovalToObserver = true; @@ -3149,7 +3219,7 @@ class _RouteEntry extends RouteTransitionRecord { // already announced this change by calling didPopNext. return !( nextRoute == null && - lastAnnouncedPoppedNextRoute == lastAnnouncedNextRoute + lastAnnouncedPoppedNextRoute.target == lastAnnouncedNextRoute ); } @@ -3197,6 +3267,20 @@ class _RouteEntry extends RouteTransitionRecord { 'This route cannot be marked for pop. Either a decision has already been ' 'made or it does not require an explicit decision on how to transition out.', ); + // Remove state that prevents a pop, e.g. LocalHistoryEntry[s]. + int attempt = 0; + while (route.willHandlePopInternally) { + assert( + () { + attempt += 1; + return attempt < kDebugPopAttemptLimit; + }(), + 'Attempted to pop $route $kDebugPopAttemptLimit times, but still failed', + ); + final bool popResult = route.didPop(result); + assert(!popResult); + + } pop(result); _isWaitingForExitingDecision = false; } @@ -3291,12 +3375,85 @@ class _NavigatorReplaceObservation extends _NavigatorObservation { } } +typedef _IndexWhereCallback = bool Function(_RouteEntry element); + +/// A collection of _RouteEntries representing a navigation history. +/// +/// Acts as a ChangeNotifier and notifies after its List of _RouteEntries is +/// mutated. +class _History extends Iterable<_RouteEntry> with ChangeNotifier { + /// Creates an instance of [_History]. + _History() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + + final List<_RouteEntry> _value = <_RouteEntry>[]; + + int indexWhere(_IndexWhereCallback test, [int start = 0]) { + return _value.indexWhere(test, start); + } + + void add(_RouteEntry element) { + _value.add(element); + notifyListeners(); + } + + void addAll(Iterable<_RouteEntry> elements) { + _value.addAll(elements); + if (elements.isNotEmpty) { + notifyListeners(); + } + } + + void clear() { + final bool valueWasEmpty = _value.isEmpty; + _value.clear(); + if (!valueWasEmpty) { + notifyListeners(); + } + } + + void insert(int index, _RouteEntry element) { + _value.insert(index, element); + notifyListeners(); + } + + _RouteEntry removeAt(int index) { + final _RouteEntry entry = _value.removeAt(index); + notifyListeners(); + return entry; + } + + _RouteEntry removeLast() { + final _RouteEntry entry = _value.removeLast(); + notifyListeners(); + return entry; + } + + _RouteEntry operator [](int index) { + return _value[index]; + } + + @override + Iterator<_RouteEntry> get iterator { + return _value.iterator; + } + + @override + String toString() { + return _value.toString(); + } +} + /// The state for a [Navigator] widget. /// /// A reference to this class can be obtained by calling [Navigator.of]. class NavigatorState extends State with TickerProviderStateMixin, RestorationMixin { late GlobalKey _overlayKey; - List<_RouteEntry> _history = <_RouteEntry>[]; + final _History _history = _History(); + /// A set for entries that are waiting to dispose until their subtrees are /// disposed. /// @@ -3326,12 +3483,43 @@ class NavigatorState extends State with TickerProviderStateMixin, Res late List _effectiveObservers; + bool get _usingPagesAPI => widget.pages != const >[]; + + void _handleHistoryChanged() { + final bool navigatorCanPop = canPop(); + late final bool routeBlocksPop; + if (!navigatorCanPop) { + final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate); + routeBlocksPop = lastEntry != null + && lastEntry.route.popDisposition == RoutePopDisposition.doNotPop; + } else { + routeBlocksPop = false; + } + final NavigationNotification notification = NavigationNotification( + canHandlePop: navigatorCanPop || routeBlocksPop, + ); + // Avoid dispatching a notification in the middle of a build. + switch (SchedulerBinding.instance.schedulerPhase) { + case SchedulerPhase.postFrameCallbacks: + notification.dispatch(context); + case SchedulerPhase.idle: + case SchedulerPhase.midFrameMicrotasks: + case SchedulerPhase.persistentCallbacks: + case SchedulerPhase.transientCallbacks: + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + if (!mounted) { + return; + } + notification.dispatch(context); + }); + } + } + @override void initState() { super.initState(); assert(() { - if (widget.pages != const >[]) { - // This navigator uses page API. + if (_usingPagesAPI) { if (widget.pages.isEmpty) { FlutterError.reportError( FlutterErrorDetails( @@ -3374,6 +3562,8 @@ class NavigatorState extends State with TickerProviderStateMixin, Res if (widget.reportsRouteUpdateToEngine) { SystemNavigator.selectSingleEntryHistory(); } + + _history.addListener(_handleHistoryChanged); } // Use [_nextPagelessRestorationScopeId] to get the next id. @@ -3556,7 +3746,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res void didUpdateWidget(Navigator oldWidget) { super.didUpdateWidget(oldWidget); assert(() { - if (widget.pages != const >[]) { + if (_usingPagesAPI) { // This navigator uses page API. if (widget.pages.isEmpty) { FlutterError.reportError( @@ -3668,6 +3858,8 @@ class NavigatorState extends State with TickerProviderStateMixin, Res _rawNextPagelessRestorationScopeId.dispose(); _serializableHistory.dispose(); userGestureInProgressNotifier.dispose(); + _history.removeListener(_handleHistoryChanged); + _history.dispose(); super.dispose(); // don't unlock, so that the object becomes unusable assert(_debugLocked); @@ -3953,7 +4145,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res pageRouteToPagelessRoutes: pageRouteToPagelessRoutes, ).cast<_RouteEntry>(); } - _history = <_RouteEntry>[]; + _history.clear(); // Adds the leading pageless routes if there is any. if (pageRouteToPagelessRoutes.containsKey(null)) { _history.addAll(pageRouteToPagelessRoutes[null]!); @@ -4969,17 +5161,17 @@ class NavigatorState extends State with TickerProviderStateMixin, Res return true; // there's at least two routes, so we can pop } - /// Consults the current route's [Route.willPop] method, and acts accordingly, - /// potentially popping the route as a result; returns whether the pop request - /// should be considered handled. + /// Consults the current route's [Route.popDisposition] method, and acts + /// accordingly, potentially popping the route as a result; returns whether + /// the pop request should be considered handled. /// /// {@macro flutter.widgets.navigator.maybePop} /// /// See also: /// - /// * [Form], which provides an `onWillPop` callback that enables the form - /// to veto a [pop] initiated by the app's back button. - /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used + /// * [Form], which provides a [Form.canPop] boolean that enables the + /// form to prevent any [pop]s initiated by the app's back button. + /// * [ModalRoute], which provides a `scopedOnPopCallback` that can be used /// to define the route's `willPop` method. @optionalTypeArgs Future maybePop([ T? result ]) async { @@ -4988,23 +5180,31 @@ class NavigatorState extends State with TickerProviderStateMixin, Res return false; } assert(lastEntry.route._navigator == this); - final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous + + // TODO(justinmc): When the deprecated willPop method is removed, delete + // this code and use only popDisposition, below. + final RoutePopDisposition willPopDisposition = await lastEntry.route.willPop(); if (!mounted) { // Forget about this pop, we were disposed in the meantime. return true; } + if (willPopDisposition == RoutePopDisposition.doNotPop) { + return true; + } final _RouteEntry? newLastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate); if (lastEntry != newLastEntry) { // Forget about this pop, something happened to our history in the meantime. return true; } - switch (disposition) { + + switch (lastEntry.route.popDisposition) { case RoutePopDisposition.bubble: return false; case RoutePopDisposition.pop: pop(result); return true; case RoutePopDisposition.doNotPop: + lastEntry.route.onPopInvoked(false); return true; } } @@ -5270,7 +5470,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res } /// Gets first route entry satisfying the predicate, or null if not found. - _RouteEntry? _firstRouteEntryWhereOrNull(_RouteEntryPredicate test) { + _RouteEntry? _firstRouteEntryWhereOrNull(_RouteEntryPredicate test) { for (final _RouteEntry element in _history) { if (test(element)) { return element; @@ -5280,7 +5480,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res } /// Gets last route entry satisfying the predicate, or null if not found. - _RouteEntry? _lastRouteEntryWhereOrNull(_RouteEntryPredicate test) { + _RouteEntry? _lastRouteEntryWhereOrNull(_RouteEntryPredicate test) { _RouteEntry? result; for (final _RouteEntry element in _history) { if (test(element)) { @@ -5294,29 +5494,46 @@ class NavigatorState extends State with TickerProviderStateMixin, Res Widget build(BuildContext context) { assert(!_debugLocked); assert(_history.isNotEmpty); + // Hides the HeroControllerScope for the widget subtree so that the other // nested navigator underneath will not pick up the hero controller above // this level. return HeroControllerScope.none( - child: Listener( - onPointerDown: _handlePointerDown, - onPointerUp: _handlePointerUpOrCancel, - onPointerCancel: _handlePointerUpOrCancel, - child: AbsorbPointer( - absorbing: false, // it's mutated directly by _cancelActivePointers above - child: FocusTraversalGroup( - policy: FocusTraversalGroup.maybeOf(context), - child: Focus( - focusNode: focusNode, - autofocus: true, - skipTraversal: true, - includeSemantics: false, - child: UnmanagedRestorationScope( - bucket: bucket, - child: Overlay( - key: _overlayKey, - clipBehavior: widget.clipBehavior, - initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const [], + child: NotificationListener( + onNotification: (NavigationNotification notification) { + // If the state of this Navigator does not change whether or not the + // whole framework can pop, propagate the Notification as-is. + if (notification.canHandlePop || !canPop()) { + return false; + } + // Otherwise, dispatch a new Notification with the correct canPop and + // stop the propagation of the old Notification. + const NavigationNotification nextNotification = NavigationNotification( + canHandlePop: true, + ); + nextNotification.dispatch(context); + return true; + }, + child: Listener( + onPointerDown: _handlePointerDown, + onPointerUp: _handlePointerUpOrCancel, + onPointerCancel: _handlePointerUpOrCancel, + child: AbsorbPointer( + absorbing: false, // it's mutated directly by _cancelActivePointers above + child: FocusTraversalGroup( + policy: FocusTraversalGroup.maybeOf(context), + child: Focus( + focusNode: focusNode, + autofocus: true, + skipTraversal: true, + includeSemantics: false, + child: UnmanagedRestorationScope( + bucket: bucket, + child: Overlay( + key: _overlayKey, + clipBehavior: widget.clipBehavior, + initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const [], + ), ), ), ), @@ -5477,7 +5694,7 @@ class _HistoryProperty extends RestorableProperty>?> { // Updating. - void update(List<_RouteEntry> history) { + void update(_History history) { assert(isRegistered); final bool wasUninitialized = _pageToPagelessRoutes == null; bool needsSerialization = wasUninitialized; @@ -5687,8 +5904,6 @@ typedef RouteCompletionCallback = void Function(T result); /// {@end-tool} class RestorableRouteFuture extends RestorableProperty { /// Creates a [RestorableRouteFuture]. - /// - /// The [onPresent] and [navigatorFinder] arguments must not be null. RestorableRouteFuture({ this.navigatorFinder = _defaultNavigatorFinder, required this.onPresent, @@ -5800,3 +6015,26 @@ class RestorableRouteFuture extends RestorableProperty { static NavigatorState _defaultNavigatorFinder(BuildContext context) => Navigator.of(context); } + +/// A notification that a change in navigation has taken place. +/// +/// Specifically, this notification indicates that at least one of the following +/// has occurred: +/// +/// * That route stack of a [Navigator] has changed in any way. +/// * The ability to pop has changed, such as controlled by [PopScope]. +class NavigationNotification extends Notification { + /// Creates a notification that some change in navigation has happened. + const NavigationNotification({ + required this.canHandlePop, + }); + + /// Indicates that the originator of this [Notification] is capable of + /// handling a navigation pop. + final bool canHandlePop; + + @override + String toString() { + return 'NavigationNotification canHandlePop: $canHandlePop'; + } +} diff --git a/packages/flutter/lib/src/widgets/navigator_pop_handler.dart b/packages/flutter/lib/src/widgets/navigator_pop_handler.dart new file mode 100644 index 0000000000000..203a85beded18 --- /dev/null +++ b/packages/flutter/lib/src/widgets/navigator_pop_handler.dart @@ -0,0 +1,110 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'framework.dart'; +import 'navigator.dart'; +import 'notification_listener.dart'; +import 'pop_scope.dart'; + +/// Enables the handling of system back gestures. +/// +/// Typically wraps a nested [Navigator] widget and allows it to handle system +/// back gestures in the [onPop] callback. +/// +/// {@tool dartpad} +/// This sample demonstrates how to use this widget to properly handle system +/// back gestures when using nested [Navigator]s. +/// +/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample demonstrates how to use this widget to properly handle system +/// back gestures with a bottom navigation bar whose tabs each have their own +/// nested [Navigator]s. +/// +/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [PopScope], which allows toggling the ability of a [Navigator] to +/// handle pops. +/// * [NavigationNotification], which indicates whether a [Navigator] in a +/// subtree can handle pops. +class NavigatorPopHandler extends StatefulWidget { + /// Creates an instance of [NavigatorPopHandler]. + const NavigatorPopHandler({ + super.key, + this.onPop, + this.enabled = true, + required this.child, + }); + + /// The widget to place below this in the widget tree. + /// + /// Typically this is a [Navigator] that will handle the pop when [onPop] is + /// called. + final Widget child; + + /// Whether this widget's ability to handle system back gestures is enabled or + /// disabled. + /// + /// When false, there will be no effect on system back gestures. If provided, + /// [onPop] will still be called. + /// + /// This can be used, for example, when the nested [Navigator] is no longer + /// active but remains in the widget tree, such as in an inactive tab. + /// + /// Defaults to true. + final bool enabled; + + /// Called when a handleable pop event happens. + /// + /// For example, a pop is handleable when a [Navigator] in [child] has + /// multiple routes on its stack. It's not handleable when it has only a + /// single route, and so [onPop] will not be called. + /// + /// Typically this is used to pop the [Navigator] in [child]. See the sample + /// code on [NavigatorPopHandler] for a full example of this. + final VoidCallback? onPop; + + @override + State createState() => _NavigatorPopHandlerState(); +} + +class _NavigatorPopHandlerState extends State { + bool _canPop = true; + + @override + Widget build(BuildContext context) { + // When the widget subtree indicates it can handle a pop, disable popping + // here, so that it can be manually handled in canPop. + return PopScope( + canPop: !widget.enabled || _canPop, + onPopInvoked: (bool didPop) { + if (didPop) { + return; + } + widget.onPop?.call(); + }, + // Listen to changes in the navigation stack in the widget subtree. + child: NotificationListener( + onNotification: (NavigationNotification notification) { + // If this subtree cannot handle pop, then set canPop to true so + // that our PopScope will allow the Navigator higher in the tree to + // handle the pop instead. + final bool nextCanPop = !notification.canHandlePop; + if (nextCanPop != _canPop) { + setState(() { + _canPop = nextCanPop; + }); + } + return false; + }, + child: widget.child, + ), + ); + } +} diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 020f3dc195a75..30ee29f9bfa10 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -289,7 +289,7 @@ class NestedScrollView extends StatefulWidget { /// outer scrollable over the inner when scrolling back. /// /// This is useful for an outer scrollable containing a [SliverAppBar] that - /// is expected to float. This cannot be null. + /// is expected to float. final bool floatHeaderSlivers; /// {@macro flutter.material.Material.clipBehavior} @@ -441,6 +441,7 @@ class NestedScrollViewState extends State { void dispose() { _coordinator!.dispose(); _coordinator = null; + _absorberHandle.dispose(); super.dispose(); } @@ -1620,6 +1621,13 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { /// [SliverOverlapAbsorber] to align its children, and which shows sample /// usage for this class. class SliverOverlapAbsorberHandle extends ChangeNotifier { + /// Creates a [SliverOverlapAbsorberHandle]. + SliverOverlapAbsorberHandle() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + // Incremented when a RenderSliverOverlapAbsorber takes ownership of this // object, decremented when it releases it. This allows us to find cases where // the same handle is being passed to two render objects. @@ -1698,8 +1706,6 @@ class SliverOverlapAbsorberHandle extends ChangeNotifier { class SliverOverlapAbsorber extends SingleChildRenderObjectWidget { /// Creates a sliver that absorbs overlap and reports it to a /// [SliverOverlapAbsorberHandle]. - /// - /// The [handle] must not be null. const SliverOverlapAbsorber({ super.key, required this.handle, @@ -1742,8 +1748,6 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil /// Create a sliver that absorbs overlap and reports it to a /// [SliverOverlapAbsorberHandle]. /// - /// The [handle] must not be null. - /// /// The [sliver] must be a [RenderSliver]. RenderSliverOverlapAbsorber({ required SliverOverlapAbsorberHandle handle, @@ -1849,8 +1853,6 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil class SliverOverlapInjector extends SingleChildRenderObjectWidget { /// Creates a sliver that is as tall as the value of the given [handle]'s /// layout extent. - /// - /// The [handle] must not be null. const SliverOverlapInjector({ super.key, required this.handle, @@ -1891,8 +1893,6 @@ class SliverOverlapInjector extends SingleChildRenderObjectWidget { /// during a particular frame. class RenderSliverOverlapInjector extends RenderSliver { /// Creates a sliver that is as tall as the value of the given [handle]'s extent. - /// - /// The [handle] must not be null. RenderSliverOverlapInjector({ required SliverOverlapAbsorberHandle handle, }) : _handle = handle; @@ -2013,8 +2013,6 @@ class RenderSliverOverlapInjector extends RenderSliver { /// the viewport needs to recompute its layout (e.g. when it is scrolled). class NestedScrollViewViewport extends Viewport { /// Creates a variant of [Viewport] that has a [SliverOverlapAbsorberHandle]. - /// - /// The [handle] must not be null. NestedScrollViewViewport({ super.key, super.axisDirection, @@ -2073,8 +2071,6 @@ class NestedScrollViewViewport extends Viewport { class RenderNestedScrollViewViewport extends RenderViewport { /// Create a variant of [RenderViewport] that has a /// [SliverOverlapAbsorberHandle]. - /// - /// The [handle] must not be null. RenderNestedScrollViewViewport({ super.axisDirection, required super.crossAxisDirection, diff --git a/packages/flutter/lib/src/widgets/orientation_builder.dart b/packages/flutter/lib/src/widgets/orientation_builder.dart index 6289374af0b49..3ef6face3daf8 100644 --- a/packages/flutter/lib/src/widgets/orientation_builder.dart +++ b/packages/flutter/lib/src/widgets/orientation_builder.dart @@ -26,8 +26,6 @@ typedef OrientationWidgetBuilder = Widget Function(BuildContext context, Orienta /// landscape or portrait mode. class OrientationBuilder extends StatelessWidget { /// Creates an orientation builder. - /// - /// The [builder] argument must not be null. const OrientationBuilder({ super.key, required this.builder, diff --git a/packages/flutter/lib/src/widgets/overflow_bar.dart b/packages/flutter/lib/src/widgets/overflow_bar.dart index b71e1686ca6a5..75291df635af6 100644 --- a/packages/flutter/lib/src/widgets/overflow_bar.dart +++ b/packages/flutter/lib/src/widgets/overflow_bar.dart @@ -54,11 +54,6 @@ enum OverflowBarAlignment { /// {@end-tool} class OverflowBar extends MultiChildRenderObjectWidget { /// Constructs an OverflowBar. - /// - /// The [spacing], [overflowSpacing], [overflowAlignment], - /// [overflowDirection], and [clipBehavior] parameters must not be - /// null. The [children] argument must not be null and must not contain - /// any null objects. const OverflowBar({ super.key, this.spacing = 0.0, @@ -199,7 +194,7 @@ class OverflowBar extends MultiChildRenderObjectWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. final Clip clipBehavior; @override diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 04c71fab73f0e..d689e10769082 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -135,20 +135,20 @@ class OverlayEntry implements Listenable { /// Whether the [OverlayEntry] is currently mounted in the widget tree. /// /// The [OverlayEntry] notifies its listeners when this value changes. - bool get mounted => _overlayEntryStateNotifier.value != null; + bool get mounted => _overlayEntryStateNotifier?.value != null; /// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry]. - final ValueNotifier<_OverlayEntryWidgetState?> _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null); + ValueNotifier<_OverlayEntryWidgetState?>? _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null); @override void addListener(VoidCallback listener) { assert(!_disposedByOwner); - _overlayEntryStateNotifier.addListener(listener); + _overlayEntryStateNotifier?.addListener(listener); } @override void removeListener(VoidCallback listener) { - _overlayEntryStateNotifier.removeListener(listener); + _overlayEntryStateNotifier?.removeListener(listener); } OverlayState? _overlay; @@ -194,7 +194,8 @@ class OverlayEntry implements Listenable { void _didUnmount() { assert(!mounted); if (_disposedByOwner) { - _overlayEntryStateNotifier.dispose(); + _overlayEntryStateNotifier?.dispose(); + _overlayEntryStateNotifier = null; } } @@ -217,12 +218,16 @@ class OverlayEntry implements Listenable { assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.'); _disposedByOwner = true; if (!mounted) { - _overlayEntryStateNotifier.dispose(); + // If we're still mounted when disposed, then this will be disposed in + // _didUnmount, to allow notifications to occur until the entry is + // unmounted. + _overlayEntryStateNotifier?.dispose(); + _overlayEntryStateNotifier = null; } } @override - String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; + String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)${_disposedByOwner ? "(DISPOSED)" : ""}'; } class _OverlayEntryWidget extends StatefulWidget { @@ -296,7 +301,7 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { late final Iterable _hitTestOrderIterable = _createChildIterable(reversed: true); // The following uses sync* because hit-testing is lazy, and LinkedList as a - // Iterable doesn't support current modification. + // Iterable doesn't support concurrent modification. Iterable _createChildIterable({ required bool reversed }) sync* { final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings; if (children == null || children.isEmpty) { @@ -315,7 +320,7 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { @override void initState() { super.initState(); - widget.entry._overlayEntryStateNotifier.value = this; + widget.entry._overlayEntryStateNotifier!.value = this; _theater = context.findAncestorRenderObjectOfType<_RenderTheater>()!; assert(_sortedTheaterSiblings == null); } @@ -335,7 +340,7 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { @override void dispose() { - widget.entry._overlayEntryStateNotifier.value = null; + widget.entry._overlayEntryStateNotifier?.value = null; widget.entry._didUnmount(); _sortedTheaterSiblings = null; super.dispose(); @@ -426,7 +431,7 @@ class Overlay extends StatefulWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// The [OverlayState] from the closest instance of [Overlay] that encloses @@ -543,6 +548,55 @@ class OverlayState extends State with TickerProviderStateMixin { return _entries.length; } + bool _debugCanInsertEntry(OverlayEntry entry) { + final List operandsInformation = [ + DiagnosticsProperty('The OverlayEntry was', entry, style: DiagnosticsTreeStyle.errorProperty), + DiagnosticsProperty( + 'The Overlay the OverlayEntry was trying to insert to was', this, style: DiagnosticsTreeStyle.errorProperty, + ), + ]; + + if (!mounted) { + throw FlutterError.fromParts([ + ErrorSummary('Attempted to insert an OverlayEntry to an already disposed Overlay.'), + ...operandsInformation, + ]); + } + + final OverlayState? currentOverlay = entry._overlay; + final bool alreadyContainsEntry = _entries.contains(entry); + + if (alreadyContainsEntry) { + final bool inconsistentOverlayState = !identical(currentOverlay, this); + throw FlutterError.fromParts([ + ErrorSummary('The specified entry is already present in the target Overlay.'), + ...operandsInformation, + if (inconsistentOverlayState) ErrorHint('This could be an error in the Flutter framework.') + else ErrorHint( + 'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, ' + 'or switching to the OverlayPortal API to avoid manual OverlayEntry management.' + ), + if (inconsistentOverlayState) DiagnosticsProperty( + "The OverlayEntry's current Overlay was", currentOverlay, style: DiagnosticsTreeStyle.errorProperty, + ), + ]); + } + + if (currentOverlay == null) { + return true; + } + + throw FlutterError.fromParts([ + ErrorSummary('The specified entry is already present in a different Overlay.'), + ...operandsInformation, + DiagnosticsProperty("The OverlayEntry's current Overlay was", currentOverlay, style: DiagnosticsTreeStyle.errorProperty,), + ErrorHint( + 'Consider calling remove on the OverlayEntry before inserting it to a different Overlay, ' + 'or switching to the OverlayPortal API to avoid manual OverlayEntry management.' + ) + ]); + } + /// Insert the given entry into the overlay. /// /// If `below` is non-null, the entry is inserted just below `below`. @@ -552,8 +606,7 @@ class OverlayState extends State with TickerProviderStateMixin { /// It is an error to specify both `above` and `below`. void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) { assert(_debugVerifyInsertPosition(above, below)); - assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.'); - assert(entry._overlay == null, 'The specified entry is already present in another Overlay.'); + assert(_debugCanInsertEntry(entry)); entry._overlay = this; setState(() { _entries.insert(_insertionIndex(below, above), entry); @@ -569,14 +622,7 @@ class OverlayState extends State with TickerProviderStateMixin { /// It is an error to specify both `above` and `below`. void insertAll(Iterable entries, { OverlayEntry? below, OverlayEntry? above }) { assert(_debugVerifyInsertPosition(above, below)); - assert( - entries.every((OverlayEntry entry) => !_entries.contains(entry)), - 'One or more of the specified entries are already present in the Overlay.', - ); - assert( - entries.every((OverlayEntry entry) => entry._overlay == null), - 'One or more of the specified entries are already present in another Overlay.', - ); + assert(entries.every(_debugCanInsertEntry)); if (entries.isEmpty) { return; } @@ -876,9 +922,9 @@ class _TheaterParentData extends StackParentData { // _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose // method. This property is only accessed during layout, paint and hit-test so // the `value!` should be safe. - Iterator? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.iterator; - Iterator? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._hitTestOrderIterable.iterator; - void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.forEach(visitor); + Iterator? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.iterator; + Iterator? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._hitTestOrderIterable.iterator; + void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.forEach(visitor); } class _RenderTheater extends RenderBox with ContainerRenderObjectMixin, _RenderTheaterMixin { @@ -965,7 +1011,7 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { @@ -985,12 +1031,14 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin _layoutSurrogate; void layoutByLayoutSurrogate() { - assert(!_parentDoingLayout); + assert(!_theaterDoingThisLayout); final _RenderTheater? theater = parent as _RenderTheater?; if (theater == null || !attached) { assert(false, '$this is not attached to parent'); @@ -2051,25 +2098,26 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM super.layout(BoxConstraints.tight(theater.constraints.biggest)); } - bool _parentDoingLayout = false; + bool _theaterDoingThisLayout = false; @override void layout(Constraints constraints, { bool parentUsesSize = false }) { assert(_needsLayout == debugNeedsLayout); // Only _RenderTheater calls this implementation. assert(parent != null); final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints; - assert(!_parentDoingLayout); - _parentDoingLayout = true; + assert(!_theaterDoingThisLayout); + _theaterDoingThisLayout = true; super.layout(constraints, parentUsesSize: parentUsesSize); - assert(_parentDoingLayout); - _parentDoingLayout = false; + assert(_theaterDoingThisLayout); + _theaterDoingThisLayout = false; _needsLayout = false; assert(!debugNeedsLayout); if (scheduleDeferredLayout) { final _RenderTheater parent = this.parent! as _RenderTheater; // Invoking markNeedsLayout as a layout callback allows this node to be - // merged back to the `PipelineOwner` if it's not already dirty. Otherwise - // this may cause some dirty descendants to performLayout a second time. + // merged back to the `PipelineOwner`'s dirty list in the right order, if + // it's not already dirty. Otherwise this may cause some dirty descendants + // to performLayout a second time. parent.invokeLayoutCallback((BoxConstraints constraints) { markNeedsLayout(); }); } } @@ -2083,7 +2131,7 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM @override void performLayout() { assert(!_debugMutationsLocked); - if (_parentDoingLayout) { + if (_theaterDoingThisLayout) { _needsLayout = false; return; } diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 1ae41dd5e2e65..ef198d769be64 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -79,9 +79,6 @@ class GlowingOverscrollIndicator extends StatefulWidget { /// In order for this widget to display an overscroll indication, the [child] /// widget must contain a widget that generates a [ScrollNotification], such /// as a [ListView] or a [GridView]. - /// - /// The [showLeading], [showTrailing], [axisDirection], [color], and - /// [notificationPredicate] arguments must not be null. const GlowingOverscrollIndicator({ super.key, this.showLeading = true, @@ -613,19 +610,15 @@ enum _StretchDirection { /// To prevent the indicator from showing the indication, call /// [OverscrollIndicatorNotification.disallowIndicator] on the notification. /// -/// Created by [ScrollBehavior.buildOverscrollIndicator] on platforms +/// Created by [MaterialScrollBehavior.buildOverscrollIndicator] on platforms /// (e.g., Android) that commonly use this type of overscroll indication when -/// [ScrollBehavior.androidOverscrollIndicator] is -/// [AndroidOverscrollIndicator.stretch]. Otherwise, the default -/// [GlowingOverscrollIndicator] is applied. -/// [ScrollBehavior.androidOverscrollIndicator] is deprecated, use -/// [ThemeData.useMaterial3], or override -/// [ScrollBehavior.buildOverscrollIndicator] to choose the desired indicator. +/// [ThemeData.useMaterial3] is true. Otherwise, when [ThemeData.useMaterial3] +/// is false, a [GlowingOverscrollIndicator] is used instead.= /// /// See also: /// -/// * [OverscrollIndicatorNotification], which can be used to prevent the stretch -/// effect from being applied at all. +/// * [OverscrollIndicatorNotification], which can be used to prevent the +/// stretch effect from being applied at all. /// * [NotificationListener], to listen for the /// [OverscrollIndicatorNotification]. /// * [GlowingOverscrollIndicator], the default overscroll indicator for @@ -637,8 +630,6 @@ class StretchingOverscrollIndicator extends StatefulWidget { /// In order for this widget to display an overscroll indication, the [child] /// widget must contain a widget that generates a [ScrollNotification], such /// as a [ListView] or a [GridView]. - /// - /// The [axisDirection] and [notificationPredicate] arguments must not be null. const StretchingOverscrollIndicator({ super.key, required this.axisDirection, @@ -666,14 +657,6 @@ class StretchingOverscrollIndicator extends StatefulWidget { /// The overscroll indicator will apply a stretch effect to this child. This /// child (and its subtree) should include a source of [ScrollNotification] /// notifications. - /// - /// Typically a [StretchingOverscrollIndicator] is created by a - /// [ScrollBehavior.buildOverscrollIndicator] method when opted-in using the - /// [ScrollBehavior.androidOverscrollIndicator] flag. In this case - /// the child is usually the one provided as an argument to that method. - /// [ScrollBehavior.androidOverscrollIndicator] is deprecated, use - /// [ThemeData.useMaterial3], or override - /// [ScrollBehavior.buildOverscrollIndicator] to choose the desired indicator. final Widget? child; @override @@ -798,10 +781,10 @@ class _StretchingOverscrollIndicatorState extends State extends ValueKey { /// Creates a [ValueKey] that defines where [PageStorage] values will be saved. const PageStorageKey(super.value); @@ -136,12 +138,12 @@ class PageStorageBucket { /// included in routes. /// /// [PageStorageKey] is used by [Scrollable] if [ScrollController.keepScrollOffset] -/// is enabled to save their [ScrollPosition]s. When more than one -/// scrollable ([ListView], [SingleChildScrollView], [TextField], etc.) appears -/// within the widget's closest ancestor [PageStorage] (such as within the same route), -/// if you want to save all of their positions independently, -/// you should give each of them unique [PageStorageKey]s, or set some of their -/// `keepScrollOffset` false to prevent saving. +/// is enabled to save their [ScrollPosition]s. When more than one scrollable +/// ([ListView], [SingleChildScrollView], [TextField], etc.) appears within the +/// widget's closest ancestor [PageStorage] (such as within the same route), to +/// save all of their positions independently, one must give each of them unique +/// [PageStorageKey]s, or set the `keepScrollOffset` property of some such +/// widgets to false to prevent saving. /// /// {@tool dartpad} /// This sample shows how to explicitly use a [PageStorage] to @@ -157,8 +159,6 @@ class PageStorageBucket { /// * [ModalRoute], which includes this class. class PageStorage extends StatelessWidget { /// Creates a widget that provides a storage bucket for its descendants. - /// - /// The [bucket] argument must not be null. const PageStorage({ super.key, required this.bucket, diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 9660ddefb3dbe..ddd0603edc474 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -112,8 +112,6 @@ import 'viewport.dart'; /// {@end-tool} class PageController extends ScrollController { /// Creates a page controller. - /// - /// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null. PageController({ this.initialPage = 0, this.keepPage = true, @@ -183,8 +181,6 @@ class PageController extends ScrollController { /// /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. - /// - /// The `duration` and `curve` arguments must not be null. Future animateToPage( int page, { required Duration duration, @@ -221,8 +217,6 @@ class PageController extends ScrollController { /// /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. - /// - /// The `duration` and `curve` arguments must not be null. Future nextPage({ required Duration duration, required Curve curve }) { return animateToPage(page!.round() + 1, duration: duration, curve: curve); } @@ -231,8 +225,6 @@ class PageController extends ScrollController { /// /// The animation lasts for the given duration and follows the given curve. /// The returned [Future] resolves when the animation completes. - /// - /// The `duration` and `curve` arguments must not be null. Future previousPage({ required Duration duration, required Curve curve }) { return animateToPage(page!.round() - 1, duration: duration, curve: curve); } @@ -612,6 +604,14 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); /// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart ** /// {@end-tool} /// +/// ## Persisting the scroll position during a session +/// +/// Scroll views attempt to persist their scroll position using [PageStorage]. +/// For a [PageView], this can be disabled by setting [PageController.keepPage] +/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for +/// the [key] of this widget is recommended to help disambiguate different +/// scroll views from each other. +/// /// See also: /// /// * [PageController], which controls which page is visible in the view. @@ -634,10 +634,10 @@ class PageView extends StatefulWidget { /// See the documentation at [SliverChildListDelegate.children] for more details. /// /// {@template flutter.widgets.PageView.allowImplicitScrolling} - /// The [allowImplicitScrolling] parameter must not be null. If true, the - /// [PageView] will participate in accessibility scrolling more like a - /// [ListView], where implicit scroll actions will move to the next page - /// rather than into the contents of the [PageView]. + /// If [allowImplicitScrolling] is true, the [PageView] will participate in + /// accessibility scrolling more like a [ListView], where implicit scroll + /// actions will move to the next page rather than into the contents of the + /// [PageView]. /// {@endtemplate} PageView({ super.key, @@ -909,7 +909,7 @@ class PageView extends StatefulWidget { /// /// If [PageController.viewportFraction] >= 1.0, this property has no effect. /// - /// This property defaults to true and must not be null. + /// This property defaults to true. final bool padEnds; @override diff --git a/packages/flutter/lib/src/widgets/pages.dart b/packages/flutter/lib/src/widgets/pages.dart index de3ff66f94391..2399efba7388d 100644 --- a/packages/flutter/lib/src/widgets/pages.dart +++ b/packages/flutter/lib/src/widgets/pages.dart @@ -11,6 +11,9 @@ import 'routes.dart'; /// The [PageRouteBuilder] subclass provides a way to create a [PageRoute] using /// callbacks rather than by defining a new class via subclassing. /// +/// If `barrierDismissible` is true, then pressing the escape key on the keyboard +/// will cause the current route to be popped with null as the value. +/// /// See also: /// /// * [Route], which documents the meaning of the `T` generic type argument. @@ -20,7 +23,8 @@ abstract class PageRoute extends ModalRoute { super.settings, this.fullscreenDialog = false, this.allowSnapshotting = true, - }); + bool barrierDismissible = false, + }) : _barrierDismissible = barrierDismissible; /// {@template flutter.widgets.PageRoute.fullscreenDialog} /// Whether this page route is a full-screen dialog. @@ -39,7 +43,8 @@ abstract class PageRoute extends ModalRoute { bool get opaque => true; @override - bool get barrierDismissible => false; + bool get barrierDismissible => _barrierDismissible; + final bool _barrierDismissible; @override bool canTransitionTo(TransitionRoute nextRoute) => nextRoute is PageRoute; @@ -65,9 +70,6 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation animat /// * [Route], which documents the meaning of the `T` generic type argument. class PageRouteBuilder extends PageRoute { /// Creates a route that delegates to builder callbacks. - /// - /// The [pageBuilder], [transitionsBuilder], [opaque], [barrierDismissible], - /// [maintainState], and [fullscreenDialog] arguments must not be null. PageRouteBuilder({ super.settings, required this.pageBuilder, diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 722a22b983a08..9916b34782c57 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import '_html_element_view_io.dart' if (dart.library.js_util) '_html_element_view_web.dart'; import 'basic.dart'; import 'debug.dart'; import 'focus_manager.dart'; @@ -65,7 +66,6 @@ class AndroidView extends StatefulWidget { /// Creates a widget that embeds an Android view. /// /// {@template flutter.widgets.AndroidView.constructorArgs} - /// The `viewType` and `hitTestBehavior` parameters must not be null. /// If `creationParams` is not null then `creationParamsCodec` must not be null. /// {@endtemplate} const AndroidView({ @@ -188,37 +188,22 @@ class AndroidView extends StatefulWidget { /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.hardEdge], and must not be null. + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; @override State createState() => _AndroidViewState(); } -// TODO(amirh): describe the embedding mechanism. -// TODO(ychris): remove the documentation for conic path not supported once https://github.com/flutter/flutter/issues/35062 is resolved. -/// Embeds an iOS view in the Widget hierarchy. -/// -/// Embedding iOS views is an expensive operation and should be avoided when a Flutter -/// equivalent is possible. -/// -/// {@macro flutter.widgets.AndroidView.layout} -/// -/// {@macro flutter.widgets.AndroidView.gestures} -/// -/// {@macro flutter.widgets.AndroidView.lifetime} +/// Common superclass for iOS and macOS platform views. /// -/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints -/// nothing while maintaining the same layout constraints. -/// -/// Clipping operations on a UiKitView can result slow performance. -/// If a conic path clipping is applied to a UIKitView, -/// a quad path is used to approximate the clip due to limitation of Quartz. -class UiKitView extends StatefulWidget { - /// Creates a widget that embeds an iOS view. +/// Platform views are used to embed native views in the widget hierarchy, with +/// support for transforms, clips, and opacity similar to any other Flutter widget. +abstract class _DarwinView extends StatefulWidget { + /// Creates a widget that embeds a platform view. /// /// {@macro flutter.widgets.AndroidView.constructorArgs} - const UiKitView({ + const _DarwinView({ super.key, required this.viewType, this.onPlatformViewCreated, @@ -244,13 +229,13 @@ class UiKitView extends StatefulWidget { /// {@macro flutter.widgets.AndroidView.layoutDirection} final TextDirection? layoutDirection; - /// Passed as the `arguments` argument of [-\[FlutterPlatformViewFactory createWithFrame:viewIdentifier:arguments:\]](/objcdoc/Protocols/FlutterPlatformViewFactory.html#/c:objc(pl)FlutterPlatformViewFactory(im)createWithFrame:viewIdentifier:arguments:) + /// Passed as the `arguments` argument of [-\[FlutterPlatformViewFactory createWithFrame:viewIdentifier:arguments:\]](/ios-embedder/protocol_flutter_platform_view_factory-p.html#a4e3c4390cd6ebd982390635e9bca4edc) /// /// This can be used by plugins to pass constructor parameters to the embedded iOS view. final dynamic creationParams; /// The codec used to encode `creationParams` before sending it to the - /// platform side. It should match the codec returned by [-\[FlutterPlatformViewFactory createArgsCodec:\]](/objcdoc/Protocols/FlutterPlatformViewFactory.html#/c:objc(pl)FlutterPlatformViewFactory(im)createArgsCodec) + /// platform side. It should match the codec returned by [-\[FlutterPlatformViewFactory createArgsCodec:\]](/ios-embedder/protocol_flutter_platform_view_factory-p.html#a32c3c067cb45a83dfa720c74a0d5c93c) /// /// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec]. /// @@ -299,11 +284,88 @@ class UiKitView extends StatefulWidget { // TODO(amirh): get a list of GestureRecognizers here. // https://github.com/flutter/flutter/issues/20953 final Set>? gestureRecognizers; +} + +// TODO(amirh): describe the embedding mechanism. +// TODO(ychris): remove the documentation for conic path not supported once https://github.com/flutter/flutter/issues/35062 is resolved. +/// Embeds an iOS view in the Widget hierarchy. +/// +/// Embedding iOS views is an expensive operation and should be avoided when a Flutter +/// equivalent is possible. +/// +/// {@macro flutter.widgets.AndroidView.layout} +/// +/// {@macro flutter.widgets.AndroidView.gestures} +/// +/// {@macro flutter.widgets.AndroidView.lifetime} +/// +/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints +/// nothing while maintaining the same layout constraints. +/// +/// Clipping operations on a UiKitView can result slow performance. +/// If a conic path clipping is applied to a UIKitView, +/// a quad path is used to approximate the clip due to limitation of Quartz. +class UiKitView extends _DarwinView { + /// Creates a widget that embeds an iOS view. + /// + /// {@macro flutter.widgets.AndroidView.constructorArgs} + const UiKitView({ + super.key, + required super.viewType, + super.onPlatformViewCreated, + super.hitTestBehavior = PlatformViewHitTestBehavior.opaque, + super.layoutDirection, + super.creationParams, + super.creationParamsCodec, + super.gestureRecognizers, + }) : assert(creationParams == null || creationParamsCodec != null); @override State createState() => _UiKitViewState(); } +/// Widget that contains a macOS AppKit view. +/// +/// Embedding macOS views is an expensive operation and should be avoided where +/// a Flutter equivalent is possible. +/// +/// The platform view's lifetime is the same as the lifetime of the [State] +/// object for this widget. When the [State] is disposed the platform view (and +/// auxiliary resources) are lazily released (some resources are immediately +/// released and some by platform garbage collector). A stateful widget's state +/// is disposed when the widget is removed from the tree or when it is moved +/// within the tree. If the stateful widget has a key and it's only moved +/// relative to its siblings, or it has a [GlobalKey] and it's moved within the +/// tree, it will not be disposed. +/// +/// Construction of AppKitViews is done asynchronously, before the underlying +/// NSView is ready this widget paints nothing while maintaining the same +/// layout constraints. +class AppKitView extends _DarwinView { + /// Creates a widget that embeds a macOS AppKit NSView. + const AppKitView({ + super.key, + required super.viewType, + super.onPlatformViewCreated, + super.hitTestBehavior = PlatformViewHitTestBehavior.opaque, + super.layoutDirection, + super.creationParams, + super.creationParamsCodec, + super.gestureRecognizers, + }); + + @override + State createState() => _AppKitViewState(); +} + +/// Callback signature for when the platform view's DOM element was created. +/// +/// [element] is the DOM element that was created. +/// +/// Also see [HtmlElementView.fromTagName] that uses this callback +/// signature. +typedef ElementCreatedCallback = void Function(Object element); + /// Embeds an HTML element in the Widget hierarchy in Flutter Web. /// /// *NOTE*: This only works in Flutter Web. To embed web content on other @@ -348,6 +410,52 @@ class HtmlElementView extends StatelessWidget { this.creationParams, }); + /// Creates a platform view that creates a DOM element specified by [tagName]. + /// + /// [isVisible] indicates whether the view is visible to the user or not. + /// Setting this to false allows the rendering pipeline to perform extra + /// optimizations knowing that the view will not result in any pixels painted + /// on the screen. + /// + /// [onElementCreated] is called when the DOM element is created. It can be + /// used by the app to customize the element by adding attributes and styles. + /// + /// ```dart + /// import 'package:flutter/widgets.dart'; + /// import 'package:web/web.dart' as web; + /// + /// // ... + /// + /// class MyWidget extends StatelessWidget { + /// const MyWidget({super.key}); + /// + /// @override + /// Widget build(BuildContext context) { + /// return HtmlElementView.fromTagName( + /// tagName: 'div', + /// onElementCreated: (Object element) { + /// element as web.HTMLElement; + /// element.style + /// ..backgroundColor = 'blue' + /// ..border = '1px solid red'; + /// }, + /// ); + /// } + /// } + /// ``` + factory HtmlElementView.fromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) => + HtmlElementViewImpl.createFromTagName( + key: key, + tagName: tagName, + isVisible: isVisible, + onElementCreated: onElementCreated, + ); + /// The unique identifier for the HTML view type to be embedded by this widget. /// /// A PlatformViewFactory for this type must have been registered. @@ -362,83 +470,7 @@ class HtmlElementView extends StatelessWidget { final Object? creationParams; @override - Widget build(BuildContext context) { - assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.'); - return PlatformViewLink( - viewType: viewType, - onCreatePlatformView: _createHtmlElementView, - surfaceFactory: (BuildContext context, PlatformViewController controller) { - return PlatformViewSurface( - controller: controller, - gestureRecognizers: const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - ); - } - - /// Creates the controller and kicks off its initialization. - _HtmlElementViewController _createHtmlElementView(PlatformViewCreationParams params) { - final _HtmlElementViewController controller = _HtmlElementViewController( - params.id, - viewType, - creationParams, - ); - controller._initialize().then((_) { - params.onPlatformViewCreated(params.id); - onPlatformViewCreated?.call(params.id); - }); - return controller; - } -} - -class _HtmlElementViewController extends PlatformViewController { - _HtmlElementViewController( - this.viewId, - this.viewType, - this.creationParams, - ); - - @override - final int viewId; - - /// The unique identifier for the HTML view type to be embedded by this widget. - /// - /// A PlatformViewFactory for this type must have been registered. - final String viewType; - - final dynamic creationParams; - - bool _initialized = false; - - Future _initialize() async { - final Map args = { - 'id': viewId, - 'viewType': viewType, - 'params': creationParams, - }; - await SystemChannels.platform_views.invokeMethod('create', args); - _initialized = true; - } - - @override - Future clearFocus() async { - // Currently this does nothing on Flutter Web. - // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 - } - - @override - Future dispatchPointerEvent(PointerEvent event) async { - // We do not dispatch pointer events to HTML views because they may contain - // cross-origin iframes, which only accept user-generated events. - } - - @override - Future dispose() async { - if (_initialized) { - await SystemChannels.platform_views.invokeMethod('dispose', viewId); - } - } + Widget build(BuildContext context) => buildImpl(context); } class _AndroidViewState extends State { @@ -573,8 +605,8 @@ class _AndroidViewState extends State { } } -class _UiKitViewState extends State { - UiKitViewController? _controller; +abstract class _DarwinViewState, ViewT extends _DarwinPlatformView> extends State { + ControllerT? _controller; TextDirection? _layoutDirection; bool _initialized = false; @@ -586,21 +618,19 @@ class _UiKitViewState extends State { @override Widget build(BuildContext context) { - final UiKitViewController? controller = _controller; + final ControllerT? controller = _controller; if (controller == null) { return const SizedBox.expand(); } return Focus( focusNode: focusNode, onFocusChange: (bool isFocused) => _onFocusChange(isFocused, controller), - child: _UiKitPlatformView( - controller: _controller!, - hitTestBehavior: widget.hitTestBehavior, - gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet, - ), + child: childPlatformView() ); } + ViewT childPlatformView(); + void _initializeOnce() { if (_initialized) { return; @@ -625,7 +655,7 @@ class _UiKitViewState extends State { } @override - void didUpdateWidget(UiKitView oldWidget) { + void didUpdateWidget(PlatformViewT oldWidget) { super.didUpdateWidget(oldWidget); final TextDirection newLayoutDirection = _findLayoutDirection(); @@ -634,6 +664,9 @@ class _UiKitViewState extends State { if (widget.viewType != oldWidget.viewType) { _controller?.dispose(); + _controller = null; + focusNode?.dispose(); + focusNode = null; _createNewUiKitView(); return; } @@ -659,15 +692,8 @@ class _UiKitViewState extends State { Future _createNewUiKitView() async { final int id = platformViewsRegistry.getNextPlatformViewId(); - final UiKitViewController controller = await PlatformViewsService.initUiKitView( - id: id, - viewType: widget.viewType, - layoutDirection: _layoutDirection!, - creationParams: widget.creationParams, - creationParamsCodec: widget.creationParamsCodec, - onFocus: () { - focusNode?.requestFocus(); - } + final ControllerT controller = await createNewViewController( + id ); if (!mounted) { controller.dispose(); @@ -680,7 +706,9 @@ class _UiKitViewState extends State { }); } - void _onFocusChange(bool isFocused, UiKitViewController controller) { + Future createNewViewController(int id); + + void _onFocusChange(bool isFocused, ControllerT controller) { if (!isFocused) { // Unlike Android, we do not need to send "clearFocus" channel message // to the engine, because focusing on another view will automatically @@ -694,6 +722,56 @@ class _UiKitViewState extends State { } } +class _UiKitViewState extends _DarwinViewState { + @override + Future createNewViewController(int id) async { + return PlatformViewsService.initUiKitView( + id: id, + viewType: widget.viewType, + layoutDirection: _layoutDirection!, + creationParams: widget.creationParams, + creationParamsCodec: widget.creationParamsCodec, + onFocus: () { + focusNode?.requestFocus(); + } + ); + } + + @override + _UiKitPlatformView childPlatformView() { + return _UiKitPlatformView( + controller: _controller!, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers ?? _DarwinViewState._emptyRecognizersSet, + ); + } +} + +class _AppKitViewState extends _DarwinViewState { + @override + Future createNewViewController(int id) async { + return PlatformViewsService.initAppKitView( + id: id, + viewType: widget.viewType, + layoutDirection: _layoutDirection!, + creationParams: widget.creationParams, + creationParamsCodec: widget.creationParamsCodec, + onFocus: () { + focusNode?.requestFocus(); + } + ); + } + + @override + _AppKitPlatformView childPlatformView() { + return _AppKitPlatformView( + controller: _controller!, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers ?? _DarwinViewState._emptyRecognizersSet, + ); + } +} + class _AndroidPlatformView extends LeafRenderObjectWidget { const _AndroidPlatformView({ required this.controller, @@ -725,17 +803,30 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { } } -class _UiKitPlatformView extends LeafRenderObjectWidget { - const _UiKitPlatformView({ +abstract class _DarwinPlatformView> extends LeafRenderObjectWidget { + const _DarwinPlatformView({ required this.controller, required this.hitTestBehavior, required this.gestureRecognizers, }); - final UiKitViewController controller; + final TController controller; final PlatformViewHitTestBehavior hitTestBehavior; final Set> gestureRecognizers; + @override + @mustCallSuper + void updateRenderObject(BuildContext context, TRender renderObject) { + renderObject + ..viewController = controller + ..hitTestBehavior = hitTestBehavior + ..updateGestureRecognizers(gestureRecognizers); + } +} + +class _UiKitPlatformView extends _DarwinPlatformView { + const _UiKitPlatformView({required super.controller, required super.hitTestBehavior, required super.gestureRecognizers}); + @override RenderObject createRenderObject(BuildContext context) { return RenderUiKitView( @@ -744,12 +835,18 @@ class _UiKitPlatformView extends LeafRenderObjectWidget { gestureRecognizers: gestureRecognizers, ); } +} + +class _AppKitPlatformView extends _DarwinPlatformView { + const _AppKitPlatformView({required super.controller, required super.hitTestBehavior, required super.gestureRecognizers}); @override - void updateRenderObject(BuildContext context, RenderUiKitView renderObject) { - renderObject.viewController = controller; - renderObject.hitTestBehavior = hitTestBehavior; - renderObject.updateGestureRecognizers(gestureRecognizers); + RenderObject createRenderObject(BuildContext context) { + return RenderAppKitView( + viewController: controller, + hitTestBehavior: hitTestBehavior, + gestureRecognizers: gestureRecognizers, + ); } } @@ -841,8 +938,6 @@ typedef CreatePlatformViewCallback = PlatformViewController Function(PlatformVie class PlatformViewLink extends StatefulWidget { /// Construct a [PlatformViewLink] widget. /// - /// The `surfaceFactory` and the `onCreatePlatformView` must not be null. - /// /// See also: /// /// * [PlatformViewSurface] for details on the widget returned by `surfaceFactory`. @@ -984,8 +1079,6 @@ class _PlatformViewLinkState extends State { class PlatformViewSurface extends LeafRenderObjectWidget { /// Construct a [PlatformViewSurface]. - /// - /// The [controller] must not be null. const PlatformViewSurface({ super.key, required this.controller, diff --git a/packages/flutter/lib/src/widgets/pop_scope.dart b/packages/flutter/lib/src/widgets/pop_scope.dart new file mode 100644 index 0000000000000..b47d83fcdbbd0 --- /dev/null +++ b/packages/flutter/lib/src/widgets/pop_scope.dart @@ -0,0 +1,137 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import 'framework.dart'; +import 'navigator.dart'; +import 'routes.dart'; + +/// Manages system back gestures. +/// +/// The [canPop] parameter can be used to disable system back gestures. Defaults +/// to true, meaning that back gestures happen as usual. +/// +/// The [onPopInvoked] parameter reports when system back gestures occur, +/// regardless of whether or not they were successful. +/// +/// If [canPop] is false, then a system back gesture will not pop the route off +/// of the enclosing [Navigator]. [onPopInvoked] will still be called, and +/// `didPop` will be `false`. +/// +/// If [canPop] is true, then a system back gesture will cause the enclosing +/// [Navigator] to receive a pop as usual. [onPopInvoked] will be called with +/// `didPop` as `true`, unless the pop failed for reasons unrelated to +/// [PopScope], in which case it will be `false`. +/// +/// {@tool dartpad} +/// This sample demonstrates how to use this widget to handle nested navigation +/// in a bottom navigation bar. +/// +/// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [NavigatorPopHandler], which is a less verbose way to handle system back +/// gestures in simple cases of nested [Navigator]s. +/// * [Form.canPop] and [Form.onPopInvoked], which can be used to handle system +/// back gestures in the case of a form with unsaved data. +/// * [ModalRoute.registerPopEntry] and [ModalRoute.unregisterPopEntry], +/// which this widget uses to integrate with Flutter's navigation system. +class PopScope extends StatefulWidget { + /// Creates a widget that registers a callback to veto attempts by the user to + /// dismiss the enclosing [ModalRoute]. + const PopScope({ + super.key, + required this.child, + this.canPop = true, + this.onPopInvoked, + }); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + + /// {@template flutter.widgets.PopScope.onPopInvoked} + /// Called after a route pop was handled. + /// {@endtemplate} + /// + /// It's not possible to prevent the pop from happening at the time that this + /// method is called; the pop has already happened. Use [canPop] to + /// disable pops in advance. + /// + /// This will still be called even when the pop is canceled. A pop is canceled + /// when the relevant [Route.popDisposition] returns false, such as when + /// [canPop] is set to false on a [PopScope]. The `didPop` parameter + /// indicates whether or not the back navigation actually happened + /// successfully. + /// + /// See also: + /// + /// * [Route.onPopInvoked], which is similar. + final PopInvokedCallback? onPopInvoked; + + /// {@template flutter.widgets.PopScope.canPop} + /// When false, blocks the current route from being popped. + /// + /// This includes the root route, where upon popping, the Flutter app would + /// exit. + /// + /// If multiple [PopScope] widgets appear in a route's widget subtree, then + /// each and every `canPop` must be `true` in order for the route to be + /// able to pop. + /// + /// [Android's predictive back](https://developer.android.com/guide/navigation/predictive-back-gesture) + /// feature will not animate when this boolean is false. + /// {@endtemplate} + final bool canPop; + + @override + State createState() => _PopScopeState(); +} + +class _PopScopeState extends State implements PopEntry { + ModalRoute? _route; + + @override + PopInvokedCallback? get onPopInvoked => widget.onPopInvoked; + + @override + late final ValueNotifier canPopNotifier; + + @override + void initState() { + super.initState(); + canPopNotifier = ValueNotifier(widget.canPop); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final ModalRoute? nextRoute = ModalRoute.of(context); + if (nextRoute != _route) { + _route?.unregisterPopEntry(this); + _route = nextRoute; + _route?.registerPopEntry(this); + } + } + + @override + void didUpdateWidget(PopScope oldWidget) { + super.didUpdateWidget(oldWidget); + canPopNotifier.value = widget.canPop; + } + + @override + void dispose() { + _route?.unregisterPopEntry(this); + canPopNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart index 832a1f6862bf0..46e611f6d22ff 100644 --- a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart +++ b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart @@ -36,10 +36,6 @@ class RawKeyboardListener extends StatefulWidget { /// /// For text entry, consider using a [EditableText], which integrates with /// on-screen keyboards and input method editors (IMEs). - /// - /// The [focusNode] and [child] arguments are required and must not be null. - /// - /// The [autofocus] argument must not be null. const RawKeyboardListener({ super.key, required this.focusNode, diff --git a/packages/flutter/lib/src/widgets/reorderable_list.dart b/packages/flutter/lib/src/widgets/reorderable_list.dart index cb9de37dec10d..af912e5ac2c13 100644 --- a/packages/flutter/lib/src/widgets/reorderable_list.dart +++ b/packages/flutter/lib/src/widgets/reorderable_list.dart @@ -21,6 +21,7 @@ import 'scrollable.dart'; import 'scrollable_helpers.dart'; import 'sliver.dart'; import 'sliver_prototype_extent_list.dart'; +import 'sliver_varied_extent_list.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -118,6 +119,7 @@ class ReorderableList extends StatefulWidget { this.onReorderStart, this.onReorderEnd, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, this.proxyDecorator, this.padding, @@ -135,10 +137,12 @@ class ReorderableList extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, }) : assert(itemCount >= 0), - assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both', - ); + assert( + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ); /// {@template flutter.widgets.reorderable_list.itemBuilder} /// Called, as needed, to build list item widgets. @@ -253,6 +257,9 @@ class ReorderableList extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.itemExtentBuilder} + final ItemExtentBuilder? itemExtentBuilder; + /// {@macro flutter.widgets.list_view.prototypeItem} final Widget? prototypeItem; @@ -450,14 +457,17 @@ class SliverReorderableList extends StatefulWidget { this.onReorderStart, this.onReorderEnd, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, this.proxyDecorator, double? autoScrollerVelocityScalar, }) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar, assert(itemCount >= 0), assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ); // An eyeballed value for a smooth scrolling experience. @@ -487,6 +497,9 @@ class SliverReorderableList extends StatefulWidget { /// {@macro flutter.widgets.list_view.itemExtent} final double? itemExtent; + /// {@macro flutter.widgets.list_view.itemExtentBuilder} + final ItemExtentBuilder? itemExtentBuilder; + /// {@macro flutter.widgets.list_view.prototypeItem} final Widget? prototypeItem; @@ -785,6 +798,25 @@ class SliverReorderableListState extends State with Ticke } void _dragEnd(_DragInfo item) { + // No changes required if last child is being inserted into the last position. + if ((_insertIndex! + 1 == _items.length) && _reverse) { + final RenderBox lastItemRenderBox = _items[_items.length - 1]!.context.findRenderObject()! as RenderBox; + final Offset lastItemOffset = lastItemRenderBox.localToGlobal(Offset.zero); + + // When drag starts, the corresponding element is removed from + // the list, and moves inside of [ReorderableListState.CustomScrollView], + // which gives [CustomScrollView] a variable height. + // + // So when the element is moved, delta would change accordingly, + // and since it's the last element, + // we animate it back to it's position and add it back to the list. + final double delta = item.itemSize.height; + + setState(() { + _finalDropPosition = Offset(lastItemOffset.dx, lastItemOffset.dy - delta); + }); + return; + } setState(() { if (_insertIndex == item.index) { _finalDropPosition = _itemOffsetAt(_insertIndex! + (_reverse ? 1 : 0)); @@ -832,6 +864,7 @@ class SliverReorderableListState extends State with Ticke _recognizer?.dispose(); _recognizer = null; _overlayEntry?.remove(); + _overlayEntry?.dispose(); _overlayEntry = null; _finalDropPosition = null; } @@ -1035,6 +1068,11 @@ class SliverReorderableListState extends State with Ticke delegate: childrenDelegate, itemExtent: widget.itemExtent!, ); + } else if (widget.itemExtentBuilder != null) { + return SliverVariedExtentList( + delegate: childrenDelegate, + itemExtentBuilder: widget.itemExtentBuilder!, + ); } else if (widget.prototypeItem != null) { return SliverPrototypeExtentList( delegate: childrenDelegate, diff --git a/packages/flutter/lib/src/widgets/restoration.dart b/packages/flutter/lib/src/widgets/restoration.dart index 101e6422daeff..7f532552c008a 100644 --- a/packages/flutter/lib/src/widgets/restoration.dart +++ b/packages/flutter/lib/src/widgets/restoration.dart @@ -54,8 +54,6 @@ class RestorationScope extends StatefulWidget { /// /// Providing null as the [restorationId] turns off state restoration for /// the [child] and its descendants. - /// - /// The [child] must not be null. const RestorationScope({ super.key, required this.restorationId, @@ -199,8 +197,6 @@ class UnmanagedRestorationScope extends InheritedWidget { /// /// When [bucket] is null state restoration is turned off for the [child] and /// its descendants. - /// - /// The [child] must not be null. const UnmanagedRestorationScope({ super.key, this.bucket, @@ -273,8 +269,6 @@ class RootRestorationScope extends StatefulWidget { /// /// Providing null as the [restorationId] turns off state restoration for /// the [child] and its descendants. - /// - /// The [child] must not be null. const RootRestorationScope({ super.key, required this.restorationId, @@ -454,6 +448,13 @@ class _RootRestorationScopeState extends State { /// * [RestorationManager], which describes how state restoration works in /// Flutter. abstract class RestorableProperty extends ChangeNotifier { + /// Creates a [RestorableProperty]. + RestorableProperty(){ + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + /// Called by the [RestorationMixin] if no restoration data is available to /// restore the value of the property from to obtain the default value for the /// property. diff --git a/packages/flutter/lib/src/widgets/router.dart b/packages/flutter/lib/src/widgets/router.dart index 918d36b526b3a..04924fcfd82be 100644 --- a/packages/flutter/lib/src/widgets/router.dart +++ b/packages/flutter/lib/src/widgets/router.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -41,7 +42,7 @@ import 'restoration_properties.dart'; class RouteInformation { /// Creates a route information object. /// - /// Either location or uri must not be null. + /// Either `location` or `uri` must not be null. const RouteInformation({ @Deprecated( 'Pass Uri.parse(location) to uri parameter instead. ' @@ -64,7 +65,7 @@ class RouteInformation { ) String get location { if (_location != null) { - return _location!; + return _location; } return Uri.decodeComponent( Uri( @@ -85,7 +86,7 @@ class RouteInformation { /// In web platform, the host and scheme are always empty. Uri get uri { if (_uri != null){ - return _uri!; + return _uri; } return Uri.parse(_location!); } @@ -119,19 +120,19 @@ class RouteInformation { /// and [BackButtonDispatcher]. This abstract class provides way to bundle these /// delegates into a single object to configure a [Router]. /// -/// The [routerDelegate] must not be null. The [backButtonDispatcher], -/// [routeInformationProvider], and [routeInformationProvider] are optional. +/// The [backButtonDispatcher], [routeInformationProvider], and +/// [routeInformationProvider] are optional. /// /// The [routeInformationProvider] and [routeInformationParser] must -/// both be provided or not provided. +/// both be provided or both not provided. class RouterConfig { /// Creates a [RouterConfig]. /// - /// The [routerDelegate] must not be null. The [backButtonDispatcher], - /// [routeInformationProvider], and [routeInformationParser] are optional. + /// The [backButtonDispatcher], [routeInformationProvider], and + /// [routeInformationParser] are optional. /// - /// The [routeInformationProvider] and [routeInformationParser] must - /// both be provided or not provided. + /// The [routeInformationProvider] and [routeInformationParser] must both be + /// provided or both not provided. const RouterConfig({ this.routeInformationProvider, this.routeInformationParser, @@ -356,8 +357,6 @@ class Router extends StatefulWidget { /// /// The [routeInformationProvider] and [routeInformationParser] must /// both be provided or not provided. - /// - /// The [routerDelegate] must not be null. const Router({ super.key, this.routeInformationProvider, @@ -380,8 +379,6 @@ class Router extends StatefulWidget { /// If the [RouterConfig.routeInformationProvider] is not null, then /// [RouterConfig.routeInformationParser] must also not be /// null. - /// - /// The [RouterConfig.routerDelegate] must not be null. factory Router.withConfig({ Key? key, required RouterConfig config, @@ -1082,8 +1079,6 @@ class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingO /// [parent] of the [ChildBackButtonDispatcher]. class ChildBackButtonDispatcher extends BackButtonDispatcher { /// Creates a back button dispatcher that acts as the child of another. - /// - /// The [parent] must not be null. ChildBackButtonDispatcher(this.parent); /// The back button dispatcher that this object will attempt to take priority @@ -1138,8 +1133,6 @@ class ChildBackButtonDispatcher extends BackButtonDispatcher { /// screen but don't want to use a new page for that. class BackButtonListener extends StatefulWidget { /// Creates a BackButtonListener widget . - /// - /// The [child] and [onBackButtonPressed] arguments must not be null. const BackButtonListener({ super.key, required this.child, @@ -1464,14 +1457,24 @@ class PlatformRouteInformationProvider extends RouteInformationProvider with Wid /// provider. PlatformRouteInformationProvider({ required RouteInformation initialRouteInformation, - }) : _value = initialRouteInformation; + }) : _value = initialRouteInformation { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + + static bool _equals(Uri a, Uri b) { + return a.path == b.path + && a.fragment == b.fragment + && const DeepCollectionEquality.unordered().equals(a.queryParametersAll, b.queryParametersAll); + } @override void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) { final bool replace = type == RouteInformationReportingType.neglect || (type == RouteInformationReportingType.none && - _valueInEngine.uri == routeInformation.uri); + _equals(_valueInEngine.uri, routeInformation.uri)); SystemNavigator.selectMultiEntryHistory(); SystemNavigator.routeInformationUpdated( uri: routeInformation.uri, diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index ad919c96dc697..6edc488d03765 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -717,6 +717,10 @@ mixin LocalHistoryRoute on Route { } } + @Deprecated( + 'Use popDisposition instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', + ) @override Future willPop() async { if (willHandlePopInternally) { @@ -725,6 +729,14 @@ mixin LocalHistoryRoute on Route { return super.willPop(); } + @override + RoutePopDisposition get popDisposition { + if (willHandlePopInternally) { + return RoutePopDisposition.pop; + } + return super.popDisposition; + } + @override bool didPop(T? result) { if (_localHistory != null && _localHistory!.isNotEmpty) { @@ -872,6 +884,7 @@ class _ModalScopeState extends State<_ModalScope> { @override void dispose() { focusScopeNode.dispose(); + primaryScrollController.dispose(); super.dispose(); } @@ -1432,11 +1445,20 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute _willPopCallbacks = []; + final Set _popEntries = {}; + /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with /// [addScopedWillPopCallback] returns either false or null. If they all /// return true, the base [Route.willPop]'s result will be returned. The @@ -1499,6 +1523,10 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute willPop() async { final _ModalScopeState? scope = _scopeKey.currentState; @@ -1511,25 +1539,43 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute { - /// ModalRoute? _route; - /// - /// // ... - /// - /// @override - /// void didChangeDependencies() { - /// super.didChangeDependencies(); - /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); - /// _route = ModalRoute.of(context); - /// _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure); - /// } - /// } - /// ``` - /// {@end-tool} - /// - /// {@tool snippet} - /// If you register a callback manually, be sure to remove the callback with - /// [removeScopedWillPopCallback] by the time the widget has been disposed. A - /// stateful widget can do this in its dispose method (continuing the previous - /// example): - /// - /// ```dart - /// abstract class _MyWidgetState2 extends State { - /// ModalRoute? _route; - /// - /// // ... - /// - /// @override - /// void dispose() { - /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); - /// _route = null; - /// super.dispose(); - /// } - /// } - /// ``` - /// {@end-tool} - /// /// See also: /// /// * [WillPopScope], which manages the registration and unregistration @@ -1590,6 +1593,10 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends PopupRoute { child: child, ); } - return _transitionBuilder!(context, animation, secondaryAnimation, child); + return _transitionBuilder(context, animation, secondaryAnimation, child); } } @@ -2099,8 +2168,7 @@ class RawDialogRoute extends PopupRoute { /// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder` /// does not share a context with the location that [showGeneralDialog] is /// originally called from. Use a [StatefulBuilder] or a custom -/// [StatefulWidget] if the dialog needs to update dynamically. The -/// `pageBuilder` argument can not be null. +/// [StatefulWidget] if the dialog needs to update dynamically. /// /// The `context` argument is used to look up the [Navigator] for the /// dialog. It is only used when the method is called. Its corresponding widget @@ -2203,3 +2271,33 @@ typedef RoutePageBuilder = Widget Function(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child); + +/// A callback type for informing that a navigation pop has been invoked, +/// whether or not it was handled successfully. +/// +/// Accepts a didPop boolean indicating whether or not back navigation +/// succeeded. +typedef PopInvokedCallback = void Function(bool didPop); + +/// Allows listening to and preventing pops. +/// +/// Can be registered in [ModalRoute] to listen to pops with [onPopInvoked] or +/// to enable/disable them with [canPopNotifier]. +/// +/// See also: +/// +/// * [PopScope], which provides similar functionality in a widget. +/// * [ModalRoute.registerPopEntry], which unregisters instances of this. +/// * [ModalRoute.unregisterPopEntry], which unregisters instances of this. +abstract class PopEntry { + /// {@macro flutter.widgets.PopScope.onPopInvoked} + PopInvokedCallback? get onPopInvoked; + + /// {@macro flutter.widgets.PopScope.canPop} + ValueListenable get canPopNotifier; + + @override + String toString() { + return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvoked'; + } +} diff --git a/packages/flutter/lib/src/widgets/safe_area.dart b/packages/flutter/lib/src/widgets/safe_area.dart index a9208d5f0c54a..93048ea3a7a10 100644 --- a/packages/flutter/lib/src/widgets/safe_area.dart +++ b/packages/flutter/lib/src/widgets/safe_area.dart @@ -146,8 +146,6 @@ class SafeArea extends StatelessWidget { /// system. class SliverSafeArea extends StatelessWidget { /// Creates a sliver that avoids operating system interfaces. - /// - /// The [left], [top], [right], [bottom], and [minimum] arguments must not be null. const SliverSafeArea({ super.key, this.left = true, diff --git a/packages/flutter/lib/src/widgets/scroll_activity.dart b/packages/flutter/lib/src/widgets/scroll_activity.dart index 64376b7dd5e73..81a1da840e61a 100644 --- a/packages/flutter/lib/src/widgets/scroll_activity.dart +++ b/packages/flutter/lib/src/widgets/scroll_activity.dart @@ -63,6 +63,8 @@ abstract class ScrollActivity { ScrollActivityDelegate get delegate => _delegate; ScrollActivityDelegate _delegate; + bool _isDisposed = false; + /// Updates the activity's link to the [ScrollActivityDelegate]. /// /// This should only be called when an activity is being moved from a defunct @@ -134,7 +136,9 @@ abstract class ScrollActivity { /// Called when the scroll view stops performing this activity. @mustCallSuper - void dispose() { } + void dispose() { + _isDisposed = true; + } @override String toString() => describeIdentity(this); @@ -226,8 +230,6 @@ class HoldScrollActivity extends ScrollActivity implements ScrollHoldController class ScrollDragController implements Drag { /// Creates an object that scrolls a scroll view as the user drags their /// finger across the screen. - /// - /// The [delegate] and `details` arguments must not be null. ScrollDragController({ required ScrollActivityDelegate delegate, required DragStartDetails details, @@ -521,8 +523,6 @@ class DragScrollActivity extends ScrollActivity { /// animation parameters. class BallisticScrollActivity extends ScrollActivity { /// Creates an activity that animates a scroll view based on a [simulation]. - /// - /// The [delegate], [simulation], and [vsync] arguments must not be null. BallisticScrollActivity( super.delegate, Simulation simulation, @@ -535,7 +535,7 @@ class BallisticScrollActivity extends ScrollActivity { ) ..addListener(_tick) ..animateWith(simulation) - .whenComplete(_end); // won't trigger if we dispose _controller first + .whenComplete(_end); // won't trigger if we dispose _controller before it completes. } late AnimationController _controller; @@ -569,7 +569,11 @@ class BallisticScrollActivity extends ScrollActivity { } void _end() { - delegate.goBallistic(0.0); + // Check if the activity was disposed before going ballistic because _end might be called + // if _controller is disposed just after completion. + if (!_isDisposed) { + delegate.goBallistic(0.0); + } } @override @@ -610,8 +614,6 @@ class BallisticScrollActivity extends ScrollActivity { class DrivenScrollActivity extends ScrollActivity { /// Creates an activity that animates a scroll view based on animation /// parameters. - /// - /// All of the parameters must be non-null. DrivenScrollActivity( super.delegate, { required double from, @@ -628,7 +630,7 @@ class DrivenScrollActivity extends ScrollActivity { ) ..addListener(_tick) ..animateTo(to, duration: duration, curve: curve) - .whenComplete(_end); // won't trigger if we dispose _controller first + .whenComplete(_end); // won't trigger if we dispose _controller before it completes. } late final Completer _completer; @@ -648,7 +650,11 @@ class DrivenScrollActivity extends ScrollActivity { } void _end() { - delegate.goBallistic(velocity); + // Check if the activity was disposed before going ballistic because _end might be called + // if _controller is disposed just after completion. + if (!_isDisposed) { + delegate.goBallistic(velocity); + } } @override diff --git a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart index 4009be91ed2ad..11d903e808f83 100644 --- a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart +++ b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart @@ -45,8 +45,7 @@ class ScrollAwareImageProvider extends ImageProvider { /// Creates a [ScrollAwareImageProvider]. /// /// The [context] object is the [BuildContext] of the [State] using this - /// provider. It is used to determine scrolling velocity during [resolve]. It - /// must not be null. + /// provider. It is used to determine scrolling velocity during [resolve]. /// /// The [imageProvider] is used to create a key and load the image. It must /// not be null, and is assumed to interact with the cache in the normal way @@ -63,7 +62,7 @@ class ScrollAwareImageProvider extends ImageProvider { /// been resolved. final DisposableBuildContext context; - /// The wrapped image provider to delegate [obtainKey] and [load] to. + /// The wrapped image provider to delegate [obtainKey] and [loadImage] to. final ImageProvider imageProvider; @override @@ -105,9 +104,6 @@ class ScrollAwareImageProvider extends ImageProvider { imageProvider.resolveStreamForKey(configuration, stream, key, handleError); } - @override - ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode); - @override ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode); diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart index 7258d602324ae..653e839b4d06e 100644 --- a/packages/flutter/lib/src/widgets/scroll_configuration.dart +++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart @@ -27,10 +27,6 @@ const Set _kTouchLikeDeviceTypes = { PointerDeviceKind.unknown, }; -/// The default overscroll indicator applied on [TargetPlatform.android]. -// TODO(Piinks): Complete migration to stretch by default. -const AndroidOverscrollIndicator _kDefaultAndroidOverscrollIndicator = AndroidOverscrollIndicator.glow; - /// Types of overscroll indicators supported by [TargetPlatform.android]. enum AndroidOverscrollIndicator { /// Utilizes a [StretchingOverscrollIndicator], which transforms the contents @@ -67,28 +63,7 @@ enum AndroidOverscrollIndicator { @immutable class ScrollBehavior { /// Creates a description of how [Scrollable] widgets should behave. - const ScrollBehavior({ - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - AndroidOverscrollIndicator? androidOverscrollIndicator, - }): _androidOverscrollIndicator = androidOverscrollIndicator; - - /// Specifies which overscroll indicator to use on [TargetPlatform.android]. - /// - /// Cannot be null. Defaults to [AndroidOverscrollIndicator.glow]. - /// - /// See also: - /// - /// * [MaterialScrollBehavior], which supports setting this property - /// using [ThemeData]. - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? _kDefaultAndroidOverscrollIndicator; - final AndroidOverscrollIndicator? _androidOverscrollIndicator; + const ScrollBehavior(); /// Creates a copy of this ScrollBehavior, making it possible to /// easily toggle `scrollbar` and `overscrollIndicator` effects. @@ -105,11 +80,6 @@ class ScrollBehavior { Set? pointerAxisModifiers, ScrollPhysics? physics, TargetPlatform? platform, - @Deprecated( - 'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. ' - 'This feature was deprecated after v2.13.0-0.0.pre.' - ) - AndroidOverscrollIndicator? androidOverscrollIndicator, }) { return _WrappedScrollBehavior( delegate: this, @@ -119,7 +89,6 @@ class ScrollBehavior { pointerAxisModifiers: pointerAxisModifiers, physics: physics, platform: platform, - androidOverscrollIndicator: androidOverscrollIndicator ); } @@ -187,23 +156,13 @@ class ScrollBehavior { case TargetPlatform.windows: return child; case TargetPlatform.android: - switch (androidOverscrollIndicator) { - case AndroidOverscrollIndicator.stretch: - return StretchingOverscrollIndicator( - axisDirection: details.direction, - child: child, - ); - case AndroidOverscrollIndicator.glow: - break; - } case TargetPlatform.fuchsia: - break; + return GlowingOverscrollIndicator( + axisDirection: details.direction, + color: _kDefaultGlowColor, + child: child, + ); } - return GlowingOverscrollIndicator( - axisDirection: details.direction, - color: _kDefaultGlowColor, - child: child, - ); } /// Specifies the type of velocity tracker to use in the descendant @@ -289,9 +248,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { Set? pointerAxisModifiers, this.physics, this.platform, - AndroidOverscrollIndicator? androidOverscrollIndicator, - }) : _androidOverscrollIndicator = androidOverscrollIndicator, - _dragDevices = dragDevices, + }) : _dragDevices = dragDevices, _pointerAxisModifiers = pointerAxisModifiers; final ScrollBehavior delegate; @@ -301,8 +258,6 @@ class _WrappedScrollBehavior implements ScrollBehavior { final TargetPlatform? platform; final Set? _dragDevices; final Set? _pointerAxisModifiers; - @override - final AndroidOverscrollIndicator? _androidOverscrollIndicator; @override Set get dragDevices => _dragDevices ?? delegate.dragDevices; @@ -310,9 +265,6 @@ class _WrappedScrollBehavior implements ScrollBehavior { @override Set get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers; - @override - AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator; - @override Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { if (overscroll) { @@ -337,7 +289,6 @@ class _WrappedScrollBehavior implements ScrollBehavior { Set? pointerAxisModifiers, ScrollPhysics? physics, TargetPlatform? platform, - AndroidOverscrollIndicator? androidOverscrollIndicator }) { return delegate.copyWith( scrollbars: scrollbars ?? this.scrollbars, @@ -346,7 +297,6 @@ class _WrappedScrollBehavior implements ScrollBehavior { pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers, physics: physics ?? this.physics, platform: platform ?? this.platform, - androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator, ); } @@ -387,8 +337,6 @@ class _WrappedScrollBehavior implements ScrollBehavior { /// decorations used by descendants of [child]. class ScrollConfiguration extends InheritedWidget { /// Creates a widget that controls how [Scrollable] widgets behave in a subtree. - /// - /// The [behavior] and [child] arguments must not be null. const ScrollConfiguration({ super.key, required this.behavior, diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index 8e38101bade3d..de88a41acc4a5 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -57,15 +57,17 @@ typedef ScrollControllerCallback = void Function(ScrollPosition position); /// listen to scrolling occur without using a [ScrollController]. class ScrollController extends ChangeNotifier { /// Creates a controller for a scrollable widget. - /// - /// The values of `initialScrollOffset` and `keepScrollOffset` must not be null. ScrollController({ double initialScrollOffset = 0.0, this.keepScrollOffset = true, this.debugLabel, this.onAttach, this.onDetach, - }) : _initialScrollOffset = initialScrollOffset; + }) : _initialScrollOffset = initialScrollOffset { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } /// The initial value to use for [offset]. /// diff --git a/packages/flutter/lib/src/widgets/scroll_delegate.dart b/packages/flutter/lib/src/widgets/scroll_delegate.dart index ba5e826b9498c..eb2ec00e409d7 100644 --- a/packages/flutter/lib/src/widgets/scroll_delegate.dart +++ b/packages/flutter/lib/src/widgets/scroll_delegate.dart @@ -395,8 +395,8 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { /// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives} /// Whether to wrap each child in an [AutomaticKeepAlive]. /// - /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] - /// widgets so that children can use [KeepAliveNotification]s to preserve + /// Typically, lazily laid out children are wrapped in [AutomaticKeepAlive] + /// widgets so that the children can use [KeepAliveNotification]s to preserve /// their state when they would otherwise be garbage collected off-screen. /// /// This feature (and [addRepaintBoundaries]) must be disabled if the children @@ -670,22 +670,22 @@ class SliverChildListDelegate extends SliverChildDelegate { } // Lazily fill the [_keyToIndex]. if (!_keyToIndex!.containsKey(key)) { - int index = _keyToIndex![null]!; + int index = _keyToIndex[null]!; while (index < children.length) { final Widget child = children[index]; if (child.key != null) { - _keyToIndex![child.key] = index; + _keyToIndex[child.key] = index; } if (child.key == key) { // Record current index for next function call. - _keyToIndex![null] = index + 1; + _keyToIndex[null] = index + 1; return index; } index += 1; } - _keyToIndex![null] = index; + _keyToIndex[null] = index; } else { - return _keyToIndex![key]; + return _keyToIndex[key]; } return null; } @@ -863,7 +863,6 @@ Widget _createErrorWidget(Object exception, StackTrace stackTrace) { return ErrorWidget.builder(details); } -// TODO(Piinks): Come back and add keep alive support, https://github.com/flutter/flutter/issues/126297 /// A delegate that supplies children for scrolling in two dimensions. /// /// A [TwoDimensionalScrollView] lazily constructs its box children to avoid @@ -929,10 +928,11 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { /// Creates a delegate that supplies children for a [TwoDimensionalScrollView] /// using the given builder callback. TwoDimensionalChildBuilderDelegate({ - this.addRepaintBoundaries = true, required this.builder, int? maxXIndex, int? maxYIndex, + this.addRepaintBoundaries = true, + this.addAutomaticKeepAlives = true, }) : assert(maxYIndex == null || maxYIndex >= -1), assert(maxXIndex == null || maxXIndex >= -1), _maxYIndex = maxYIndex, @@ -1030,6 +1030,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries} final bool addRepaintBoundaries; + /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives} + final bool addAutomaticKeepAlives; + @override Widget? build(BuildContext context, ChildVicinity vicinity) { // If we have exceeded explicit upper bounds, return null. @@ -1052,6 +1055,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { if (addRepaintBoundaries) { child = RepaintBoundary(child: child); } + if (addAutomaticKeepAlives) { + child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child)); + } return child; } @@ -1097,6 +1103,7 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { /// null. TwoDimensionalChildListDelegate({ this.addRepaintBoundaries = true, + this.addAutomaticKeepAlives = true, required this.children, }); @@ -1116,6 +1123,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries} final bool addRepaintBoundaries; + /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives} + final bool addAutomaticKeepAlives; + @override Widget? build(BuildContext context, ChildVicinity vicinity) { // If we have exceeded explicit upper bounds, return null. @@ -1130,7 +1140,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { if (addRepaintBoundaries) { child = RepaintBoundary(child: child); } - + if (addAutomaticKeepAlives) { + child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child)); + } return child; } diff --git a/packages/flutter/lib/src/widgets/scroll_notification_observer.dart b/packages/flutter/lib/src/widgets/scroll_notification_observer.dart index 4419269da84ba..f7112ec149437 100644 --- a/packages/flutter/lib/src/widgets/scroll_notification_observer.dart +++ b/packages/flutter/lib/src/widgets/scroll_notification_observer.dart @@ -82,8 +82,6 @@ final class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { /// {@end-tool} class ScrollNotificationObserver extends StatefulWidget { /// Create a [ScrollNotificationObserver]. - /// - /// The [child] parameter must not be null. const ScrollNotificationObserver({ super.key, required this.child, diff --git a/packages/flutter/lib/src/widgets/scroll_physics.dart b/packages/flutter/lib/src/widgets/scroll_physics.dart index 59ee6fcc50aed..ef8ca02b0a143 100644 --- a/packages/flutter/lib/src/widgets/scroll_physics.dart +++ b/packages/flutter/lib/src/widgets/scroll_physics.dart @@ -220,14 +220,10 @@ class ScrollPhysics { /// Provides a heuristic to determine if expensive frame-bound tasks should be /// deferred. /// - /// The velocity parameter must not be null, but may be positive, negative, or - /// zero. + /// The `velocity` parameter may be positive, negative, or zero. /// - /// The metrics parameter must not be null. - /// - /// The context parameter must not be null. It normally refers to the - /// [BuildContext] of the widget making the call, such as an [Image] widget - /// in a [ListView]. + /// The `context` parameter normally refers to the [BuildContext] of the widget + /// making the call, such as an [Image] widget in a [ListView]. /// /// This can be used to determine whether decoding or fetching complex data /// for the currently visible part of the viewport should be delayed diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index e9154a1218afb..e1ec77eae2b78 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -178,8 +178,6 @@ enum ScrollPositionAlignmentPolicy { abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// Creates an object that determines which portion of the content is visible /// in a scroll view. - /// - /// The [physics], [context], and [keepScrollOffset] parameters must not be null. ScrollPosition({ required this.physics, required this.context, @@ -812,14 +810,32 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { double target; switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) { case ScrollPositionAlignmentPolicy.explicit: - target = clampDouble(viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset, minScrollExtent, maxScrollExtent); + target = viewport.getOffsetToReveal( + object, + alignment, + rect: targetRect, + axis: axis, + ).offset; + target = clampDouble(target, minScrollExtent, maxScrollExtent); case ScrollPositionAlignmentPolicy.keepVisibleAtEnd: - target = clampDouble(viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent); + target = viewport.getOffsetToReveal( + object, + 1.0, // Aligns to end + rect: targetRect, + axis: axis, + ).offset; + target = clampDouble(target, minScrollExtent, maxScrollExtent); if (target < pixels) { target = pixels; } case ScrollPositionAlignmentPolicy.keepVisibleAtStart: - target = clampDouble(viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent); + target = viewport.getOffsetToReveal( + object, + 0.0, // Aligns to start + rect: targetRect, + axis: axis, + ).offset; + target = clampDouble(target, minScrollExtent, maxScrollExtent); if (target > pixels) { target = pixels; } diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 33708cc11d648..0e6b4ed248685 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -24,6 +24,7 @@ import 'scrollable.dart'; import 'scrollable_helpers.dart'; import 'sliver.dart'; import 'sliver_prototype_extent_list.dart'; +import 'sliver_varied_extent_list.dart'; import 'viewport.dart'; // Examples can assume: @@ -61,6 +62,16 @@ enum ScrollViewKeyboardDismissBehavior { /// To control the initial scroll offset of the scroll view, provide a /// [controller] with its [ScrollController.initialScrollOffset] property set. /// +/// {@template flutter.widgets.ScrollView.PageStorage} +/// ## Persisting the scroll position during a session +/// +/// Scroll views attempt to persist their scroll position using [PageStorage]. +/// This can be disabled by setting [ScrollController.keepScrollOffset] to false +/// on the [controller]. If it is enabled, using a [PageStorageKey] for the +/// [key] of this widget is recommended to help disambiguate different scroll +/// views from each other. +/// {@endtemplate} +/// /// See also: /// /// * [ListView], which is a commonly used [ScrollView] that displays a @@ -86,9 +97,7 @@ abstract class ScrollView extends StatelessWidget { /// /// If the [shrinkWrap] argument is true, the [center] argument must be null. /// - /// The [scrollDirection], [reverse], and [shrinkWrap] arguments must not be null. - /// - /// The [anchor] argument must be non-null and in the range 0.0 to 1.0. + /// The [anchor] argument must be in the range zero to one, inclusive. const ScrollView({ super.key, this.scrollDirection = Axis.vertical, @@ -619,6 +628,8 @@ abstract class ScrollView extends StatelessWidget { /// parameter `semanticChildCount`. This should always be the same as the /// number of widgets wrapped in [IndexedSemantics]. /// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// /// See also: /// /// * [SliverList], which is a sliver that displays linear list of children. @@ -1165,6 +1176,8 @@ abstract class BoxScrollView extends ScrollView { /// /// {@macro flutter.widgets.BoxScroll.scrollBehaviour} /// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// /// See also: /// /// * [SingleChildScrollView], which is a scrollable widget that has a single @@ -1216,6 +1229,7 @@ class ListView extends BoxScrollView { super.shrinkWrap, super.padding, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, @@ -1228,8 +1242,10 @@ class ListView extends BoxScrollView { super.restorationId, super.clipBehavior, }) : assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both.', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ), childrenDelegate = SliverChildListDelegate( children, @@ -1289,6 +1305,7 @@ class ListView extends BoxScrollView { super.shrinkWrap, super.padding, this.itemExtent, + this.itemExtentBuilder, this.prototypeItem, required NullableIndexedWidgetBuilder itemBuilder, ChildIndexGetter? findChildIndexCallback, @@ -1305,8 +1322,10 @@ class ListView extends BoxScrollView { }) : assert(itemCount == null || itemCount >= 0), assert(semanticChildCount == null || semanticChildCount <= itemCount!), assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both.', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ), childrenDelegate = SliverChildBuilderDelegate( itemBuilder, @@ -1394,6 +1413,7 @@ class ListView extends BoxScrollView { super.clipBehavior, }) : assert(itemCount >= 0), itemExtent = null, + itemExtentBuilder = null, prototypeItem = null, childrenDelegate = SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -1514,6 +1534,7 @@ class ListView extends BoxScrollView { super.padding, this.itemExtent, this.prototypeItem, + this.itemExtentBuilder, required this.childrenDelegate, super.cacheExtent, super.semanticChildCount, @@ -1522,8 +1543,10 @@ class ListView extends BoxScrollView { super.restorationId, super.clipBehavior, }) : assert( - itemExtent == null || prototypeItem == null, - 'You can only pass itemExtent or prototypeItem, not both', + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', ); /// {@template flutter.widgets.list_view.itemExtent} @@ -1542,9 +1565,38 @@ class ListView extends BoxScrollView { /// extent along the main axis. /// * The [prototypeItem] property, which allows forcing the children's /// extent to be the same as the given widget. + /// * The [itemExtentBuilder] property, which allows forcing the children's + /// extent to be the value returned by the callback. /// {@endtemplate} final double? itemExtent; + /// {@template flutter.widgets.list_view.itemExtentBuilder} + /// If non-null, forces the children to have the corresponding extent returned + /// by the builder. + /// + /// Specifying an [itemExtentBuilder] is more efficient than letting the children + /// determine their own extent because the scrolling machinery can make use of + /// the foreknowledge of the children's extent to save work, for example when + /// the scroll position changes drastically. + /// + /// This will be called multiple times during the layout phase of a frame to find + /// the items that should be loaded by the lazy loading process. + /// + /// Unlike [itemExtent] or [prototypeItem], this allows children to have + /// different extents. + /// + /// See also: + /// + /// * [SliverVariedExtentList], the sliver used internally when this property + /// is provided. It constrains its box children to have a specific given + /// extent along the main axis. + /// * The [itemExtent] property, which allows forcing the children's extent + /// to a given value. + /// * The [prototypeItem] property, which allows forcing the children's + /// extent to be the same as the given widget. + /// {@endtemplate} + final ItemExtentBuilder? itemExtentBuilder; + /// {@template flutter.widgets.list_view.prototypeItem} /// If non-null, forces the children to have the same extent as the given /// widget in the scroll direction. @@ -1561,6 +1613,8 @@ class ListView extends BoxScrollView { /// extent as a prototype item along the main axis. /// * The [itemExtent] property, which allows forcing the children's extent /// to a given value. + /// * The [itemExtentBuilder] property, which allows forcing the children's + /// extent to be the value returned by the callback. /// {@endtemplate} final Widget? prototypeItem; @@ -1579,6 +1633,11 @@ class ListView extends BoxScrollView { delegate: childrenDelegate, itemExtent: itemExtent!, ); + } else if (itemExtentBuilder != null) { + return SliverVariedExtentList( + delegate: childrenDelegate, + itemExtentBuilder: itemExtentBuilder!, + ); } else if (prototypeItem != null) { return SliverPrototypeExtentList( delegate: childrenDelegate, @@ -1667,6 +1726,10 @@ class ListView extends BoxScrollView { /// [SliverList] or [SliverAppBar], can be put in the [CustomScrollView.slivers] /// list. /// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// +/// ## Examples +/// /// {@tool snippet} /// This example demonstrates how to create a [GridView] with two columns. The /// children are spaced apart using the `crossAxisSpacing` and `mainAxisSpacing` @@ -1772,6 +1835,25 @@ class ListView extends BoxScrollView { /// ``` /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows a custom implementation of selection in list and grid views. +/// Use the button in the top right (possibly hidden under the DEBUG banner) to toggle between +/// [ListView] and [GridView]. +/// Long press any [ListTile] or [GridTile] to enable selection mode. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows a custom [SliverGridDelegate]. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// +/// ## Troubleshooting +/// +/// ### Padding +/// /// By default, [GridView] will automatically pad the limits of the /// grid's scrollable to avoid partial obstructions indicated by /// [MediaQuery]'s padding. To avoid this behavior, override with a @@ -1803,13 +1885,6 @@ class ListView extends BoxScrollView { /// ``` /// {@end-tool} /// -/// {@tool dartpad} -/// This example shows a custom implementation of [ListTile] selection in a [GridView] or [ListView]. -/// Long press any ListTile to enable selection mode. -/// -/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** -/// {@end-tool} -/// /// See also: /// /// * [SingleChildScrollView], which is a scrollable widget that has a single @@ -1830,8 +1905,6 @@ class GridView extends BoxScrollView { /// Creates a scrollable, 2D array of widgets with a custom /// [SliverGridDelegate]. /// - /// The [gridDelegate] argument must not be null. - /// /// The `addAutomaticKeepAlives` argument corresponds to the /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The /// `addRepaintBoundaries` argument corresponds to the @@ -1930,8 +2003,6 @@ class GridView extends BoxScrollView { /// /// To use an [IndexedWidgetBuilder] callback to build children, either use /// a [SliverChildBuilderDelegate] or use the [GridView.builder] constructor. - /// - /// The [gridDelegate] and [childrenDelegate] arguments must not be null. const GridView.custom({ super.key, super.scrollDirection, diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 17a90007d42bb..86c350401d4b6 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -44,6 +44,13 @@ typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset p /// which the scrollable content is displayed. typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition); +// The return type of _performEnsureVisible. +// +// The list of futures represents each pending ScrollPosition call to +// ensureVisible. The returned ScrollableState's context is used to find the +// next potential ancestor Scrollable. +typedef _EnsureVisibleResults = (List>, ScrollableState); + /// A widget that manages scrolling in one dimension and informs the [Viewport] /// through which the content is viewed. /// @@ -72,6 +79,14 @@ typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, Vi /// [PageController], which creates a page-oriented scroll position subclass /// that keeps the same page visible when the [Scrollable] resizes. /// +/// ## Persisting the scroll position during a session +/// +/// Scrollables attempt to persist their scroll position using [PageStorage]. +/// This can be disabled by setting [ScrollController.keepScrollOffset] to false +/// on the [controller]. If it is enabled, using a [PageStorageKey] for the +/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is +/// recommended to help disambiguate different [Scrollable]s from each other. +/// /// See also: /// /// * [ListView], which is a commonly used [ScrollView] that displays a @@ -88,8 +103,6 @@ typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, Vi /// the scroll position without using a [ScrollController]. class Scrollable extends StatefulWidget { /// Creates a widget that scrolls. - /// - /// The [axisDirection] and [viewportBuilder] arguments must not be null. const Scrollable({ super.key, this.axisDirection = AxisDirection.down, @@ -435,6 +448,10 @@ class Scrollable extends StatefulWidget { /// Scrolls the scrollables that enclose the given context so as to make the /// given context visible. + /// + /// If the [Scrollable] of the provided [BuildContext] is a + /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure + /// the target is made visible. static Future ensureVisible( BuildContext context, { double alignment = 0.0, @@ -453,14 +470,16 @@ class Scrollable extends StatefulWidget { RenderObject? targetRenderObject; ScrollableState? scrollable = Scrollable.maybeOf(context); while (scrollable != null) { - futures.add(scrollable.position.ensureVisible( + final List> newFutures; + (newFutures, scrollable) = scrollable._performEnsureVisible( context.findRenderObject()!, alignment: alignment, duration: duration, curve: curve, alignmentPolicy: alignmentPolicy, targetRenderObject: targetRenderObject, - )); + ); + futures.addAll(newFutures); targetRenderObject = targetRenderObject ?? context.findRenderObject(); context = scrollable.context; @@ -624,6 +643,12 @@ class ScrollableState extends State with TickerProviderStateMixin, R } bool _shouldUpdatePosition(Scrollable oldWidget) { + if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { + return true; + } + if (widget.scrollBehavior != null && oldWidget.scrollBehavior != null && widget.scrollBehavior!.shouldNotify(oldWidget.scrollBehavior!)) { + return true; + } ScrollPhysics? newPhysics = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); ScrollPhysics? oldPhysics = oldWidget.physics ?? oldWidget.scrollBehavior?.getScrollPhysics(context); do { @@ -645,7 +670,7 @@ class ScrollableState extends State with TickerProviderStateMixin, R if (oldWidget.controller == null) { // The old controller was null, meaning the fallback cannot be null. // Dispose of the fallback. - assert(_fallbackScrollController != null); + assert(_fallbackScrollController != null); assert(widget.controller != null); _fallbackScrollController!.detach(position); _fallbackScrollController!.dispose(); @@ -999,6 +1024,28 @@ class ScrollableState extends State with TickerProviderStateMixin, R return result; } + // Returns the Future from calling ensureVisible for the ScrollPosition, as + // as well as this ScrollableState instance so its context can be used to + // check for other ancestor Scrollables in executing ensureVisible. + _EnsureVisibleResults _performEnsureVisible( + RenderObject object, { + double alignment = 0.0, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, + RenderObject? targetRenderObject, + }) { + final Future ensureVisibleFuture = position.ensureVisible( + object, + alignment: alignment, + duration: duration, + curve: curve, + alignmentPolicy: alignmentPolicy, + targetRenderObject: targetRenderObject, + ); + return (>[ ensureVisibleFuture ], this); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -1169,11 +1216,11 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont if (event.type == SelectionEventType.endEdgeUpdate) { _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); - event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset); + event = SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset, granularity: event.granularity); } else { _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); - event = SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset); + event = SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset, granularity: event.granularity); } final SelectionResult result = super.handleSelectionEdgeUpdate(event); @@ -1422,6 +1469,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset)); + // Make sure we track that we have synthesized a start event for this selectable, + // so we don't synthesize events unnecessarily. + _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; } final double? previousEndRecord = _selectableEndEdgeUpdateRecords[selectable]; if (_currentDragEndRelatedToOrigin != null && @@ -1430,6 +1480,9 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(-deltaToOrigin.dx, -deltaToOrigin.dy); selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset)); + // Make sure we track that we have synthesized an end event for this selectable, + // so we don't synthesize events unnecessarily. + _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; } } @@ -1901,7 +1954,7 @@ class TwoDimensionalScrollableState extends State { if (oldWidget.horizontalDetails.controller == null) { // The old controller was null, meaning the fallback cannot be null. // Dispose of the fallback. - assert(_horizontalFallbackController != null); + assert(_horizontalFallbackController != null); assert(widget.horizontalDetails.controller != null); _horizontalFallbackController!.dispose(); _horizontalFallbackController = null; @@ -2022,6 +2075,25 @@ class _VerticalOuterDimension extends Scrollable { class _VerticalOuterDimensionState extends ScrollableState { DiagonalDragBehavior get diagonalDragBehavior => (widget as _VerticalOuterDimension).diagonalDragBehavior; + // Implemented in the _HorizontalInnerDimension instead. + @override + _EnsureVisibleResults _performEnsureVisible( + RenderObject object, { + double alignment = 0.0, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, + RenderObject? targetRenderObject, + }) { + assert( + false, + 'The _performEnsureVisible method was called for the vertical scrollable ' + 'of a TwoDimensionalScrollable. This should not happen as the horizontal ' + 'scrollable handles both axes.' + ); + return (>[], this); + } + @override void setCanDrag(bool value) { switch (diagonalDragBehavior) { @@ -2101,6 +2173,39 @@ class _HorizontalInnerDimensionState extends ScrollableState { super.didChangeDependencies(); } + // Returns the Future from calling ensureVisible for the ScrollPosition, as + // as well as the vertical ScrollableState instance so its context can be + // used to check for other ancestor Scrollables in executing ensureVisible. + @override + _EnsureVisibleResults _performEnsureVisible( + RenderObject object, { + double alignment = 0.0, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, + RenderObject? targetRenderObject, + }) { + final List> newFutures = >[]; + + newFutures.add(position.ensureVisible( + object, + alignment: alignment, + duration: duration, + curve: curve, + alignmentPolicy: alignmentPolicy, + )); + + newFutures.add(verticalScrollable.position.ensureVisible( + object, + alignment: alignment, + duration: duration, + curve: curve, + alignmentPolicy: alignmentPolicy, + )); + + return (newFutures, verticalScrollable); + } + void _evaluateLockedAxis(Offset offset) { assert(lastDragOffset != null); final Offset offsetDelta = lastDragOffset! - offset; diff --git a/packages/flutter/lib/src/widgets/scrollable_helpers.dart b/packages/flutter/lib/src/widgets/scrollable_helpers.dart index 20466f96ef84c..1854cfc217513 100644 --- a/packages/flutter/lib/src/widgets/scrollable_helpers.dart +++ b/packages/flutter/lib/src/widgets/scrollable_helpers.dart @@ -28,8 +28,7 @@ export 'package:flutter/physics.dart' show Tolerance; /// information about the Scrollable in order to be initialized. @immutable class ScrollableDetails { - /// Creates a set of details describing the [Scrollable]. The [direction] - /// cannot be null. + /// Creates a set of details describing the [Scrollable]. const ScrollableDetails({ required this.direction, this.controller, @@ -338,8 +337,6 @@ enum ScrollIncrementType { /// for the scrollable. class ScrollIncrementDetails { /// A const constructor for a [ScrollIncrementDetails]. - /// - /// All of the arguments must not be null, and are required. const ScrollIncrementDetails({ required this.type, required this.metrics, @@ -405,8 +402,8 @@ class ScrollAction extends ContextAction { /// /// Must not be called when the position is null, or when any of the position /// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are - /// null. The type and state arguments must not be null, and the widget must - /// have already been laid out so that the position fields are valid. + /// null. The widget must have already been laid out so that the position + /// fields are valid. static double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) { assert(state.position.hasPixels); assert(state.resolvedPhysics == null || state.resolvedPhysics!.shouldAcceptUserOffset(state.position)); diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index bfdc0307fcdf9..6b32305ea0dbb 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -220,7 +220,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// /// The scrollbar track consumes this space. /// - /// Must not be null and defaults to 0. + /// Defaults to zero. double get crossAxisMargin => _crossAxisMargin; double _crossAxisMargin; set crossAxisMargin(double value) { @@ -276,8 +276,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// partial obstructions such as display notches. If you only want additional /// margins around the scrollbar, see [mainAxisMargin]. /// - /// Defaults to [EdgeInsets.zero]. Must not be null and offsets from all four - /// directions must be greater than or equal to zero. + /// Defaults to [EdgeInsets.zero]. Offsets from all four directions must be + /// greater than or equal to zero. EdgeInsets get padding => _padding; EdgeInsets _padding; set padding(EdgeInsets value) { @@ -655,7 +655,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// Convert between a thumb track position and the corresponding scroll /// position. /// - /// thumbOffsetLocal is a position in the thumb track. Cannot be null. + /// The `thumbOffsetLocal` argument is a position in the thumb track. double getTrackToScroll(double thumbOffsetLocal) { final double scrollableExtent = _lastMetrics!.maxScrollExtent - _lastMetrics!.minScrollExtent; final double thumbMovableExtent = _traversableTrackExtent - _thumbExtent; @@ -957,9 +957,6 @@ class RawScrollbar extends StatefulWidget { /// /// The [child], or a descendant of the [child], should be a source of /// [ScrollNotification] notifications, typically a [Scrollable] widget. - /// - /// The [child], [fadeDuration], [pressDuration], and [timeToFade] arguments - /// must not be null. const RawScrollbar({ super.key, required this.child, @@ -1245,18 +1242,18 @@ class RawScrollbar extends StatefulWidget { /// The [Duration] of the fade animation. /// - /// Cannot be null, defaults to a [Duration] of 300 milliseconds. + /// Defaults to a [Duration] of 300 milliseconds. final Duration fadeDuration; /// The [Duration] of time until the fade animation begins. /// - /// Cannot be null, defaults to a [Duration] of 600 milliseconds. + /// Defaults to a [Duration] of 600 milliseconds. final Duration timeToFade; /// The [Duration] of time that a LongPress will trigger the drag gesture of /// the scrollbar thumb. /// - /// Cannot be null, defaults to [Duration.zero]. + /// Defaults to [Duration.zero]. final Duration pressDuration; /// {@template flutter.widgets.Scrollbar.notificationPredicate} @@ -1307,7 +1304,7 @@ class RawScrollbar extends StatefulWidget { /// /// The scrollbar track consumes this space. /// - /// Must not be null and defaults to 0. + /// Defaults to zero. final double crossAxisMargin; /// The insets by which the scrollbar thumb and track should be padded. diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index 1786b00ff7af6..70369b8b3a08e 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -39,6 +39,11 @@ const Set _kLongPressSelectionDevices = { PointerDeviceKind.invertedStylus, }; +// In practice some selectables like widgetspan shift several pixels. So when +// the vertical position diff is within the threshold, compare the horizontal +// position to make the compareScreenOrder function more robust. +const double _kSelectableVerticalComparingThreshold = 3.0; + /// A widget that introduces an area for user selections. /// /// Flutter widgets are not selectable by default. Wrapping a widget subtree @@ -336,7 +341,21 @@ class SelectableRegionState extends State with TextSelectionDe _gestureRecognizers[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { - instance.onTap = _clearSelection; + instance.onTapUp = (TapUpDetails details) { + if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) { + // On iOS when the tap occurs on the previous selection, instead of + // moving the selection, the context menu will be toggled. + final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; + if (toolbarIsVisible) { + hideToolbar(false); + } else { + _showToolbar(location: details.globalPosition); + } + } else { + hideToolbar(); + _collapseSelectionAt(offset: details.globalPosition); + } + }; instance.onSecondaryTapDown = _handleRightClickDown; }, ); @@ -417,15 +436,47 @@ class SelectableRegionState extends State with TextSelectionDe // gestures. + // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, + // which can grow to be infinitely large, to a value between 1 and the supported + // max consecutive tap count. The value that the raw count is converted to is + // based on the default observed behavior on the native platforms. + // + // This method should be used in all instances when details.consecutiveTapCount + // would be used. + static int _getEffectiveConsecutiveTapCount(int rawCount) { + const int maxConsecutiveTap = 2; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + // From observation, these platforms reset their tap count to 0 when + // the number of consecutive taps exceeds the max consecutive tap supported. + // For example on Debian Linux with GTK, when going past a triple click, + // on the fourth click the selection is moved to the precise click + // position, on the fifth click the word at the position is selected, and + // on the sixth click the paragraph at the position is selected. + return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.windows: + // From observation, these platforms either hold their tap count at the max + // consecutive tap supported. For example on macOS, when going past a triple + // click, the selection should be retained at the paragraph that was first + // selected on triple click. + return min(rawCount, maxConsecutiveTap); + } + } + void _initMouseGestureRecognizer() { - _gestureRecognizers[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(debugOwner:this, supportedDevices: { PointerDeviceKind.mouse }), - (PanGestureRecognizer instance) { + _gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => TapAndPanGestureRecognizer(debugOwner:this, supportedDevices: { PointerDeviceKind.mouse }), + (TapAndPanGestureRecognizer instance) { instance - ..onDown = _startNewMouseSelectionGesture - ..onStart = _handleMouseDragStart - ..onUpdate = _handleMouseDragUpdate - ..onEnd = _handleMouseDragEnd + ..onTapDown = _startNewMouseSelectionGesture + ..onTapUp = _handleMouseTapUp + ..onDragStart = _handleMouseDragStart + ..onDragUpdate = _handleMouseDragUpdate + ..onDragEnd = _handleMouseDragEnd ..onCancel = _clearSelection ..dragStartBehavior = DragStartBehavior.down; }, @@ -444,18 +495,67 @@ class SelectableRegionState extends State with TextSelectionDe ); } - void _startNewMouseSelectionGesture(DragDownDetails details) { - widget.focusNode.requestFocus(); - hideToolbar(); - _clearSelection(); + void _startNewMouseSelectionGesture(TapDragDownDetails details) { + switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { + case 1: + widget.focusNode.requestFocus(); + hideToolbar(); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + // On mobile platforms the selection is set on tap up. + break; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + _collapseSelectionAt(offset: details.globalPosition); + } + case 2: + _selectWordAt(offset: details.globalPosition); + } + _updateSelectedContentIfNeeded(); } - void _handleMouseDragStart(DragStartDetails details) { - _selectStartTo(offset: details.globalPosition); + void _handleMouseDragStart(TapDragStartDetails details) { + switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { + case 1: + _selectStartTo(offset: details.globalPosition); + } + _updateSelectedContentIfNeeded(); } - void _handleMouseDragUpdate(DragUpdateDetails details) { - _selectEndTo(offset: details.globalPosition, continuous: true); + void _handleMouseDragUpdate(TapDragUpdateDetails details) { + switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { + case 1: + _selectEndTo(offset: details.globalPosition, continuous: true); + case 2: + _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word); + } + _updateSelectedContentIfNeeded(); + } + + void _handleMouseDragEnd(TapDragEndDetails details) { + _finalizeSelection(); + _updateSelectedContentIfNeeded(); + } + + void _handleMouseTapUp(TapDragUpDetails details) { + switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { + case 1: + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + _collapseSelectionAt(offset: details.globalPosition); + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + // On desktop platforms the selection is set on tap down. + break; + } + } + _updateSelectedContentIfNeeded(); } void _updateSelectedContentIfNeeded() { @@ -465,27 +565,31 @@ class SelectableRegionState extends State with TextSelectionDe } } - void _handleMouseDragEnd(DragEndDetails details) { - _finalizeSelection(); - _updateSelectedContentIfNeeded(); - } - void _handleTouchLongPressStart(LongPressStartDetails details) { HapticFeedback.selectionClick(); widget.focusNode.requestFocus(); _selectWordAt(offset: details.globalPosition); - _showToolbar(); - _showHandles(); + // Platforms besides Android will show the text selection handles when + // the long press is initiated. Android shows the text selection handles when + // the long press has ended, usually after a pointer up event is received. + if (defaultTargetPlatform != TargetPlatform.android) { + _showHandles(); + } _updateSelectedContentIfNeeded(); } void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - _selectEndTo(offset: details.globalPosition); + _selectEndTo(offset: details.globalPosition, textGranularity: TextGranularity.word); + _updateSelectedContentIfNeeded(); } void _handleTouchLongPressEnd(LongPressEndDetails details) { _finalizeSelection(); _updateSelectedContentIfNeeded(); + _showToolbar(); + if (defaultTargetPlatform == TargetPlatform.android) { + _showHandles(); + } } bool _positionIsOnActiveSelection({required Offset globalPosition}) { @@ -512,8 +616,7 @@ class SelectableRegionState extends State with TextSelectionDe // keep the current selection, if not then collapse it. final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); if (!lastSecondaryTapDownPositionWasOnActiveSelection) { - _selectStartTo(offset: lastSecondaryTapDownPosition!); - _selectEndTo(offset: lastSecondaryTapDownPosition!); + _collapseSelectionAt(offset: lastSecondaryTapDownPosition!); } _showHandles(); _showToolbar(location: lastSecondaryTapDownPosition); @@ -538,8 +641,7 @@ class SelectableRegionState extends State with TextSelectionDe // keep the current selection, if not then collapse it. final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); if (!lastSecondaryTapDownPositionWasOnActiveSelection) { - _selectStartTo(offset: lastSecondaryTapDownPosition!); - _selectEndTo(offset: lastSecondaryTapDownPosition!); + _collapseSelectionAt(offset: lastSecondaryTapDownPosition!); } _showHandles(); _showToolbar(location: lastSecondaryTapDownPosition); @@ -558,7 +660,7 @@ class SelectableRegionState extends State with TextSelectionDe /// If the selectable subtree returns a [SelectionResult.pending], this method /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result /// is not pending or users end their gestures. - void _triggerSelectionEndEdgeUpdate() { + void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) { // This method can be called when the drag is not in progress. This can // happen if the child scrollable returns SelectionResult.pending, and // the selection area scheduled a selection update for the next frame, but @@ -567,14 +669,14 @@ class SelectableRegionState extends State with TextSelectionDe return; } if (_selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forEnd(globalPosition: _selectionEndPosition!)) == SelectionResult.pending) { + SelectionEdgeUpdateEvent.forEnd(globalPosition: _selectionEndPosition!, granularity: textGranularity)) == SelectionResult.pending) { _scheduledSelectionEndEdgeUpdate = true; SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { if (!_scheduledSelectionEndEdgeUpdate) { return; } _scheduledSelectionEndEdgeUpdate = false; - _triggerSelectionEndEdgeUpdate(); + _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); }); return; } @@ -612,7 +714,7 @@ class SelectableRegionState extends State with TextSelectionDe /// If the selectable subtree returns a [SelectionResult.pending], this method /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result /// is not pending or users end their gestures. - void _triggerSelectionStartEdgeUpdate() { + void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) { // This method can be called when the drag is not in progress. This can // happen if the child scrollable returns SelectionResult.pending, and // the selection area scheduled a selection update for the next frame, but @@ -621,14 +723,14 @@ class SelectableRegionState extends State with TextSelectionDe return; } if (_selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forStart(globalPosition: _selectionStartPosition!)) == SelectionResult.pending) { + SelectionEdgeUpdateEvent.forStart(globalPosition: _selectionStartPosition!, granularity: textGranularity)) == SelectionResult.pending) { _scheduledSelectionStartEdgeUpdate = true; SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { if (!_scheduledSelectionStartEdgeUpdate) { return; } _scheduledSelectionStartEdgeUpdate = false; - _triggerSelectionStartEdgeUpdate(); + _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); }); return; } @@ -655,6 +757,7 @@ class SelectableRegionState extends State with TextSelectionDe details.globalPosition, _selectionDelegate.value.startSelectionPoint!, )); + _updateSelectedContentIfNeeded(); } void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { @@ -668,6 +771,7 @@ class SelectableRegionState extends State with TextSelectionDe details.globalPosition, _selectionDelegate.value.startSelectionPoint!, )); + _updateSelectedContentIfNeeded(); } void _handleSelectionEndHandleDragStart(DragStartDetails details) { @@ -680,6 +784,7 @@ class SelectableRegionState extends State with TextSelectionDe details.globalPosition, _selectionDelegate.value.endSelectionPoint!, )); + _updateSelectedContentIfNeeded(); } void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { @@ -693,6 +798,7 @@ class SelectableRegionState extends State with TextSelectionDe details.globalPosition, _selectionDelegate.value.endSelectionPoint!, )); + _updateSelectedContentIfNeeded(); } MagnifierInfo _buildInfoForMagnifier(Offset globalGesturePosition, SelectionPoint selectionPoint) { @@ -840,20 +946,25 @@ class SelectableRegionState extends State with TextSelectionDe /// /// The `offset` is in global coordinates. /// + /// Provide the `textGranularity` if the selection should not move by the default + /// [TextGranularity.character]. Only [TextGranularity.character] and + /// [TextGranularity.word] are currently supported. + /// /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clear the ongoing selection. + /// * [_clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. + /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [selectAll], which selects the entire content. - void _selectEndTo({required Offset offset, bool continuous = false}) { + void _selectEndTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { if (!continuous) { - _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: offset)); + _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: offset, granularity: textGranularity)); return; } if (_selectionEndPosition != offset) { _selectionEndPosition = offset; - _triggerSelectionEndEdgeUpdate(); + _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); } } @@ -875,23 +986,42 @@ class SelectableRegionState extends State with TextSelectionDe /// /// The `offset` is in global coordinates. /// + /// Provide the `textGranularity` if the selection should not move by the default + /// [TextGranularity.character]. Only [TextGranularity.character] and + /// [TextGranularity.word] are currently supported. + /// /// See also: /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clear the ongoing selection. + /// * [_clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. + /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [selectAll], which selects the entire content. - void _selectStartTo({required Offset offset, bool continuous = false}) { + void _selectStartTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { if (!continuous) { - _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: offset)); + _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: offset, granularity: textGranularity)); return; } if (_selectionStartPosition != offset) { _selectionStartPosition = offset; - _triggerSelectionStartEdgeUpdate(); + _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); } } + /// Collapses the selection at the given `offset` location. + /// + /// See also: + /// * [_selectStartTo], which sets or updates selection start edge. + /// * [_selectEndTo], which sets or updates selection end edge. + /// * [_finalizeSelection], which stops the `continuous` updates. + /// * [_clearSelection], which clears the ongoing selection. + /// * [_selectWordAt], which selects a whole word at the location. + /// * [selectAll], which selects the entire content. + void _collapseSelectionAt({required Offset offset}) { + _selectStartTo(offset: offset); + _selectEndTo(offset: offset); + } + /// Selects a whole word at the `offset` location. /// /// If the whole word is already in the current selection, selection won't @@ -905,7 +1035,8 @@ class SelectableRegionState extends State with TextSelectionDe /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clear the ongoing selection. + /// * [_clearSelection], which clears the ongoing selection. + /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [selectAll], which selects the entire content. void _selectWordAt({required Offset offset}) { // There may be other selection ongoing. @@ -997,6 +1128,7 @@ class SelectableRegionState extends State with TextSelectionDe granularity: granularity, ), ); + _updateSelectedContentIfNeeded(); } double? _directionalHorizontalBaseline; @@ -1018,6 +1150,7 @@ class SelectableRegionState extends State with TextSelectionDe dx: globalSelectionPointOffset.dx, ), ); + _updateSelectedContentIfNeeded(); } // [TextSelectionDelegate] overrides. @@ -1497,6 +1630,13 @@ class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContain /// This class optimize the selection update by keeping track of the /// [Selectable]s that currently contain the selection edges. abstract class MultiSelectableSelectionContainerDelegate extends SelectionContainerDelegate with ChangeNotifier { + /// Creates an instance of [MultiSelectableSelectionContainerDelegate]. + MultiSelectableSelectionContainerDelegate() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + /// Gets the list of selectables this delegate is managing. List selectables = []; @@ -1703,11 +1843,11 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai /// Returns positive if a is lower, negative if a is higher, 0 if their /// order can't be determine solely by their vertical position. static int _compareVertically(Rect a, Rect b) { - if ((a.top - b.top < precisionErrorTolerance && a.bottom - b.bottom > - precisionErrorTolerance) || - (b.top - a.top < precisionErrorTolerance && b.bottom - a.bottom > - precisionErrorTolerance)) { + if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) || + (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) { return 0; } - if ((a.top - b.top).abs() > precisionErrorTolerance) { + if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) { return a.top > b.top ? 1 : -1; } return a.bottom > b.bottom ? 1 : -1; @@ -1786,7 +1926,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai SelectionPoint? startPoint; if (startGeometry.startSelectionPoint != null) { - final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]); + final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]); final Offset start = MatrixUtils.transformPoint(startTransform, startGeometry.startSelectionPoint!.localPosition); // It can be NaN if it is detached or off-screen. if (start.isFinite) { @@ -1807,7 +1947,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai } SelectionPoint? endPoint; if (endGeometry.endSelectionPoint != null) { - final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]); + final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]); final Offset end = MatrixUtils.transformPoint(endTransform, endGeometry.endSelectionPoint!.localPosition); // It can be NaN if it is detached or off-screen. if (end.isFinite) { @@ -1891,8 +2031,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai final Rect? drawableArea = hasSize ? Rect .fromLTWH(0, 0, containerSize.width, containerSize.height) .inflate(_kSelectionHandleDrawableAreaPadding) : null; - final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || !drawableArea.contains(value.startSelectionPoint!.localPosition); - final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null|| !drawableArea.contains(value.endSelectionPoint!.localPosition); + final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || !drawableArea.contains(value.startSelectionPoint!.localPosition); + final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null|| !drawableArea.contains(value.endSelectionPoint!.localPosition); effectiveStartHandle = hideStartHandle ? null : _startHandleLayer; effectiveEndHandle = hideEndHandle ? null : _endHandleLayer; } @@ -1952,6 +2092,34 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ); } + // Clears the selection on all selectables not in the range of + // currentSelectionStartIndex..currentSelectionEndIndex. + // + // If one of the edges does not exist, then this method will clear the selection + // in all selectables except the existing edge. + // + // If neither of the edges exist this method immediately returns. + void _flushInactiveSelections() { + if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) { + return; + } + if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { + final int skipIndex = currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex; + selectables + .where((Selectable target) => target != selectables[skipIndex]) + .forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent())); + return; + } + final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex); + final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex); + for (int index = 0; index < selectables.length; index += 1) { + if (index >= skipStart && index <= skipEnd) { + continue; + } + dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent()); + } + } + /// Selects all contents of all selectables. @protected SelectionResult handleSelectAll(SelectAllSelectionEvent event) { @@ -1975,15 +2143,15 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai if (globalRect.contains(event.globalPosition)) { final SelectionGeometry existingGeometry = selectables[index].value; lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event); + if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) { + return SelectionResult.next; + } if (lastSelectionResult == SelectionResult.next) { continue; } if (index == 0 && lastSelectionResult == SelectionResult.previous) { return SelectionResult.previous; } - if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) { - return SelectionResult.next; - } if (selectables[index].value != existingGeometry) { // Geometry has changed as a result of select word, need to clear the // selection of other selectables to keep selection in sync. @@ -2195,7 +2363,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai bool hasFoundEdgeIndex = false; SelectionResult? result; for (int index = 0; index < selectables.length && !hasFoundEdgeIndex; index += 1) { - final Selectable child = selectables[index]; + final Selectable child = selectables[index]; final SelectionResult childResult = dispatchSelectionEventToChild(child, event); switch (childResult) { case SelectionResult.next: @@ -2228,6 +2396,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai } else { currentSelectionStartIndex = newIndex; } + _flushInactiveSelections(); // The result can only be null if the loop went through the entire list // without any of the selection returned end or previous. In this case, the // caller of this method needs to find the next selectable in their list. @@ -2250,13 +2419,39 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai return true; }()); SelectionResult? finalResult; - int newIndex = isEnd ? currentSelectionEndIndex : currentSelectionStartIndex; + // Determines if the edge being adjusted is within the current viewport. + // - If so, we begin the search for the new selection edge position at the + // currentSelectionEndIndex/currentSelectionStartIndex. + // - If not, we attempt to locate the new selection edge starting from + // the opposite end. + // - If neither edge is in the current viewport, the search for the new + // selection edge position begins at 0. + // + // This can happen when there is a scrollable child and the edge being adjusted + // has been scrolled out of view. + final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null; + final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null; + int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) { + (true, true, true) => currentSelectionEndIndex, + (true, true, false) => currentSelectionEndIndex, + (true, false, true) => currentSelectionStartIndex, + (true, false, false) => 0, + (false, true, true) => currentSelectionStartIndex, + (false, true, false) => currentSelectionStartIndex, + (false, false, true) => currentSelectionEndIndex, + (false, false, false) => 0, + }; bool? forward; late SelectionResult currentSelectableResult; - // This loop sends the selection event to the - // currentSelectionEndIndex/currentSelectionStartIndex to determine the - // direction of the search. If the result is `SelectionResult.next`, this - // loop look backward. Otherwise, it looks forward. + // This loop sends the selection event to one of the following to determine + // the direction of the search. + // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge + // is in the current viewport. + // - The opposite edge index if the current edge is not in the current viewport. + // - Index 0 if neither edge is in the current viewport. + // + // If the result is `SelectionResult.next`, this loop look backward. + // Otherwise, it looks forward. // // The terminate condition are: // 1. the selectable returns end, pending, none. @@ -2296,6 +2491,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai } else { currentSelectionStartIndex = newIndex; } + _flushInactiveSelections(); return finalResult!; } } diff --git a/packages/flutter/lib/src/widgets/selection_container.dart b/packages/flutter/lib/src/widgets/selection_container.dart index 3c16d7909b9df..b0e6c743b9999 100644 --- a/packages/flutter/lib/src/widgets/selection_container.dart +++ b/packages/flutter/lib/src/widgets/selection_container.dart @@ -41,8 +41,6 @@ class SelectionContainer extends StatefulWidget { /// /// If [registrar] is not provided, this selection container gets the /// [SelectionRegistrar] from the context instead. - /// - /// The [delegate] and [child] must not be null. const SelectionContainer({ super.key, this.registrar, @@ -59,8 +57,6 @@ class SelectionContainer extends StatefulWidget { /// /// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** /// {@end-tool} - /// - /// The [child] must not be null. const SelectionContainer.disabled({ super.key, required this.child, diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index c51ecf9a1ac4a..892140fdd572f 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -21,8 +21,6 @@ import 'view.dart'; class SemanticsDebugger extends StatefulWidget { /// Creates a widget that visualizes the semantics for the child. /// - /// The [child] argument must not be null. - /// /// [labelStyle] dictates the [TextStyle] used for the semantics labels. const SemanticsDebugger({ super.key, @@ -47,25 +45,31 @@ class SemanticsDebugger extends StatefulWidget { } class _SemanticsDebuggerState extends State with WidgetsBindingObserver { - late _SemanticsClient _client; + _SemanticsClient? _client; + PipelineOwner? _pipelineOwner; @override void initState() { super.initState(); - // TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance - // static here because we might not be in a tree that's attached to that - // binding. Instead, we should find a way to get to the PipelineOwner from - // the BuildContext. - _client = _SemanticsClient(WidgetsBinding.instance.pipelineOwner) - ..addListener(_update); WidgetsBinding.instance.addObserver(this); } + @override + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final PipelineOwner newOwner = View.pipelineOwnerOf(context); + if (newOwner != _pipelineOwner) { + _client?.dispose(); + _client = _SemanticsClient(newOwner) + ..addListener(_update); + _pipelineOwner = newOwner; + } + } + @override void dispose() { - _client - ..removeListener(_update) - ..dispose(); + _client?.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -145,19 +149,15 @@ class _SemanticsDebuggerState extends State with WidgetsBindi } void _performAction(Offset position, SemanticsAction action) { - _pipelineOwner.semanticsOwner?.performActionAt(position, action); + _pipelineOwner?.semanticsOwner?.performActionAt(position, action); } - // TODO(abarth): This shouldn't be a static. We should get the pipeline owner - // from [context] somehow. - PipelineOwner get _pipelineOwner => WidgetsBinding.instance.pipelineOwner; - @override Widget build(BuildContext context) { return CustomPaint( foregroundPainter: _SemanticsDebuggerPainter( - _pipelineOwner, - _client.generation, + _pipelineOwner!, + _client!.generation, _lastPointerDownLocation, // in physical pixels View.of(context).devicePixelRatio, widget.labelStyle, diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart index 5bc3879de8dd2..293d28e979199 100644 --- a/packages/flutter/lib/src/widgets/shortcuts.dart +++ b/packages/flutter/lib/src/widgets/shortcuts.dart @@ -747,7 +747,11 @@ class ShortcutManager with Diagnosticable, ChangeNotifier { ShortcutManager({ Map shortcuts = const {}, this.modal = false, - }) : _shortcuts = shortcuts; + }) : _shortcuts = shortcuts { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } /// True if the [ShortcutManager] should not pass on keys that it doesn't /// handle to any key-handling widgets that are ancestors to this one. @@ -1196,6 +1200,13 @@ class ShortcutRegistryEntry { /// widgets that are not descendants of the registry can listen to it (e.g. in /// overlays). class ShortcutRegistry with ChangeNotifier { + /// Creates an instance of [ShortcutRegistry]. + ShortcutRegistry() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + bool _notificationScheduled = false; bool _disposed = false; @@ -1433,6 +1444,7 @@ class _ShortcutRegistrarState extends State { void dispose() { registry.removeListener(_shortcutsChanged); registry.dispose(); + manager.dispose(); super.dispose(); } diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index 818a8a1ad4487..59b4956f5f7dc 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -130,6 +130,8 @@ import 'scrollable.dart'; /// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart ** /// {@end-tool} /// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// /// See also: /// /// * [ListView], which handles multiple children in a scrolling list. @@ -246,6 +248,7 @@ class SingleChildScrollView extends StatelessWidget { controller: scrollController, physics: physics, restorationId: restorationId, + clipBehavior: clipBehavior, viewportBuilder: (BuildContext context, ViewportOffset offset) { return _SingleChildViewport( axisDirection: axisDirection, @@ -359,7 +362,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix /// {@macro flutter.material.Material.clipBehavior} /// - /// Defaults to [Clip.none], and must not be null. + /// Defaults to [Clip.none]. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.none; set clipBehavior(Clip value) { @@ -589,7 +592,16 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix } @override - RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { + RevealedOffset getOffsetToReveal( + RenderObject target, + double alignment, { + Rect? rect, + Axis? axis, + }) { + // One dimensional viewport has only one axis, override if it was + // provided/may be mismatched. + axis = this.axis; + rect ??= target.paintBounds; if (target is! RenderBox) { return RevealedOffset(offset: offset.pixels, rect: rect); diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 0090198cb3893..9e76d448753a3 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -177,8 +177,8 @@ class SliverList extends SliverMultiBoxAdaptorWidget { /// [SliverChildBuilderDelegate.addSemanticIndexes] property. /// /// {@tool snippet} - /// This example, which would be inserted into a [CustomScrollView.slivers] - /// list, shows an infinite number of items in varying shades of blue: + /// This example, which would be provided in [CustomScrollView.slivers], + /// shows an infinite number of items in varying shades of blue: /// /// ```dart /// SliverList.builder( @@ -236,10 +236,11 @@ class SliverList extends SliverMultiBoxAdaptorWidget { /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The /// `addSemanticIndexes` argument corresponds to the /// [SliverChildBuilderDelegate.addSemanticIndexes] property. - /// {@tool snippet} /// + /// {@tool snippet} /// This example shows how to create a [SliverList] whose [Container] items - /// are separated by [Divider]s. + /// are separated by [Divider]s. The [SliverList] would be provided in + /// [CustomScrollView.slivers]. /// /// ```dart /// SliverList.separated( @@ -303,8 +304,8 @@ class SliverList extends SliverMultiBoxAdaptorWidget { /// [SliverChildBuilderDelegate.addSemanticIndexes] property. /// /// {@tool snippet} - /// This example, which would be inserted into a [CustomScrollView.slivers] - /// list, shows an infinite number of items in varying shades of blue: + /// This example, which would be provided in [CustomScrollView.slivers], + /// shows a list containing two [Text] widgets: /// /// ```dart /// SliverList.list( @@ -1112,8 +1113,7 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render class SliverOpacity extends SingleChildRenderObjectWidget { /// Creates a sliver that makes its sliver child partially transparent. /// - /// The [opacity] argument must not be null and must be between 0.0 and 1.0 - /// (inclusive). + /// The [opacity] argument must be between zero and one, inclusive. const SliverOpacity({ super.key, required this.opacity, @@ -1127,8 +1127,6 @@ class SliverOpacity extends SingleChildRenderObjectWidget { /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent /// (i.e. invisible). /// - /// The opacity must not be null. - /// /// Values 1.0 and 0.0 are painted with a fast path. Other values /// require painting the sliver child into an intermediate buffer, which is /// expensive. @@ -1178,15 +1176,20 @@ class SliverOpacity extends SingleChildRenderObjectWidget { /// child as usual. It just cannot be the target of located events, because it /// returns false from [RenderSliver.hitTest]. /// -/// {@macro flutter.widgets.IgnorePointer.Semantics} +/// ## Semantics +/// +/// Using this class may also affect how the semantics subtree underneath is +/// collected. +/// +/// {@macro flutter.widgets.IgnorePointer.semantics} +/// +/// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} /// /// See also: /// /// * [IgnorePointer], the equivalent widget for boxes. class SliverIgnorePointer extends SingleChildRenderObjectWidget { /// Creates a sliver widget that is invisible to hit testing. - /// - /// The [ignoring] argument must not be null. const SliverIgnorePointer({ super.key, this.ignoring = true, @@ -1203,13 +1206,13 @@ class SliverIgnorePointer extends SingleChildRenderObjectWidget { /// Regardless of whether this sliver is ignored during hit testing, it will /// still consume space during layout and be visible during painting. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.semantics} final bool ignoring; /// Whether the semantics of this sliver is ignored when compiling the /// semantics tree. /// - /// {@macro flutter.widgets.IgnorePointer.Semantics} + /// {@macro flutter.widgets.IgnorePointer.ignoringSemantics} @Deprecated( 'Create a custom sliver ignore pointer widget instead. ' 'This feature was deprecated after v3.8.0-12.0.pre.' @@ -1303,8 +1306,8 @@ class _SliverOffstageElement extends SingleChildRenderObjectElement { /// Mark a child as needing to stay alive even when it's in a lazy list that /// would otherwise remove it. /// -/// This widget is for use in [SliverWithKeepAliveWidget]s, such as -/// [SliverGrid] or [SliverList]. +/// This widget is for use in a [RenderAbstractViewport]s, such as +/// [Viewport] or [TwoDimensionalViewport]. /// /// This widget is rarely used directly. The [SliverChildBuilderDelegate] and /// [SliverChildListDelegate] delegates, used with [SliverList] and @@ -1314,6 +1317,9 @@ class _SliverOffstageElement extends SingleChildRenderObjectElement { /// each child, causing [KeepAlive] widgets to be automatically added and /// configured in response to [KeepAliveNotification]s. /// +/// The same `addAutomaticKeepAlives` feature is supported by the +/// [TwoDimensionalChildBuilderDelegate] and [TwoDimensionalChildListDelegate]. +/// /// Therefore, to keep a widget alive, it is more common to use those /// notifications than to directly deal with [KeepAlive] widgets. /// @@ -1322,8 +1328,6 @@ class _SliverOffstageElement extends SingleChildRenderObjectElement { /// for that mixin class for details. class KeepAlive extends ParentDataWidget { /// Marks a child as needing to remain alive. - /// - /// The [child] and [keepAlive] arguments must not be null. const KeepAlive({ super.key, required this.keepAlive, @@ -1357,7 +1361,10 @@ class KeepAlive extends ParentDataWidget { bool debugCanApplyOutOfTurn() => keepAlive; @override - Type get debugTypicalAncestorWidgetClass => SliverWithKeepAliveWidget; + Type get debugTypicalAncestorWidgetClass => throw FlutterError('Multiple Types are supported, use debugTypicalAncestorWidgetDescription.'); + + @override + String get debugTypicalAncestorWidgetDescription => 'SliverWithKeepAliveWidget or TwoDimensionalViewport'; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/packages/flutter/lib/src/widgets/sliver_fill.dart b/packages/flutter/lib/src/widgets/sliver_fill.dart index 743453620fd85..f39a5f1b1a16f 100644 --- a/packages/flutter/lib/src/widgets/sliver_fill.dart +++ b/packages/flutter/lib/src/widgets/sliver_fill.dart @@ -45,14 +45,14 @@ class SliverFillViewport extends StatelessWidget { /// Whether to add padding to both ends of the list. /// /// If this is set to true and [viewportFraction] < 1.0, padding will be added - /// such that the first and last child slivers will be in the center of - /// the viewport when scrolled all the way to the start or end, respectively. - /// You may want to set this to false if this [SliverFillViewport] is not the only + /// such that the first and last child slivers will be in the center of the + /// viewport when scrolled all the way to the start or end, respectively. You + /// may want to set this to false if this [SliverFillViewport] is not the only /// widget along this main axis, such as in a [CustomScrollView] with multiple /// children. /// - /// This option cannot be null. If [viewportFraction] >= 1.0, this option has no - /// effect. Defaults to true. + /// If [viewportFraction] is greater than one, this option has no effect. + /// Defaults to true. final bool padEnds; /// {@macro flutter.widgets.SliverMultiBoxAdaptorWidget.delegate} @@ -282,10 +282,9 @@ class SliverFillRemaining extends StatelessWidget { /// Indicates whether the child should stretch to fill the overscroll area /// created by certain scroll physics, such as iOS' default scroll physics. - /// This value cannot be null. This flag is only relevant when the - /// [hasScrollBody] value is false. + /// This flag is only relevant when [hasScrollBody] is false. /// - /// Defaults to false, meaning the default behavior is for the child to + /// Defaults to false, meaning that the default behavior is for the child to /// maintain its size and not extend into the overscroll area. final bool fillOverscroll; diff --git a/packages/flutter/lib/src/widgets/sliver_layout_builder.dart b/packages/flutter/lib/src/widgets/sliver_layout_builder.dart index e4f0308cd6a21..43af22187511e 100644 --- a/packages/flutter/lib/src/widgets/sliver_layout_builder.dart +++ b/packages/flutter/lib/src/widgets/sliver_layout_builder.dart @@ -25,8 +25,6 @@ typedef SliverLayoutWidgetBuilder = Widget Function(BuildContext context, Sliver /// * [LayoutBuilder], the non-sliver version of this widget. class SliverLayoutBuilder extends ConstrainedLayoutBuilder { /// Creates a sliver widget that defers its building until layout. - /// - /// The [builder] argument must not be null. const SliverLayoutBuilder({ super.key, required super.builder, diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart index 23e0e2911a3f4..359badf4da7ec 100644 --- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart @@ -119,8 +119,6 @@ abstract class SliverPersistentHeaderDelegate { class SliverPersistentHeader extends StatelessWidget { /// Creates a sliver that varies its size when it is scrolled to the start of /// a viewport. - /// - /// The [delegate], [pinned], and [floating] arguments must not be null. const SliverPersistentHeader({ super.key, required this.delegate, diff --git a/packages/flutter/lib/src/widgets/sliver_varied_extent_list.dart b/packages/flutter/lib/src/widgets/sliver_varied_extent_list.dart new file mode 100644 index 0000000000000..b727726b36aa9 --- /dev/null +++ b/packages/flutter/lib/src/widgets/sliver_varied_extent_list.dart @@ -0,0 +1,135 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; +import 'scroll_delegate.dart'; +import 'sliver.dart'; + +/// A sliver that places its box children in a linear array and constrains them +/// to have the corresponding extent returned by [itemExtentBuilder]. +/// +/// _To learn more about slivers, see [CustomScrollView.slivers]._ +/// +/// [SliverVariedExtentList] arranges its children in a line along +/// the main axis starting at offset zero and without gaps. Each child is +/// constrained to the corresponding extent along the main axis +/// and the [SliverConstraints.crossAxisExtent] along the cross axis. +/// +/// [SliverVariedExtentList] is more efficient than [SliverList] because +/// [SliverVariedExtentList] does not need to lay out its children to obtain +/// their extent along the main axis. It's a little more flexible than +/// [SliverFixedExtentList] because this allow the children to have different extents. +/// +/// See also: +/// +/// * [SliverFixedExtentList], whose children are forced to a given pixel +/// extent. +/// * [SliverList], which does not require its children to have the same +/// extent in the main axis. +/// * [SliverFillViewport], which sizes its children based on the +/// size of the viewport, regardless of what else is in the scroll view. +class SliverVariedExtentList extends SliverMultiBoxAdaptorWidget { + /// Creates a sliver that places box children with the same main axis extent + /// in a linear array. + const SliverVariedExtentList({ + super.key, + required super.delegate, + required this.itemExtentBuilder, + }); + + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// [SliverVariedExtentList] places its children in a linear array along the main + /// axis starting at offset zero and without gaps. Each child is forced to have + /// the returned extent of [itemExtentBuilder] in the main axis and the + /// [SliverConstraints.crossAxisExtent] in the cross axis. + /// + /// This constructor is appropriate for sliver lists with a large (or + /// infinite) number of children whose extent is already determined. + /// + /// Providing a non-null `itemCount` improves the ability of the [SliverGrid] + /// to estimate the maximum scroll extent. + SliverVariedExtentList.builder({ + super.key, + required NullableIndexedWidgetBuilder itemBuilder, + required this.itemExtentBuilder, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// [SliverVariedExtentList] places its children in a linear array along the main + /// axis starting at offset zero and without gaps. Each child is forced to have + /// the returned extent of [itemExtentBuilder] in the main axis and the + /// [SliverConstraints.crossAxisExtent] in the cross axis. + /// + /// This constructor uses a list of [Widget]s to build the sliver. + SliverVariedExtentList.list({ + super.key, + required List children, + required this.itemExtentBuilder, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + + /// The children extent builder. + final ItemExtentBuilder itemExtentBuilder; + + @override + RenderSliverVariedExtentList createRenderObject(BuildContext context) { + final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; + return RenderSliverVariedExtentList(childManager: element, itemExtentBuilder: itemExtentBuilder); + } + + @override + void updateRenderObject(BuildContext context, RenderSliverVariedExtentList renderObject) { + renderObject.itemExtentBuilder = itemExtentBuilder; + } +} + +/// A sliver that places multiple box children with the corresponding main axis extent in +/// a linear array. +class RenderSliverVariedExtentList extends RenderSliverFixedExtentBoxAdaptor { + /// Creates a sliver that contains multiple box children that have a explicit + /// extent in the main axis. + RenderSliverVariedExtentList({ + required super.childManager, + required ItemExtentBuilder itemExtentBuilder, + }) : _itemExtentBuilder = itemExtentBuilder; + + @override + ItemExtentBuilder get itemExtentBuilder => _itemExtentBuilder; + ItemExtentBuilder _itemExtentBuilder; + set itemExtentBuilder(ItemExtentBuilder value) { + if (_itemExtentBuilder == value) { + return; + } + _itemExtentBuilder = value; + markNeedsLayout(); + } + + @override + double? get itemExtent => null; +} diff --git a/packages/flutter/lib/src/widgets/status_transitions.dart b/packages/flutter/lib/src/widgets/status_transitions.dart index b79bd0df437b7..734cd0f29b0c0 100644 --- a/packages/flutter/lib/src/widgets/status_transitions.dart +++ b/packages/flutter/lib/src/widgets/status_transitions.dart @@ -8,8 +8,6 @@ import 'framework.dart'; /// A widget that rebuilds when the given animation changes status. abstract class StatusTransitionWidget extends StatefulWidget { /// Initializes fields for subclasses. - /// - /// The [animation] argument must not be null. const StatusTransitionWidget({ super.key, required this.animation, diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index a3aba1c95fe25..316a118efba66 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -112,9 +112,6 @@ class _TableElementRow { /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Table extends RenderObjectWidget { /// Creates a table. - /// - /// The [children], [defaultColumnWidth], and [defaultVerticalAlignment] - /// arguments must not be null. Table({ super.key, this.children = const [], @@ -169,8 +166,7 @@ class Table extends RenderObjectWidget { /// The rows of the table. /// - /// Every row in a table must have the same number of children, and all the - /// children must be non-null. + /// Every row in a table must have the same number of children. final List children; /// How the horizontal extents of the columns of this table should be determined. diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index 45111223b0639..a6054c60acad0 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -40,11 +40,6 @@ class DefaultTextStyle extends InheritedTheme { /// Consider using [DefaultTextStyle.merge] to inherit styling information /// from the current default text style for a given [BuildContext]. /// - /// The [style] and [child] arguments are required and must not be null. - /// - /// The [softWrap] and [overflow] arguments must not be null (though they do - /// have default values). - /// /// The [maxLines] property may be null (and indeed defaults to null), but if /// it is not null, it must be greater than zero. const DefaultTextStyle({ @@ -235,8 +230,6 @@ class _NullWidget extends StatelessWidget { /// [Text] widgets. class DefaultTextHeightBehavior extends InheritedTheme { /// Creates a default text height behavior for the given subtree. - /// - /// The [textHeightBehavior] and [child] arguments are required and must not be null. const DefaultTextHeightBehavior({ super.key, required this.textHeightBehavior, @@ -419,8 +412,6 @@ class Text extends StatelessWidget { /// If the [style] argument is null, the text will use the style from the /// closest enclosing [DefaultTextStyle]. /// - /// The [data] parameter must not be null. - /// /// The [overflow] property's behavior is affected by the [softWrap] argument. /// If the [softWrap] is true or null, the glyph causing overflow, and those /// that follow, will not be rendered. Otherwise, it will be shown with the @@ -435,13 +426,23 @@ class Text extends StatelessWidget { this.locale, this.softWrap, this.overflow, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) this.textScaleFactor, + this.textScaler, this.maxLines, this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, this.selectionColor, - }) : textSpan = null; + }) : textSpan = null, + assert( + textScaler == null || textScaleFactor == null, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ); /// Creates a text widget with a [InlineSpan]. /// @@ -450,8 +451,6 @@ class Text extends StatelessWidget { /// * [TextSpan]s define text and children [InlineSpan]s. /// * [WidgetSpan]s define embedded inline widgets. /// - /// The [textSpan] parameter must not be null. - /// /// See [RichText] which provides a lower-level way to draw text. const Text.rich( InlineSpan this.textSpan, { @@ -463,13 +462,23 @@ class Text extends StatelessWidget { this.locale, this.softWrap, this.overflow, + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) this.textScaleFactor, + this.textScaler, this.maxLines, this.semanticsLabel, this.textWidthBasis, this.textHeightBehavior, this.selectionColor, - }) : data = null; + }) : data = null, + assert( + textScaler == null || textScaleFactor == null, + 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', + ); /// The text to display. /// @@ -529,6 +538,9 @@ class Text extends StatelessWidget { /// from the nearest [DefaultTextStyle] ancestor will be used. final TextOverflow? overflow; + /// Deprecated. Will be removed in a future version of Flutter. Use + /// [textScaler] instead. + /// /// The number of font pixels for each logical pixel. /// /// For example, if the text scale factor is 1.5, text will be 50% larger than @@ -537,8 +549,16 @@ class Text extends StatelessWidget { /// The value given to the constructor as textScaleFactor. If null, will /// use the [MediaQueryData.textScaleFactor] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. + @Deprecated( + 'Use textScaler instead. ' + 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' + 'This feature was deprecated after v3.12.0-2.0.pre.', + ) final double? textScaleFactor; + /// {@macro flutter.painting.textPainter.textScaler} + final TextScaler? textScaler; + /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. @@ -595,13 +615,20 @@ class Text extends StatelessWidget { effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold)); } final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); + final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) { + (final TextScaler textScaler, _) => textScaler, + // For unmigrated apps, fall back to textScaleFactor. + (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), + (null, null) => MediaQuery.textScalerOf(context), + }; + Widget result = RichText( textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null. locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null softWrap: softWrap ?? defaultTextStyle.softWrap, overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow, - textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context), + textScaler: textScaler, maxLines: maxLines ?? defaultTextStyle.maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 34e3279551925..c0d699aae8f15 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -24,7 +24,6 @@ import 'gesture_detector.dart'; import 'magnifier.dart'; import 'overlay.dart'; import 'scrollable.dart'; -import 'tap_and_drag_gestures.dart'; import 'tap_region.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -62,23 +61,28 @@ class ToolbarItemsParentData extends ContainerBoxParentData { /// An interface for building the selection UI, to be provided by the /// implementer of the toolbar widget. /// -/// Override text operations such as [handleCut] if needed. +/// Parts of this class, including [buildToolbar], have been deprecated in favor +/// of [EditableText.contextMenuBuilder], which is now the preferred way to +/// customize the context menus. /// /// ## Use with [EditableText.contextMenuBuilder] -/// [buildToolbar] has been deprecated in favor of -/// [EditableText.contextMenuBuilder], and that is the preferred way to -/// customize the context menus now. However, both ways will continue to work -/// during the deprecation period. /// -/// To use both [EditableText.contextMenuBuilder] and [buildHandle], a two-step -/// migration is necessary. First, migrate to [TextSelectionHandleControls], -/// using its [TextSelectionHandleControls.buildHandle] method and moving -/// toolbar code to [EditableText.contextMenuBuilder]. Later, the deprecation -/// period will expire, [buildToolbar] will be removed, and -/// [TextSelectionHandleControls] will be deprecated. Migrate back to -/// [TextSelectionControls.buildHandle], so that the final state is to use -/// [EditableText.contextMenuBuilder] for the toolbar and -/// [TextSelectionControls] for the handles. +/// For backwards compatibility during the deprecation period, when +/// [EditableText.selectionControls] is set to an object that does not mix in +/// [TextSelectionHandleControls], [EditableText.contextMenuBuilder] is ignored +/// in favor of the deprecated [buildToolbar]. +/// +/// To migrate code from [buildToolbar] to the preferred +/// [EditableText.contextMenuBuilder], while still using [buildHandle], mix in +/// [TextSelectionHandleControls] into the [TextSelectionControls] subclass when +/// moving any toolbar code to a callback passed to +/// [EditableText.contextMenuBuilder]. +/// +/// In due course, [buildToolbar] will be removed, and the mixin will no longer +/// be necessary as a way to flag to the framework that the code has been +/// migrated and does not expect [buildToolbar] to be called. +/// +/// For more information, see . /// /// See also: /// @@ -309,7 +313,7 @@ final TextSelectionControls emptyTextSelectionControls = EmptyTextSelectionContr class TextSelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// - /// The [context] must not be null and must have an [Overlay] as an ancestor. + /// The [context] must have an [Overlay] as an ancestor. TextSelectionOverlay({ required TextEditingValue value, required this.context, @@ -370,13 +374,6 @@ class TextSelectionOverlay { /// {@endtemplate} final BuildContext context; - /// Controls the fade-in and fade-out animations for the toolbar and handles. - @Deprecated( - 'Use `SelectionOverlay.fadeDuration` instead. ' - 'This feature was deprecated after v2.12.0-4.1.pre.' - ) - static const Duration fadeDuration = SelectionOverlay.fadeDuration; - // TODO(mpcomplete): what if the renderObject is removed or replaced, or // moves? Not sure what cases I need to handle, or how to handle them. /// The editable line in which the selected text is being displayed. @@ -917,7 +914,7 @@ class TextSelectionOverlay { class SelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// - /// The [context] must not be null and must have an [Overlay] as an ancestor. + /// The [context] must have an [Overlay] as an ancestor. SelectionOverlay({ required this.context, this.debugRequiredFor, @@ -1367,7 +1364,9 @@ class SelectionOverlay { void hideHandles() { if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } } @@ -1476,7 +1475,9 @@ class SelectionOverlay { _magnifierController.hide(); if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } if (_toolbar != null || _contextMenuController.isShown || _spellCheckToolbarController.isShown) { @@ -1496,6 +1497,7 @@ class SelectionOverlay { return; } _toolbar?.remove(); + _toolbar?.dispose(); _toolbar = null; } @@ -1504,6 +1506,7 @@ class SelectionOverlay { /// {@endtemplate} void dispose() { hide(); + _magnifierInfo.dispose(); } Widget _buildStartHandle(BuildContext context) { @@ -1861,12 +1864,13 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S /// Delegate interface for the [TextSelectionGestureDetectorBuilder]. /// -/// The interface is usually implemented by text field implementations wrapping -/// [EditableText], that use a [TextSelectionGestureDetectorBuilder] to build a -/// [TextSelectionGestureDetector] for their [EditableText]. The delegate provides -/// the builder with information about the current state of the text field. -/// Based on these information, the builder adds the correct gesture handlers -/// to the gesture detector. +/// The interface is usually implemented by the [State] of text field +/// implementations wrapping [EditableText], so that they can use a +/// [TextSelectionGestureDetectorBuilder] to build a +/// [TextSelectionGestureDetector] for their [EditableText]. The delegate +/// provides the builder with information about the current state of the text +/// field. Based on that information, the builder adds the correct gesture +/// handlers to the gesture detector. /// /// See also: /// @@ -1897,6 +1901,12 @@ abstract class TextSelectionGestureDetectorBuilderDelegate { /// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is /// obtained by calling [buildGestureDetector]. /// +/// A [TextSelectionGestureDetectorBuilder] must be provided a +/// [TextSelectionGestureDetectorBuilderDelegate], from which information about +/// the [EditableText] may be obtained. Typically, the [State] of the widget +/// that builds the [EditableText] implements this interface, and then passes +/// itself as the [delegate]. +/// /// See also: /// /// * [TextField], which uses a subclass to implement the Material-specific @@ -1905,8 +1915,6 @@ abstract class TextSelectionGestureDetectorBuilderDelegate { /// Cupertino-specific gesture logic of an [EditableText]. class TextSelectionGestureDetectorBuilder { /// Creates a [TextSelectionGestureDetectorBuilder]. - /// - /// The [delegate] must not be null. TextSelectionGestureDetectorBuilder({ required this.delegate, }); @@ -1916,6 +1924,9 @@ class TextSelectionGestureDetectorBuilder { /// The delegate provides the builder with information about what actions can /// currently be performed on the text field. Based on this, the builder adds /// the correct gesture handlers to the gesture detector. + /// + /// Typically implemented by a [State] of a widget that builds an + /// [EditableText]. @protected final TextSelectionGestureDetectorBuilderDelegate delegate; @@ -1993,11 +2004,6 @@ class TextSelectionGestureDetectorBuilder { && targetSelection.end >= textPosition.offset; } - /// Returns true if shift left or right is contained in the given set. - static bool _containsShift(Set keysPressed) { - return keysPressed.any({ LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight }.contains); - } - // Expand the selection to the given global position. // // Either base or extent will be moved to the last tapped position, whichever @@ -2074,6 +2080,10 @@ class TextSelectionGestureDetectorBuilder { @protected RenderEditable get renderEditable => editableText.renderEditable; + /// Whether the Shift key was pressed when the most recent [PointerDownEvent] + /// was tracked by the [BaseTapAndDragGestureRecognizer]. + bool _isShiftPressed = false; + /// The viewport offset pixels of any [Scrollable] containing the /// [RenderEditable] at the last drag start. double _dragStartScrollOffset = 0.0; @@ -2113,6 +2123,30 @@ class TextSelectionGestureDetectorBuilder { // focused, the cursor moves to the long press position. bool _longPressStartedWithoutFocus = false; + /// Handler for [TextSelectionGestureDetector.onTapTrackStart]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTapTrackStart], which triggers this + /// callback. + @protected + void onTapTrackStart() { + _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed + .intersection({LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight}) + .isNotEmpty; + } + + /// Handler for [TextSelectionGestureDetector.onTapTrackReset]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTapTrackReset], which triggers this + /// callback. + @protected + void onTapTrackReset() { + _isShiftPressed = false; + } + /// Handler for [TextSelectionGestureDetector.onTapDown]. /// /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets @@ -2145,11 +2179,9 @@ class TextSelectionGestureDetectorBuilder { || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; - // Handle shift + click selection if needed. - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); // It is impossible to extend the selection when the shift key is pressed, if the // renderEditable.selection is invalid. - final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null; + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: @@ -2246,11 +2278,9 @@ class TextSelectionGestureDetectorBuilder { @protected void onSingleTapUp(TapDragUpDetails details) { if (delegate.selectionEnabled) { - // Handle shift + click selection if needed. - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); // It is impossible to extend the selection when the shift key is pressed, if the // renderEditable.selection is invalid. - final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null; + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.linux: case TargetPlatform.macOS: @@ -2538,14 +2568,13 @@ class TextSelectionGestureDetectorBuilder { _selectTextBoundariesInRange(boundary: lineBoundary, from: from, to: to, cause: cause); } - // Returns the closest boundary location to `extent` but not including `extent` - // itself. - TextRange _moveBeyondTextBoundary(TextPosition extent, TextBoundary textBoundary) { + // Returns the location of a text boundary at `extent`. When `extent` is at + // the end of the text, returns the previous text boundary's location. + TextRange _moveToTextBoundary(TextPosition extent, TextBoundary textBoundary) { assert(extent.offset >= 0); - // if x is a boundary defined by `textBoundary`, most textBoundaries (except - // LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`. - // Use x - 1 here to make sure we don't get stuck at the fixed point x. - final int start = textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0; + // Use extent.offset - 1 when `extent` is at the end of the text to retrieve + // the previous text boundary's location. + final int start = textBoundary.getLeadingTextBoundaryAt(extent.offset == editableText.textEditingValue.text.length ? extent.offset - 1 : extent.offset) ?? 0; final int end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? editableText.textEditingValue.text.length; return TextRange(start: start, end: end); } @@ -2560,13 +2589,13 @@ class TextSelectionGestureDetectorBuilder { // beginning and end of a text boundary respectively. void _selectTextBoundariesInRange({required TextBoundary boundary, required Offset from, Offset? to, SelectionChangedCause? cause}) { final TextPosition fromPosition = renderEditable.getPositionForPoint(from); - final TextRange fromRange = _moveBeyondTextBoundary(fromPosition, boundary); + final TextRange fromRange = _moveToTextBoundary(fromPosition, boundary); final TextPosition toPosition = to == null ? fromPosition : renderEditable.getPositionForPoint(to); final TextRange toRange = toPosition == fromPosition ? fromRange - : _moveBeyondTextBoundary(toPosition, boundary); + : _moveToTextBoundary(toPosition, boundary); final bool isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end; final TextSelection newSelection = isFromBoundaryBeforeToBoundary @@ -2641,9 +2670,7 @@ class TextSelectionGestureDetectorBuilder { return; } - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - - if (isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) { + if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -2730,9 +2757,7 @@ class TextSelectionGestureDetectorBuilder { return; } - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - - if (!isShiftPressed) { + if (!_isShiftPressed) { // Adjust the drag start offset for possible viewport offset changes. final Offset editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) @@ -2931,14 +2956,13 @@ class TextSelectionGestureDetectorBuilder { /// callback. @protected void onDragSelectionEnd(TapDragEndDetails details) { - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); _dragBeganOnPreviousSelection = null; if (_shouldShowSelectionToolbar && _TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) { editableText.showToolbar(); } - if (isShiftPressed) { + if (_isShiftPressed) { _dragStartSelection = null; } @@ -2948,7 +2972,9 @@ class TextSelectionGestureDetectorBuilder { /// Returns a [TextSelectionGestureDetector] configured with the handlers /// provided by this builder. /// - /// The [child] or its subtree should contain [EditableText]. + /// The [child] or its subtree should contain an [EditableText] whose key is + /// the [GlobalKey] provided by the [delegate]'s + /// [TextSelectionGestureDetectorBuilderDelegate.editableTextKey]. Widget buildGestureDetector({ Key? key, HitTestBehavior? behavior, @@ -2956,6 +2982,8 @@ class TextSelectionGestureDetectorBuilder { }) { return TextSelectionGestureDetector( key: key, + onTapTrackStart: onTapTrackStart, + onTapTrackReset: onTapTrackReset, onTapDown: onTapDown, onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, @@ -2993,9 +3021,10 @@ class TextSelectionGestureDetector extends StatefulWidget { /// Create a [TextSelectionGestureDetector]. /// /// Multiple callbacks can be called for one sequence of input gesture. - /// The [child] parameter must not be null. const TextSelectionGestureDetector({ super.key, + this.onTapTrackStart, + this.onTapTrackReset, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, @@ -3015,6 +3044,12 @@ class TextSelectionGestureDetector extends StatefulWidget { required this.child, }); + /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackStart} + final VoidCallback? onTapTrackStart; + + /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackReset} + final VoidCallback? onTapTrackReset; + /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement /// to not qualify as taps (e.g. pans and flings). @@ -3125,6 +3160,14 @@ class _TextSelectionGestureDetectorState extends State with Widget )); // In the case of an error from the Clipboard API, set the value to // unknown so that it will try to update again later. - if (_disposed || value == ClipboardStatus.unknown) { + if (_disposed) { return; } value = ClipboardStatus.unknown; @@ -3322,7 +3369,7 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget ? ClipboardStatus.pasteable : ClipboardStatus.notPasteable; - if (_disposed || nextStatus == value) { + if (_disposed) { return; } value = nextStatus; diff --git a/packages/flutter/lib/src/widgets/texture.dart b/packages/flutter/lib/src/widgets/texture.dart index ce68edef1e191..4f506b46e1a89 100644 --- a/packages/flutter/lib/src/widgets/texture.dart +++ b/packages/flutter/lib/src/widgets/texture.dart @@ -28,9 +28,9 @@ import 'framework.dart'; /// /// See also: /// -/// * +/// * [TextureRegistry](/javadoc/io/flutter/view/TextureRegistry.html) /// for how to create and manage backend textures on Android. -/// * +/// * [TextureRegistry Protocol](/ios-embedder/protocol_flutter_texture_registry-p.html) /// for how to create and manage backend textures on iOS. class Texture extends LeafRenderObjectWidget { /// Creates a widget backed by the texture identified by [textureId], and use diff --git a/packages/flutter/lib/src/widgets/ticker_provider.dart b/packages/flutter/lib/src/widgets/ticker_provider.dart index 09dcc608a8143..270e7b66a7ec8 100644 --- a/packages/flutter/lib/src/widgets/ticker_provider.dart +++ b/packages/flutter/lib/src/widgets/ticker_provider.dart @@ -20,8 +20,6 @@ export 'package:flutter/scheduler.dart' show TickerProvider; /// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin]. class TickerMode extends StatefulWidget { /// Creates a widget that enables or disables tickers. - /// - /// The [enabled] argument must not be null. const TickerMode({ super.key, required this.enabled, diff --git a/packages/flutter/lib/src/widgets/title.dart b/packages/flutter/lib/src/widgets/title.dart index a97ca974f8bfb..ad20e4db54a35 100644 --- a/packages/flutter/lib/src/widgets/title.dart +++ b/packages/flutter/lib/src/widgets/title.dart @@ -23,7 +23,6 @@ class Title extends StatelessWidget { }) : assert(color.alpha == 0xFF); /// A one-line description of this app for use in the window manager. - /// Must not be null. final String title; /// A color that the window manager should use to identify this app. Must be diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 57fdc07a5cb71..24d90161c93f9 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -168,8 +168,6 @@ class _AnimatedState extends State { /// position based on the value of a rectangle relative to a bounding box. class SlideTransition extends AnimatedWidget { /// Creates a fractional translation transition. - /// - /// The [position] argument must not be null. const SlideTransition({ super.key, required Animation position, @@ -225,51 +223,58 @@ class SlideTransition extends AnimatedWidget { } } -/// Animates the scale of a transformed widget. +/// Signature for the callback to [MatrixTransition.onTransform]. /// -/// Here's an illustration of the [ScaleTransition] widget, with it's [alignment] -/// animated by a [CurvedAnimation] set to [Curves.fastOutSlowIn]: -/// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/scale_transition.mp4} +/// Computes a [Matrix4] to be used in the [MatrixTransition] transformed widget +/// from the [MatrixTransition.animation] value. +typedef TransformCallback = Matrix4 Function(double animationValue); + +/// Animates the [Matrix4] of a transformed widget. +/// +/// The [onTransform] callback computes a [Matrix4] from the animated value, it +/// is called every time the [animation] changes its value. /// /// {@tool dartpad} -/// The following code implements the [ScaleTransition] as seen in the video -/// above: +/// The following example implements a [MatrixTransition] with a rotation around +/// the Y axis, with a 3D perspective skew. /// -/// ** See code in examples/api/lib/widgets/transitions/scale_transition.0.dart ** +/// ** See code in examples/api/lib/widgets/transitions/matrix_transition.0.dart ** /// {@end-tool} /// /// See also: /// -/// * [PositionedTransition], a widget that animates its child from a start -/// position to an end position over the lifetime of the animation. -/// * [RelativePositionedTransition], a widget that transitions its child's -/// position based on the value of a rectangle relative to a bounding box. -/// * [SizeTransition], a widget that animates its own size and clips and -/// aligns its child. -class ScaleTransition extends AnimatedWidget { - /// Creates a scale transition. +/// * [ScaleTransition], which animates the scale of a widget, by providing a +/// matrix which scales along the X and Y axis. +/// * [RotationTransition], which animates the rotation of a widget, by +/// providing a matrix which rotates along the Z axis. +class MatrixTransition extends AnimatedWidget { + /// Creates a matrix transition. /// - /// The [scale] argument must not be null. The [alignment] argument defaults - /// to [Alignment.center]. - const ScaleTransition({ + /// The [alignment] argument defaults to [Alignment.center]. + const MatrixTransition({ super.key, - required Animation scale, + required Animation animation, + required this.onTransform, this.alignment = Alignment.center, this.filterQuality, this.child, - }) : super(listenable: scale); + }) : super(listenable: animation); - /// The animation that controls the scale of the child. + /// The callback to compute a [Matrix4] from the [animation]. It's called + /// every time [animation] changes its value. + final TransformCallback onTransform; + + /// The animation that controls the matrix of the child. /// - /// If the current value of the scale animation is v, the child will be - /// painted v times its normal size. - Animation get scale => listenable as Animation; + /// The matrix will be computed from the animation with the [onTransform] + /// callback. + Animation get animation => listenable as Animation; - /// The alignment of the origin of the coordinate system in which the scale - /// takes place, relative to the size of the box. + /// The alignment of the origin of the coordinate system in which the + /// transform takes place, relative to the size of the box. /// - /// For example, to set the origin of the scale to bottom middle, you can use - /// an alignment of (0.0, 1.0). + /// For example, to set the origin of the transform to bottom middle, you can + /// use an alignment of (0.0, 1.0). final Alignment alignment; /// The filter quality with which to apply the transform as a bitmap operation. @@ -292,7 +297,7 @@ class ScaleTransition extends AnimatedWidget { // but leaving it in the layer tree before the animation has started or after // it has finished significantly hurts performance. final bool useFilterQuality; - switch (scale.status) { + switch (animation.status) { case AnimationStatus.dismissed: case AnimationStatus.completed: useFilterQuality = false; @@ -300,8 +305,8 @@ class ScaleTransition extends AnimatedWidget { case AnimationStatus.reverse: useFilterQuality = true; } - return Transform.scale( - scale: scale.value, + return Transform( + transform: onTransform(animation.value), alignment: alignment, filterQuality: useFilterQuality ? filterQuality : null, child: child, @@ -309,6 +314,49 @@ class ScaleTransition extends AnimatedWidget { } } +/// Animates the scale of a transformed widget. +/// +/// Here's an illustration of the [ScaleTransition] widget, with it's [scale] +/// animated by a [CurvedAnimation] set to [Curves.fastOutSlowIn]: +/// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/scale_transition.mp4} +/// +/// {@tool dartpad} +/// The following code implements the [ScaleTransition] as seen in the video +/// above: +/// +/// ** See code in examples/api/lib/widgets/transitions/scale_transition.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [PositionedTransition], a widget that animates its child from a start +/// position to an end position over the lifetime of the animation. +/// * [RelativePositionedTransition], a widget that transitions its child's +/// position based on the value of a rectangle relative to a bounding box. +/// * [SizeTransition], a widget that animates its own size and clips and +/// aligns its child. +class ScaleTransition extends MatrixTransition { + /// Creates a scale transition. + /// + /// The [alignment] argument defaults to [Alignment.center]. + const ScaleTransition({ + super.key, + required Animation scale, + super.alignment = Alignment.center, + super.filterQuality, + super.child, + }) : super(animation: scale, onTransform: _handleScaleMatrix); + + /// The animation that controls the scale of the child. + Animation get scale => animation; + + /// The callback that controls the scale of the child. + /// + /// If the current value of the animation is v, the child will be + /// painted v times its normal size. + static Matrix4 _handleScaleMatrix(double value) => Matrix4.diagonal3Values(value, value, 1.0); +} + /// Animates the rotation of a widget. /// /// Here's an illustration of the [RotationTransition] widget, with it's [turns] @@ -328,66 +376,24 @@ class ScaleTransition extends AnimatedWidget { /// widget. /// * [SizeTransition], a widget that animates its own size and clips and /// aligns its child. -class RotationTransition extends AnimatedWidget { +class RotationTransition extends MatrixTransition { /// Creates a rotation transition. - /// - /// The [turns] argument must not be null. const RotationTransition({ super.key, required Animation turns, - this.alignment = Alignment.center, - this.filterQuality, - this.child, - }) : super(listenable: turns); + super.alignment = Alignment.center, + super.filterQuality, + super.child, + }) : super(animation: turns, onTransform: _handleTurnsMatrix); /// The animation that controls the rotation of the child. - /// - /// If the current value of the turns animation is v, the child will be - /// rotated v * 2 * pi radians before being painted. - Animation get turns => listenable as Animation; + Animation get turns => animation; - /// The alignment of the origin of the coordinate system around which the - /// rotation occurs, relative to the size of the box. - /// - /// For example, to set the origin of the rotation to top right corner, use - /// an alignment of (1.0, -1.0) or use [Alignment.topRight] - final Alignment alignment; - - /// The filter quality with which to apply the transform as a bitmap operation. - /// - /// When the animation is stopped (either in [AnimationStatus.dismissed] or - /// [AnimationStatus.completed]), the filter quality argument will be ignored. + /// The callback that controls the rotation of the child. /// - /// {@macro flutter.widgets.Transform.optional.FilterQuality} - final FilterQuality? filterQuality; - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget? child; - - @override - Widget build(BuildContext context) { - // The ImageFilter layer created by setting filterQuality will introduce - // a saveLayer call. This is usually worthwhile when animating the layer, - // but leaving it in the layer tree before the animation has started or after - // it has finished significantly hurts performance. - final bool useFilterQuality; - switch (turns.status) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - useFilterQuality = false; - case AnimationStatus.forward: - case AnimationStatus.reverse: - useFilterQuality = true; - } - return Transform.rotate( - angle: turns.value * math.pi * 2.0, - alignment: alignment, - filterQuality: useFilterQuality ? filterQuality : null, - child: child, - ); - } + /// If the current value of the animation is v, the child will be rotated + /// v * 2 * pi radians before being painted. + static Matrix4 _handleTurnsMatrix(double value) => Matrix4.rotationZ(value * math.pi * 2.0); } /// Animates its own size and clips and aligns its child. @@ -427,9 +433,8 @@ class RotationTransition extends AnimatedWidget { class SizeTransition extends AnimatedWidget { /// Creates a size transition. /// - /// The [axis], [sizeFactor], and [axisAlignment] arguments must not be null. /// The [axis] argument defaults to [Axis.vertical]. The [axisAlignment] - /// defaults to 0.0, which centers the child along the main axis during the + /// defaults to zero, which centers the child along the main axis during the /// transition. const SizeTransition({ super.key, @@ -507,6 +512,26 @@ class SizeTransition extends AnimatedWidget { /// ** See code in examples/api/lib/widgets/transitions/fade_transition.0.dart ** /// {@end-tool} /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being +/// applied to the descendants of the [FadeTransition] widget. This can be +/// confusing for the user, who may not see anything, and may believe the area +/// of the interface where the [FadeTransition] is hiding a widget to be +/// non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the [FadeTransition] +/// widget at all when the [opacity] animation reaches zero. +/// +/// To avoid such problems, it is generally a good idea to combine this widget +/// with an [IgnorePointer] that one enables when the [opacity] animation +/// reaches zero. This prevents interactions with any children in the subtree +/// when the [child] is not visible. For performance reasons, when implementing +/// this, care should be taken not to rebuild the relevant widget (e.g. by +/// calling [State.setState]) except at the transition point. +/// /// See also: /// /// * [Opacity], which does not animate changes in opacity. @@ -515,8 +540,6 @@ class SizeTransition extends AnimatedWidget { /// * [SliverFadeTransition], the sliver version of this widget. class FadeTransition extends SingleChildRenderObjectWidget { /// Creates an opacity transition. - /// - /// The [opacity] argument must not be null. const FadeTransition({ super.key, required this.opacity, @@ -580,14 +603,33 @@ class FadeTransition extends SingleChildRenderObjectWidget { /// /// {@animation 300 378 https://flutter.github.io/assets-for-api-docs/assets/widgets/fade_transition.mp4} /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being +/// applied to the descendants of the [SliverFadeTransition] widget. This can be +/// confusing for the user, who may not see anything, and may believe the area +/// of the interface where the [SliverFadeTransition] is hiding a widget to be +/// non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the +/// [SliverFadeTransition] widget at all when the [opacity] animation reaches +/// zero. +/// +/// To avoid such problems, it is generally a good idea to combine this widget +/// with a [SliverIgnorePointer] that one enables when the [opacity] animation +/// reaches zero. This prevents interactions with any children in the subtree +/// when the [sliver] is not visible. For performance reasons, when implementing +/// this, care should be taken not to rebuild the relevant widget (e.g. by +/// calling [State.setState]) except at the transition point. +/// /// See also: /// /// * [SliverOpacity], which does not animate changes in opacity. /// * [FadeTransition], the box version of this widget. class SliverFadeTransition extends SingleChildRenderObjectWidget { /// Creates an opacity transition. - /// - /// The [opacity] argument must not be null. const SliverFadeTransition({ super.key, required this.opacity, @@ -687,8 +729,6 @@ class RelativeRectTween extends Tween { /// aligns its child. class PositionedTransition extends AnimatedWidget { /// Creates a transition for [Positioned]. - /// - /// The [rect] argument must not be null. const PositionedTransition({ super.key, required Animation rect, @@ -746,7 +786,7 @@ class RelativePositionedTransition extends AnimatedWidget { /// /// Each frame, the [Positioned] widget will be configured to represent the /// current value of the [rect] argument assuming that the stack has the given - /// [size]. Both [rect] and [size] must not be null. + /// [size]. const RelativePositionedTransition({ super.key, required Animation rect, @@ -809,8 +849,6 @@ class DecoratedBoxTransition extends AnimatedWidget { /// Creates an animated [DecoratedBox] whose [Decoration] animation updates /// the widget. /// - /// The [decoration] and [position] must not be null. - /// /// See also: /// /// * [DecoratedBox.new] @@ -1043,8 +1081,6 @@ class DefaultTextStyleTransition extends AnimatedWidget { /// reports the new value in its builder callback. class ListenableBuilder extends AnimatedWidget { /// Creates a builder that responds to changes in [listenable]. - /// - /// The [listenable] and [builder] arguments must not be null. const ListenableBuilder({ super.key, required super.listenable, diff --git a/packages/flutter/lib/src/widgets/tween_animation_builder.dart b/packages/flutter/lib/src/widgets/tween_animation_builder.dart index edd7b619b51d6..3ef9f63fe4117 100644 --- a/packages/flutter/lib/src/widgets/tween_animation_builder.dart +++ b/packages/flutter/lib/src/widgets/tween_animation_builder.dart @@ -93,9 +93,6 @@ import 'value_listenable_builder.dart'; class TweenAnimationBuilder extends ImplicitlyAnimatedWidget { /// Creates a [TweenAnimationBuilder]. /// - /// The properties [tween], [duration], and [builder] are required. The values - /// for [tween], [curve], and [builder] must not be null. - /// /// The [TweenAnimationBuilder] takes full ownership of the provided [tween] /// instance and mutates it. Once a [Tween] has been passed to a /// [TweenAnimationBuilder], its properties should not be accessed or changed diff --git a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart index 82befbce08432..5759df9c90e9a 100644 --- a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart +++ b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; +import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; import 'framework.dart'; @@ -391,7 +392,7 @@ class _TwoDimensionalViewportElement extends RenderObjectElement /// RenderTwoDimensionalViewport override the paint method, the [paintOffset] /// should be used to position the child in the viewport in order to account for /// a reversed [AxisDirection] in one or both dimensions. -class TwoDimensionalViewportParentData extends ParentData { +class TwoDimensionalViewportParentData extends ParentData with KeepAliveParentDataMixin { /// The offset at which to paint the child in the parent's coordinate system. /// /// This [Offset] represents the top left corner of the child of the @@ -472,14 +473,18 @@ class TwoDimensionalViewportParentData extends ParentData { /// position the children instead of [layoutOffset]. Offset? paintOffset; + @override + bool get keptAlive => keepAlive && !isVisible; + @override String toString() { return 'vicinity=$vicinity; ' 'layoutOffset=$layoutOffset; ' 'paintOffset=$paintOffset; ' '${_paintExtent == null - ? 'not visible ' - : '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent'}'; + ? 'not visible; ' + : '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent; '}' + '${keepAlive ? "keepAlive; " : ""}'; } } @@ -493,9 +498,6 @@ class TwoDimensionalViewportParentData extends ParentData { /// /// Subclasses should not override [performLayout], as it handles housekeeping /// on either side of the call to [layoutChildSequence]. -// TODO(Piinks): Two follow up changes: -// - Keep alive https://github.com/flutter/flutter/issues/126297 -// - ensureVisible https://github.com/flutter/flutter/issues/126299 abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport { /// Initializes fields for subclasses. /// @@ -527,7 +529,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA _delegate = delegate, _mainAxis = mainAxis, _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent, - _clipBehavior = clipBehavior; + _clipBehavior = clipBehavior { + assert(() { + _debugDanglingKeepAlives = []; + return true; + }()); + } /// Which part of the content inside the viewport should be visible in the /// horizontal axis. @@ -674,6 +681,16 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA } final TwoDimensionalChildManager _childManager; + final Map _children = {}; + /// Children that have been laid out (or re-used) during the course of + /// performLayout, used to update the keep alive bucket at the end of + /// performLayout. + final Map _activeChildrenForLayoutPass = {}; + /// The nodes being kept alive despite not being visible. + final Map _keepAliveBucket = {}; + + late List _debugDanglingKeepAlives; + bool _hasVisualOverflow = false; final LayerHandle _clipRectLayer = LayerHandle(); @@ -683,7 +700,6 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA @override bool get sizedByParent => true; - final Map _children = {}; // Keeps track of the upper and lower bounds of ChildVicinity indices when // subclasses call buildOrObtainChildFor during layoutChildSequence. These // values are used to sort children in accordance with the mainAxis for @@ -788,6 +804,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA for (final RenderBox child in _children.values) { child.attach(owner); } + for (final RenderBox child in _keepAliveBucket.values) { + child.attach(owner); + } } @override @@ -799,6 +818,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA for (final RenderBox child in _children.values) { child.detach(); } + for (final RenderBox child in _keepAliveBucket.values) { + child.detach(); + } } @override @@ -806,6 +828,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA for (final RenderBox child in _children.values) { child.redepthChildren(); } + _keepAliveBucket.values.forEach(redepthChild); } @override @@ -815,6 +838,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA visitor(child); child = parentDataOf(child)._nextSibling; } + _keepAliveBucket.values.forEach(visitor); } @override @@ -824,11 +848,10 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA RenderBox? child = _firstChild; while (child != null) { final TwoDimensionalViewportParentData childParentData = parentDataOf(child); - if (childParentData.isVisible) { - visitor(child); - } + visitor(child); child = childParentData._nextSibling; } + // Do not visit children in [_keepAliveBucket]. } @override @@ -893,10 +916,273 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA } } + @protected + @override + RevealedOffset getOffsetToReveal( + RenderObject target, + double alignment, { + Rect? rect, + Axis? axis, + }) { + // If an axis has not been specified, use the mainAxis. + axis ??= mainAxis; + + final (double offset, AxisDirection axisDirection) = switch (axis) { + Axis.vertical => (verticalOffset.pixels, verticalAxisDirection), + Axis.horizontal => (horizontalOffset.pixels, horizontalAxisDirection), + }; + + rect ??= target.paintBounds; + // `child` will be the last RenderObject before the viewport when walking + // up from `target`. + RenderObject child = target; + while (child.parent != this) { + child = child.parent!; + } + + assert(child.parent == this); + final RenderBox box = child as RenderBox; + final Rect rectLocal = MatrixUtils.transformRect(target.getTransformTo(child), rect); + + final double targetMainAxisExtent; + double leadingScrollOffset = offset; + // The scroll offset of `rect` within `child`. + switch (axisDirection) { + case AxisDirection.up: + leadingScrollOffset += child.size.height - rectLocal.bottom; + targetMainAxisExtent = rectLocal.height; + case AxisDirection.right: + leadingScrollOffset += rectLocal.left; + targetMainAxisExtent = rectLocal.width; + case AxisDirection.down: + leadingScrollOffset += rectLocal.top; + targetMainAxisExtent = rectLocal.height; + case AxisDirection.left: + leadingScrollOffset += child.size.width - rectLocal.right; + targetMainAxisExtent = rectLocal.width; + } + + // The scroll offset in the viewport to `rect`. + final TwoDimensionalViewportParentData childParentData = parentDataOf(box); + leadingScrollOffset += switch (axisDirection) { + AxisDirection.down => childParentData.paintOffset!.dy, + AxisDirection.up => viewportDimension.height - childParentData.paintOffset!.dy - box.size.height, + AxisDirection.right => childParentData.paintOffset!.dx, + AxisDirection.left => viewportDimension.width - childParentData.paintOffset!.dx - box.size.width, + }; + + // This step assumes the viewport's layout is up-to-date, i.e., if + // the position is changed after the last performLayout, the new scroll + // position will not be accounted for. + final Matrix4 transform = target.getTransformTo(this); + Rect targetRect = MatrixUtils.transformRect(transform, rect); + + final double mainAxisExtent = switch (axisDirectionToAxis(axisDirection)) { + Axis.horizontal => viewportDimension.width, + Axis.vertical => viewportDimension.height, + }; + + final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; + + final double offsetDifference = switch (axisDirectionToAxis(axisDirection)){ + Axis.vertical => verticalOffset.pixels - targetOffset, + Axis.horizontal => horizontalOffset.pixels - targetOffset, + }; + switch (axisDirection) { + case AxisDirection.down: + targetRect = targetRect.translate(0.0, offsetDifference); + case AxisDirection.right: + targetRect = targetRect.translate(offsetDifference, 0.0); + case AxisDirection.up: + targetRect = targetRect.translate(0.0, -offsetDifference); + case AxisDirection.left: + targetRect = targetRect.translate(-offsetDifference, 0.0); + } + + final RevealedOffset revealedOffset = RevealedOffset( + offset: targetOffset, + rect: targetRect, + ); + return revealedOffset; + } + @override - RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { - // TODO(Piinks): Add this back in follow up change (ensureVisible), https://github.com/flutter/flutter/issues/126299 - return const RevealedOffset(offset: 0.0, rect: Rect.zero); + void showOnScreen({ + RenderObject? descendant, + Rect? rect, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + }) { + // It is possible for one and not both axes to allow for implicit scrolling, + // so handling is split between the options for allowed implicit scrolling. + final bool allowHorizontal = horizontalOffset.allowImplicitScrolling; + final bool allowVertical = verticalOffset.allowImplicitScrolling; + AxisDirection? axisDirection; + switch ((allowHorizontal, allowVertical)) { + case (true, true): + // Both allow implicit scrolling. + break; + case (false, true): + // Only the vertical Axis allows implicit scrolling. + axisDirection = verticalAxisDirection; + case (true, false): + // Only the horizontal Axis allows implicit scrolling. + axisDirection = horizontalAxisDirection; + case (false, false): + // Neither axis allows for implicit scrolling. + return super.showOnScreen( + descendant: descendant, + rect: rect, + duration: duration, + curve: curve, + ); + } + + final Rect? newRect = RenderTwoDimensionalViewport.showInViewport( + descendant: descendant, + viewport: this, + axisDirection: axisDirection, + rect: rect, + duration: duration, + curve: curve, + ); + + super.showOnScreen( + rect: newRect, + duration: duration, + curve: curve, + ); + } + + /// Make (a portion of) the given `descendant` of the given `viewport` fully + /// visible in one or both dimensions of the `viewport` by manipulating the + /// [ViewportOffset]s. + /// + /// The `axisDirection` determines from which axes the `descendant` will be + /// revealed. When the `axisDirection` is null, both axes will be updated to + /// reveal the descendant. + /// + /// The optional `rect` parameter describes which area of the `descendant` + /// should be shown in the viewport. If `rect` is null, the entire + /// `descendant` will be revealed. The `rect` parameter is interpreted + /// relative to the coordinate system of `descendant`. + /// + /// The returned [Rect] describes the new location of `descendant` or `rect` + /// in the viewport after it has been revealed. See [RevealedOffset.rect] + /// for a full definition of this [Rect]. + /// + /// The parameter `viewport` is required and cannot be null. If `descendant` + /// is null, this is a no-op and `rect` is returned. + /// + /// If both `descendant` and `rect` are null, null is returned because there + /// is nothing to be shown in the viewport. + /// + /// The `duration` parameter can be set to a non-zero value to animate the + /// target object into the viewport with an animation defined by `curve`. + /// + /// See also: + /// + /// * [RenderObject.showOnScreen], overridden by + /// [RenderTwoDimensionalViewport] to delegate to this method. + static Rect? showInViewport({ + RenderObject? descendant, + Rect? rect, + required RenderTwoDimensionalViewport viewport, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + AxisDirection? axisDirection, + }) { + if (descendant == null) { + return rect; + } + + Rect? showVertical(Rect? rect) { + return RenderTwoDimensionalViewport._showInViewportForAxisDirection( + descendant: descendant, + viewport: viewport, + axis: Axis.vertical, + rect: rect, + duration: duration, + curve: curve, + ); + } + + Rect? showHorizontal(Rect? rect) { + return RenderTwoDimensionalViewport._showInViewportForAxisDirection( + descendant: descendant, + viewport: viewport, + axis: Axis.horizontal, + rect: rect, + duration: duration, + curve: curve, + ); + } + + switch (axisDirection) { + case AxisDirection.left: + case AxisDirection.right: + return showHorizontal(rect); + case AxisDirection.up: + case AxisDirection.down: + return showVertical(rect); + case null: + // Update rect after revealing in one axis before revealing in the next. + rect = showHorizontal(rect) ?? rect; + // We only return the final rect after both have been revealed. + rect = showVertical(rect); + if (rect == null) { + // `descendant` is between leading and trailing edge and hence already + // fully shown on screen. + assert(viewport.parent != null); + final Matrix4 transform = descendant.getTransformTo(viewport.parent); + return MatrixUtils.transformRect( + transform, + rect ?? descendant.paintBounds, + ); + } + return rect; + } + } + + static Rect? _showInViewportForAxisDirection({ + required RenderObject descendant, + Rect? rect, + required RenderTwoDimensionalViewport viewport, + required Axis axis, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + }) { + final ViewportOffset offset = switch (axis) { + Axis.vertical => viewport.verticalOffset, + Axis.horizontal => viewport.horizontalOffset, + }; + + final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal( + descendant, + 0.0, + rect: rect, + axis: axis, + ); + final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal( + descendant, + 1.0, + rect: rect, + axis: axis, + ); + final double currentOffset = offset.pixels; + + final RevealedOffset? targetOffset = RevealedOffset.clampOffset( + leadingEdgeOffset: leadingEdgeOffset, + trailingEdgeOffset: trailingEdgeOffset, + currentOffset: currentOffset, + ); + if (targetOffset == null) { + // Already visible in this axis. + return null; + } + + offset.moveTo(targetOffset.offset, duration: duration, curve: curve); + return targetOffset.rect; } /// Should be used by subclasses to invalidate any cached metrics for the @@ -959,6 +1245,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA void performLayout() { _firstChild = null; _lastChild = null; + _activeChildrenForLayoutPass.clear(); _childManager._startLayout(); // Subclass lays out children. @@ -967,15 +1254,35 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA assert(_debugCheckContentDimensions()); _didResize = false; _needsDelegateRebuild = false; + _cacheKeepAlives(); invokeLayoutCallback((BoxConstraints _) { _childManager._endLayout(); assert(_debugOrphans?.isEmpty ?? true); + assert(_debugDanglingKeepAlives.isEmpty); + // Ensure we are not keeping anything alive that should not be any longer. + assert(_keepAliveBucket.values.where((RenderBox child) { + return !parentDataOf(child).keepAlive; + }).isEmpty); // Organize children in paint order and complete parent data after // un-used children are disposed of by the childManager. _reifyChildren(); }); } + void _cacheKeepAlives() { + final List remainingChildren = _children.values.toSet().difference( + _activeChildrenForLayoutPass.values.toSet() + ).toList(); + for (final RenderBox child in remainingChildren) { + final TwoDimensionalViewportParentData childParentData = parentDataOf(child); + if (childParentData.keepAlive) { + _keepAliveBucket[childParentData.vicinity] = child; + // Let the child manager know we intend to keep this. + _childManager._reuseChild(childParentData.vicinity); + } + } + } + // Ensures all children have a layoutOffset, sets paintExtent & paintOffset, // and arranges children in paint order. void _reifyChildren() { @@ -1082,12 +1389,20 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA return true; } - /// Returns the child for a given [ChildVicinity]. + /// Returns the child for a given [ChildVicinity], should be called during + /// [layoutChildSequence] in order to instantiate or retrieve children. /// /// This method will build the child if it has not been already, or will reuse - /// it if it already exists. + /// it if it already exists, whether it was part of the previous frame or kept + /// alive. + /// + /// Children for the given [ChildVicinity] will be inserted into the active + /// children list, and so should be visible, or contained within the + /// [cacheExtent]. RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) { assert(vicinity != ChildVicinity.invalid); + // This should only be called during layout. + assert(debugDoingThisLayout); if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) { // First child of this layout pass. Set leading and trailing trackers. _leadingXIndex = vicinity.xIndex; @@ -1107,11 +1422,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA _leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!); _trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!); } - if (_needsDelegateRebuild || !_children.containsKey(vicinity)) { + if (_needsDelegateRebuild || (!_children.containsKey(vicinity) && !_keepAliveBucket.containsKey(vicinity))) { invokeLayoutCallback((BoxConstraints _) { _childManager._buildChild(vicinity); }); } else { + _keepAliveBucket.remove(vicinity); _childManager._reuseChild(vicinity); } if (!_children.containsKey(vicinity)) { @@ -1122,6 +1438,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA assert(_children.containsKey(vicinity)); final RenderBox child = _children[vicinity]!; + _activeChildrenForLayoutPass[vicinity] = child; parentDataOf(child).vicinity = vicinity; return child; } @@ -1304,23 +1621,59 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA void _insertChild(RenderBox child, ChildVicinity slot) { assert(_debugTrackOrphans(newOrphan: _children[slot])); + assert(!_keepAliveBucket.containsValue(child)); _children[slot] = child; adoptChild(child); } void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) { - if (_children[from] == child) { - _children.remove(from); + final TwoDimensionalViewportParentData childParentData = parentDataOf(child); + if (!childParentData.keptAlive) { + if (_children[from] == child) { + _children.remove(from); + } + assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child)); + _children[to] = child; + return; + } + // If the child in the bucket is not current child, that means someone has + // already moved and replaced current child, and we cannot remove this + // child. + if (_keepAliveBucket[childParentData.vicinity] == child) { + _keepAliveBucket.remove(childParentData.vicinity); } - assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child)); - _children[to] = child; + assert(() { + _debugDanglingKeepAlives.remove(child); + return true; + }()); + // If there is an existing child in the new slot, that mean that child + // will be moved to other index. In other cases, the existing child should + // have been removed by _removeChild. Thus, it is ok to overwrite it. + assert(() { + if (_keepAliveBucket.containsKey(childParentData.vicinity)) { + _debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.vicinity]!); + } + return true; + }()); + _keepAliveBucket[childParentData.vicinity] = child; } void _removeChild(RenderBox child, ChildVicinity slot) { - if (_children[slot] == child) { - _children.remove(slot); + final TwoDimensionalViewportParentData childParentData = parentDataOf(child); + if (!childParentData.keptAlive) { + if (_children[slot] == child) { + _children.remove(slot); + } + assert(_debugTrackOrphans(noLongerOrphan: child)); + dropChild(child); + return; } - assert(_debugTrackOrphans(noLongerOrphan: child)); + assert(_keepAliveBucket[childParentData.vicinity] == child); + assert(() { + _debugDanglingKeepAlives.remove(child); + return true; + }()); + _keepAliveBucket.remove(childParentData.vicinity); dropChild(child); } diff --git a/packages/flutter/lib/src/widgets/unique_widget.dart b/packages/flutter/lib/src/widgets/unique_widget.dart index fa6c078f5c261..64688e4ac4b21 100644 --- a/packages/flutter/lib/src/widgets/unique_widget.dart +++ b/packages/flutter/lib/src/widgets/unique_widget.dart @@ -20,8 +20,8 @@ import 'framework.dart'; abstract class UniqueWidget> extends StatefulWidget { /// Creates a widget that has exactly one inflated instance in the tree. /// - /// The [key] argument must not be null because it identifies the unique - /// inflated instance of this widget. + /// The [key] argument is required because it identifies the unique inflated + /// instance of this widget. const UniqueWidget({ required GlobalKey key, }) : super(key: key); diff --git a/packages/flutter/lib/src/widgets/value_listenable_builder.dart b/packages/flutter/lib/src/widgets/value_listenable_builder.dart index 79a3d8cb6bff4..0673fd2fd0c84 100644 --- a/packages/flutter/lib/src/widgets/value_listenable_builder.dart +++ b/packages/flutter/lib/src/widgets/value_listenable_builder.dart @@ -111,7 +111,6 @@ typedef ValueWidgetBuilder = Widget Function(BuildContext context, T value, W class ValueListenableBuilder extends StatefulWidget { /// Creates a [ValueListenableBuilder]. /// - /// The [valueListenable] and [builder] arguments must not be null. /// The [child] is optional but is good practice to use if part of the widget /// subtree does not depend on the value of the [valueListenable]. const ValueListenableBuilder({ @@ -125,8 +124,6 @@ class ValueListenableBuilder extends StatefulWidget { /// /// This widget does not ensure that the [ValueListenable]'s value is not /// null, therefore your [builder] may need to handle null values. - /// - /// This [ValueListenable] itself must not be null. final ValueListenable valueListenable; /// A [ValueWidgetBuilder] which builds a widget depending on the @@ -134,8 +131,6 @@ class ValueListenableBuilder extends StatefulWidget { /// /// Can incorporate a [valueListenable] value-independent widget subtree /// from the [child] parameter into the returned widget tree. - /// - /// Must not be null. final ValueWidgetBuilder builder; /// A [valueListenable]-independent widget which is passed back to the [builder]. diff --git a/packages/flutter/lib/src/widgets/view.dart b/packages/flutter/lib/src/widgets/view.dart index 14f87c6d10df0..cfb6e02e1f378 100644 --- a/packages/flutter/lib/src/widgets/view.dart +++ b/packages/flutter/lib/src/widgets/view.dart @@ -2,48 +2,98 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show FlutterView; +import 'dart:collection'; +import 'dart:ui' show FlutterView, SemanticsUpdate; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'framework.dart'; import 'lookup_boundary.dart'; import 'media_query.dart'; -/// Injects a [FlutterView] into the tree and makes it available to descendants -/// within the same [LookupBoundary] via [View.of] and [View.maybeOf]. +/// Bootstraps a render tree that is rendered into the provided [FlutterView]. +/// +/// The content rendered into that view is determined by the provided [child]. +/// Descendants within the same [LookupBoundary] can look up the view they are +/// rendered into via [View.of] and [View.maybeOf]. /// /// The provided [child] is wrapped in a [MediaQuery] constructed from the given /// [view]. /// -/// In a future version of Flutter, the functionality of this widget will be -/// extended to actually bootstrap the render tree that is going to be rendered -/// into the provided [view]. This will enable rendering content into multiple -/// [FlutterView]s from a single widget tree. +/// For most use cases, using [MediaQuery.of] is a more appropriate way of +/// obtaining the information that a [FlutterView] exposes. For example, using +/// [MediaQuery] will expose the _logical_ device size ([MediaQueryData.size]) +/// rather than the physical size ([FlutterView.physicalSize]). Similarly, while +/// [FlutterView.padding] conveys the information from the operating system, the +/// [MediaQueryData.padding] further adjusts this information to be aware of the +/// context of the widget; e.g. the [Scaffold] widget adjusts the values for its +/// various children. /// /// Each [FlutterView] can be associated with at most one [View] widget in the /// widget tree. Two or more [View] widgets configured with the same /// [FlutterView] must never exist within the same widget tree at the same time. -/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives -/// its identity from the [view] provided to this widget. +/// This limitation is enforced by a [GlobalObjectKey] that derives its identity +/// from the [view] provided to this widget. +/// +/// Since the [View] widget bootstraps its own independent render tree, neither +/// it nor any of its descendants will insert a [RenderObject] into an existing +/// render tree. Therefore, the [View] widget can only be used in those parts of +/// the widget tree where it is not required to participate in the construction +/// of the surrounding render tree. In other words, the widget may only be used +/// in a non-rendering zone of the widget tree (see [WidgetsBinding] for a +/// definition of rendering and non-rendering zones). +/// +/// In practical terms, the widget is typically used at the root of the widget +/// tree outside of any other [View] widget, as a child of a [ViewCollection] +/// widget, or in the [ViewAnchor.view] slot of a [ViewAnchor] widget. It is not +/// required to be a direct child, though, since other non-[RenderObjectWidget]s +/// (e.g. [InheritedWidget]s, [Builder]s, or [StatefulWidget]s/[StatelessWidget] +/// that only produce non-[RenderObjectWidget]s) are allowed to be present +/// between those widgets and the [View] widget. +/// +/// See also: +/// +/// * [Element.debugExpectsRenderObjectForSlot], which defines whether a [View] +/// widget is allowed in a given child slot. class View extends StatelessWidget { - /// Injects the provided [view] into the widget tree. - View({required this.view, required this.child}) : super(key: GlobalObjectKey(view)); + /// Create a [View] widget to bootstrap a render tree that is rendered into + /// the provided [FlutterView]. + /// + /// The content rendered into that [view] is determined by the given [child] + /// widget. + View({ + super.key, + required this.view, + @Deprecated( + 'Do not use. ' + 'This parameter only exists to implement the deprecated RendererBinding.pipelineOwner property until it is removed. ' + 'This feature was deprecated after v3.10.0-12.0.pre.' + ) + PipelineOwner? deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner, + @Deprecated( + 'Do not use. ' + 'This parameter only exists to implement the deprecated RendererBinding.renderView property until it is removed. ' + 'This feature was deprecated after v3.10.0-12.0.pre.' + ) + RenderView? deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView, + required this.child, + }) : _deprecatedPipelineOwner = deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner, + _deprecatedRenderView = deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView, + assert((deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner == null) == (deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null)), + assert(deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null || deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView.flutterView == view); - /// The [FlutterView] to be injected into the tree. + /// The [FlutterView] into which [child] is drawn. final FlutterView view; + /// The widget below this widget in the tree, which will be drawn into the + /// [view]. + /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; - @override - Widget build(BuildContext context) { - return _ViewScope( - view: view, - child: MediaQuery.fromView( - view: view, - child: child, - ), - ); - } + final PipelineOwner? _deprecatedPipelineOwner; + final RenderView? _deprecatedRenderView; /// Returns the [FlutterView] that the provided `context` will render into. /// @@ -52,7 +102,7 @@ class View extends StatelessWidget { /// The method creates a dependency on the `context`, which will be informed /// when the identity of the [FlutterView] changes (i.e. the `context` is /// moved to render into a different [FlutterView] then before). The context - /// will not be informed when the properties on the [FlutterView] itself + /// will not be informed when the _properties_ on the [FlutterView] itself /// change their values. To access the property values of a [FlutterView] it /// is best practise to use [MediaQuery.maybeOf] instead, which will ensure /// that the `context` is informed when the view properties change. @@ -72,7 +122,7 @@ class View extends StatelessWidget { /// The method creates a dependency on the `context`, which will be informed /// when the identity of the [FlutterView] changes (i.e. the `context` is /// moved to render into a different [FlutterView] then before). The context - /// will not be informed when the properties on the [FlutterView] itself + /// will not be informed when the _properties_ on the [FlutterView] itself /// change their values. To access the property values of a [FlutterView] it /// is best practise to use [MediaQuery.of] instead, which will ensure that /// the `context` is informed when the view properties change. @@ -106,13 +156,588 @@ class View extends StatelessWidget { }()); return result!; } + + /// Returns the [PipelineOwner] parent to which a child [View] should attach + /// its [PipelineOwner] to. + /// + /// If `context` has a [View] ancestor, it returns the [PipelineOwner] + /// responsible for managing the render tree of that view. If there is no + /// [View] ancestor, [RendererBinding.rootPipelineOwner] is returned instead. + static PipelineOwner pipelineOwnerOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_PipelineOwnerScope>()?.pipelineOwner + ?? RendererBinding.instance.rootPipelineOwner; + } + + @override + Widget build(BuildContext context) { + return _RawView( + view: view, + deprecatedPipelineOwner: _deprecatedPipelineOwner, + deprecatedRenderView: _deprecatedRenderView, + builder: (BuildContext context, PipelineOwner owner) { + return _ViewScope( + view: view, + child: _PipelineOwnerScope( + pipelineOwner: owner, + child: MediaQuery.fromView( + view: view, + child: child, + ), + ), + ); + } + ); + } +} + +/// A builder for the content [Widget] of a [_RawView]. +/// +/// The widget returned by the builder defines the content that is drawn into +/// the [FlutterView] configured on the [_RawView]. +/// +/// The builder is given the [PipelineOwner] that the [_RawView] uses to manage +/// its render tree. Typical builder implementations make that pipeline owner +/// available as an attachment point for potential child views. +/// +/// Used by [_RawView.builder]. +typedef _RawViewContentBuilder = Widget Function(BuildContext context, PipelineOwner owner); + +/// The workhorse behind the [View] widget that actually bootstraps a render +/// tree. +/// +/// It instantiates the [RenderView] as the root of that render tree and adds it +/// to the [RendererBinding] via [RendererBinding.addRenderView]. It also owns +/// the [PipelineOwner] that manages this render tree and adds it as a child to +/// the surrounding parent [PipelineOwner] obtained with [View.pipelineOwnerOf]. +/// This ensures that the render tree bootstrapped by this widget participates +/// properly in frame production and hit testing. +class _RawView extends RenderObjectWidget { + /// Create a [RawView] widget to bootstrap a render tree that is rendered into + /// the provided [FlutterView]. + /// + /// The content rendered into that [view] is determined by the [Widget] + /// returned by [builder]. + _RawView({ + required this.view, + required PipelineOwner? deprecatedPipelineOwner, + required RenderView? deprecatedRenderView, + required this.builder, + }) : _deprecatedPipelineOwner = deprecatedPipelineOwner, + _deprecatedRenderView = deprecatedRenderView, + assert(deprecatedRenderView == null || deprecatedRenderView.flutterView == view), + // TODO(goderbauer): Replace this with GlobalObjectKey(view) when the deprecated properties are removed. + super(key: _DeprecatedRawViewKey(view, deprecatedPipelineOwner, deprecatedRenderView)); + + /// The [FlutterView] into which the [Widget] returned by [builder] is drawn. + final FlutterView view; + + /// Determines the content [Widget] that is drawn into the [view]. + /// + /// The [builder] is given the [PipelineOwner] responsible for the render tree + /// bootstrapped by this widget and typically makes it available as an + /// attachment point for potential child views. + final _RawViewContentBuilder builder; + + final PipelineOwner? _deprecatedPipelineOwner; + final RenderView? _deprecatedRenderView; + + @override + RenderObjectElement createElement() => _RawViewElement(this); + + @override + RenderObject createRenderObject(BuildContext context) { + return _deprecatedRenderView ?? RenderView( + view: view, + ); + } + + // No need to implement updateRenderObject: RawView uses the view as a + // GlobalKey, so we never need to update the RenderObject with a new view. +} + +class _RawViewElement extends RenderTreeRootElement { + _RawViewElement(super.widget); + + late final PipelineOwner _pipelineOwner = PipelineOwner( + onSemanticsOwnerCreated: _handleSemanticsOwnerCreated, + onSemanticsUpdate: _handleSemanticsUpdate, + onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed, + ); + + PipelineOwner get _effectivePipelineOwner => (widget as _RawView)._deprecatedPipelineOwner ?? _pipelineOwner; + + void _handleSemanticsOwnerCreated() { + (_effectivePipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics(); + } + + void _handleSemanticsOwnerDisposed() { + (_effectivePipelineOwner.rootNode as RenderView?)?.clearSemantics(); + } + + void _handleSemanticsUpdate(SemanticsUpdate update) { + (widget as _RawView).view.updateSemantics(update); + } + + @override + RenderView get renderObject => super.renderObject as RenderView; + + Element? _child; + + void _updateChild() { + try { + final Widget child = (widget as _RawView).builder(this, _effectivePipelineOwner); + _child = updateChild(_child, child, null); + } catch (e, stack) { + final FlutterErrorDetails details = FlutterErrorDetails( + exception: e, + stack: stack, + library: 'widgets library', + context: ErrorDescription('building $this'), + informationCollector: !kDebugMode ? null : () => [ + DiagnosticsDebugCreator(DebugCreator(this)), + ], + ); + FlutterError.reportError(details); + final Widget error = ErrorWidget.builder(details); + _child = updateChild(null, error, slot); + } + } + + @override + void mount(Element? parent, Object? newSlot) { + super.mount(parent, newSlot); + assert(_effectivePipelineOwner.rootNode == null); + _effectivePipelineOwner.rootNode = renderObject; + _attachView(); + _updateChild(); + renderObject.prepareInitialFrame(); + if (_effectivePipelineOwner.semanticsOwner != null) { + renderObject.scheduleInitialSemantics(); + } + } + + PipelineOwner? _parentPipelineOwner; // Is null if view is currently not attached. + + void _attachView([PipelineOwner? parentPipelineOwner]) { + assert(_parentPipelineOwner == null); + parentPipelineOwner ??= View.pipelineOwnerOf(this); + parentPipelineOwner.adoptChild(_effectivePipelineOwner); + RendererBinding.instance.addRenderView(renderObject); + _parentPipelineOwner = parentPipelineOwner; + } + + void _detachView() { + final PipelineOwner? parentPipelineOwner = _parentPipelineOwner; + if (parentPipelineOwner != null) { + RendererBinding.instance.removeRenderView(renderObject); + parentPipelineOwner.dropChild(_effectivePipelineOwner); + _parentPipelineOwner = null; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_parentPipelineOwner == null) { + return; + } + final PipelineOwner newParentPipelineOwner = View.pipelineOwnerOf(this); + if (newParentPipelineOwner != _parentPipelineOwner) { + _detachView(); + _attachView(newParentPipelineOwner); + } + } + + @override + void performRebuild() { + super.performRebuild(); + _updateChild(); + } + + @override + void activate() { + super.activate(); + assert(_effectivePipelineOwner.rootNode == null); + _effectivePipelineOwner.rootNode = renderObject; + _attachView(); + } + + @override + void deactivate() { + _detachView(); + assert(_effectivePipelineOwner.rootNode == renderObject); + _effectivePipelineOwner.rootNode = null; // To satisfy the assert in the super class. + super.deactivate(); + } + + @override + void update(_RawView newWidget) { + super.update(newWidget); + _updateChild(); + } + + @override + void visitChildren(ElementVisitor visitor) { + if (_child != null) { + visitor(_child!); + } + } + + @override + void forgetChild(Element child) { + assert(child == _child); + _child = null; + super.forgetChild(child); + } + + @override + void insertRenderObjectChild(RenderBox child, Object? slot) { + assert(slot == null); + assert(renderObject.debugValidateChild(child)); + renderObject.child = child; + } + + @override + void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { + assert(false); + } + + @override + void removeRenderObjectChild(RenderObject child, Object? slot) { + assert(slot == null); + assert(renderObject.child == child); + renderObject.child = null; + } + + @override + void unmount() { + if (_effectivePipelineOwner != (widget as _RawView)._deprecatedPipelineOwner) { + _effectivePipelineOwner.dispose(); + } + super.unmount(); + } } class _ViewScope extends InheritedWidget { const _ViewScope({required this.view, required super.child}); - final FlutterView view; + final FlutterView? view; @override bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view; } + +class _PipelineOwnerScope extends InheritedWidget { + const _PipelineOwnerScope({ + required this.pipelineOwner, + required super.child, + }); + + final PipelineOwner pipelineOwner; + + @override + bool updateShouldNotify(_PipelineOwnerScope oldWidget) => pipelineOwner != oldWidget.pipelineOwner; +} + +class _MultiChildComponentWidget extends Widget { + const _MultiChildComponentWidget({ + super.key, + List views = const [], + Widget? child, + }) : _views = views, _child = child; + + // It is up to the subclasses to make the relevant properties public. + final List _views; + final Widget? _child; + + @override + Element createElement() => _MultiChildComponentElement(this); +} + +/// A collection of sibling [View]s. +/// +/// This widget can only be used in places were a [View] widget is allowed, i.e. +/// in a non-rendering zone of the widget tree. In practical terms, it can be +/// used at the root of the widget tree outside of any [View] widget, as a child +/// to a another [ViewCollection], or in the [ViewAnchor.view] slot of a +/// [ViewAnchor] widget. It is not required to be a direct child of those +/// widgets; other non-[RenderObjectWidget]s may appear in between the two (such +/// as an [InheritedWidget]). +/// +/// Similarly, the [views] children of this widget must be [View]s, but they +/// may be wrapped in additional non-[RenderObjectWidget]s (e.g. +/// [InheritedWidget]s). +/// +/// See also: +/// +/// * [WidgetsBinding] for an explanation of rendering and non-rendering zones. +class ViewCollection extends _MultiChildComponentWidget { + /// Creates a [ViewCollection] widget. + /// + /// The provided list of [views] must contain at least one widget. + const ViewCollection({super.key, required super.views}) : assert(views.length > 0); + + /// The [View] descendants of this widget. + /// + /// The [View]s may be wrapped in other non-[RenderObjectWidget]s (e.g. + /// [InheritedWidget]s). However, no [RenderObjectWidget] is allowed to appear + /// between the [ViewCollection] and the next [View] widget. + List get views => _views; +} + +/// Decorates a [child] widget with a side [View]. +/// +/// This widget must have a [View] ancestor, into which the [child] widget +/// is rendered. +/// +/// Typically, a [View] or [ViewCollection] widget is used in the [view] slot to +/// define the content of the side view(s). Those widgets may be wrapped in +/// other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s). However, no +/// [RenderObjectWidget] is allowed to appear between the [ViewAnchor] and the +/// next [View] widget in the [view] slot. The widgets in the [view] slot have +/// access to all [InheritedWidget]s above the [ViewAnchor] in the tree. +/// +/// In technical terms, the [ViewAnchor] can only be used in a rendering zone of +/// the widget tree and the [view] slot marks the start of a new non-rendering +/// zone (see [WidgetsBinding] for a definition of these zones). Typically, +/// it is occupied by a [View] widget, which will start a new rendering zone. +/// +/// {@template flutter.widgets.ViewAnchor} +/// An example use case for this widget is a tooltip for a button. The tooltip +/// should be able to extend beyond the bounds of the main view. For this, the +/// tooltip can be implemented as a separate [View], which is anchored to the +/// button in the main view by wrapping that button with a [ViewAnchor]. In this +/// example, the [view] slot is configured with the tooltip [View] and the +/// [child] is the button widget rendered into the surrounding view. +/// {@endtemplate} +class ViewAnchor extends StatelessWidget { + /// Creates a [ViewAnchor] widget. + const ViewAnchor({ + super.key, + this.view, + required this.child, + }); + + /// The widget that defines the view anchored to this widget. + /// + /// Typically, a [View] or [ViewCollection] widget is used, which may be + /// wrapped in other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s). + /// + /// {@macro flutter.widgets.ViewAnchor} + final Widget? view; + + /// The widget below this widget in the tree. + /// + /// It is rendered into the surrounding view, not in the view defined by + /// [view]. + /// + /// {@macro flutter.widgets.ViewAnchor} + final Widget child; + + @override + Widget build(BuildContext context) { + return _MultiChildComponentWidget( + views: [ + if (view != null) + _ViewScope( + view: null, + child: view!, + ), + ], + child: child, + ); + } +} + +class _MultiChildComponentElement extends Element { + _MultiChildComponentElement(super.widget); + + List _viewElements = []; + final Set _forgottenViewElements = HashSet(); + Element? _childElement; + + bool _debugAssertChildren() { + final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget; + // Each view widget must have a corresponding element. + assert(_viewElements.length == typedWidget._views.length); + // Iff there is a child widget, it must have a corresponding element. + assert((_childElement == null) == (typedWidget._child == null)); + // The child element is not also a view element. + assert(!_viewElements.contains(_childElement)); + return true; + } + + @override + void attachRenderObject(Object? newSlot) { + super.attachRenderObject(newSlot); + assert(_debugCheckMustAttachRenderObject(newSlot)); + } + + @override + void mount(Element? parent, Object? newSlot) { + super.mount(parent, newSlot); + assert(_debugCheckMustAttachRenderObject(newSlot)); + assert(_viewElements.isEmpty); + assert(_childElement == null); + rebuild(); + assert(_debugAssertChildren()); + } + + @override + void updateSlot(Object? newSlot) { + super.updateSlot(newSlot); + assert(_debugCheckMustAttachRenderObject(newSlot)); + } + + bool _debugCheckMustAttachRenderObject(Object? slot) { + // Check only applies in the ViewCollection configuration. + if (!kDebugMode || (widget as _MultiChildComponentWidget)._child != null) { + return true; + } + bool hasAncestorRenderObjectElement = false; + bool ancestorWantsRenderObject = true; + visitAncestorElements((Element ancestor) { + if (!ancestor.debugExpectsRenderObjectForSlot(slot)) { + ancestorWantsRenderObject = false; + return false; + } + if (ancestor is RenderObjectElement) { + hasAncestorRenderObjectElement = true; + return false; + } + return true; + }); + if (hasAncestorRenderObjectElement && ancestorWantsRenderObject) { + FlutterError.reportError( + FlutterErrorDetails(exception: FlutterError.fromParts( + [ + ErrorSummary( + 'The Element for ${toStringShort()} cannot be inserted into slot "$slot" of its ancestor. ', + ), + ErrorDescription( + 'The ownership chain for the Element in question was:\n ${debugGetCreatorChain(10)}', + ), + ErrorDescription( + 'This Element allows the creation of multiple independent render trees, which cannot ' + 'be attached to an ancestor in an existing render tree. However, an ancestor RenderObject ' + 'is expecting that a child will be attached.' + ), + ErrorHint( + 'Try moving the subtree that contains the ${toStringShort()} widget into the ' + 'view property of a ViewAnchor widget or to the root of the widget tree, where ' + 'it is not expected to attach its RenderObject to its ancestor.', + ), + ], + )), + ); + } + return true; + } + + @override + void update(_MultiChildComponentWidget newWidget) { + // Cannot switch from ViewAnchor config to ViewCollection config. + assert((newWidget._child == null) == ((widget as _MultiChildComponentWidget)._child == null)); + super.update(newWidget); + rebuild(force: true); + assert(_debugAssertChildren()); + } + + static const Object _viewSlot = Object(); + + @override + bool debugExpectsRenderObjectForSlot(Object? slot) => slot != _viewSlot; + + @override + void performRebuild() { + final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget; + + _childElement = updateChild(_childElement, typedWidget._child, slot); + + final List views = typedWidget._views; + _viewElements = updateChildren( + _viewElements, + views, + forgottenChildren: _forgottenViewElements, + slots: List.generate(views.length, (_) => _viewSlot), + ); + _forgottenViewElements.clear(); + + super.performRebuild(); // clears the dirty flag + assert(_debugAssertChildren()); + } + + @override + void forgetChild(Element child) { + if (child == _childElement) { + _childElement = null; + } else { + assert(_viewElements.contains(child)); + assert(!_forgottenViewElements.contains(child)); + _forgottenViewElements.add(child); + } + super.forgetChild(child); + } + + @override + void visitChildren(ElementVisitor visitor) { + if (_childElement != null) { + visitor(_childElement!); + } + for (final Element child in _viewElements) { + if (!_forgottenViewElements.contains(child)) { + visitor(child); + } + } + } + + @override + bool get debugDoingBuild => false; // This element does not have a concept of "building". + + @override + Element? get renderObjectAttachingChild => _childElement; + + @override + List debugDescribeChildren() { + final List children = []; + if (_childElement != null) { + children.add(_childElement!.toDiagnosticsNode()); + } + for (int i = 0; i < _viewElements.length; i++) { + children.add(_viewElements[i].toDiagnosticsNode( + name: 'view ${i + 1}', + style: DiagnosticsTreeStyle.offstage, + )); + } + return children; + } +} + +// A special [GlobalKey] to support passing the deprecated +// [RendererBinding.renderView] and [RendererBinding.pipelineOwner] to the +// [_RawView]. Will be removed when those deprecated properties are removed. +@optionalTypeArgs +class _DeprecatedRawViewKey> extends GlobalKey { + const _DeprecatedRawViewKey(this.view, this.owner, this.renderView) : super.constructor(); + + final FlutterView view; + final PipelineOwner? owner; + final RenderView? renderView; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _DeprecatedRawViewKey + && identical(other.view, view) + && identical(other.owner, owner) + && identical(other.renderView, renderView); + } + + @override + int get hashCode => Object.hash(view, owner, renderView); + + @override + String toString() => '[_DeprecatedRawViewKey ${describeIdentity(view)}]'; +} diff --git a/packages/flutter/lib/src/widgets/viewport.dart b/packages/flutter/lib/src/widgets/viewport.dart index ad4a67facc204..36f556510e92e 100644 --- a/packages/flutter/lib/src/widgets/viewport.dart +++ b/packages/flutter/lib/src/widgets/viewport.dart @@ -53,8 +53,6 @@ class Viewport extends MultiChildRenderObjectWidget { /// The viewport listens to the [offset], which means you do not need to /// rebuild this widget when the [offset] changes. /// - /// The [offset] argument must not be null. - /// /// The [cacheExtent] must be specified if the [cacheExtentStyle] is /// not [CacheExtentStyle.pixel]. Viewport({ @@ -327,8 +325,6 @@ class ShrinkWrappingViewport extends MultiChildRenderObjectWidget { /// /// The viewport listens to the [offset], which means you do not need to /// rebuild this widget when the [offset] changes. - /// - /// The [offset] argument must not be null. const ShrinkWrappingViewport({ super.key, this.axisDirection = AxisDirection.down, diff --git a/packages/flutter/lib/src/widgets/visibility.dart b/packages/flutter/lib/src/widgets/visibility.dart index eaf95eb2e3be3..a5e2fca28f919 100644 --- a/packages/flutter/lib/src/widgets/visibility.dart +++ b/packages/flutter/lib/src/widgets/visibility.dart @@ -42,10 +42,6 @@ import 'ticker_provider.dart'; class Visibility extends StatelessWidget { /// Control whether the given [child] is [visible]. /// - /// The [child] and [replacement] arguments must not be null. - /// - /// The boolean arguments must not be null. - /// /// The [maintainSemantics] and [maintainInteractivity] arguments can only be /// set if [maintainSize] is set. /// @@ -332,10 +328,6 @@ class _VisibilityScope extends InheritedWidget { class SliverVisibility extends StatelessWidget { /// Control whether the given [sliver] is [visible]. /// - /// The [sliver] and [replacementSliver] arguments must not be null. - /// - /// The boolean arguments must not be null. - /// /// The [maintainSemantics] and [maintainInteractivity] arguments can only be /// set if [maintainSize] is set. /// @@ -372,8 +364,6 @@ class SliverVisibility extends StatelessWidget { /// Control whether the given [sliver] is [visible]. /// - /// The [sliver] and [replacementSliver] arguments must not be null. - /// /// This is equivalent to the default [SliverVisibility] constructor with all /// "maintain" fields set to true. This constructor should be used in place of /// a [SliverOpacity] widget that only takes on values of `0.0` or `1.0`, as it diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 7a88e928e81de..bf6324d2ec9b7 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -616,8 +616,6 @@ class _ScreenshotPaintingContext extends PaintingContext { class _DiagnosticsPathNode { /// Creates a full description of a step in a path through a tree of /// [DiagnosticsNode] objects. - /// - /// The [node] and [child] arguments must not be null. _DiagnosticsPathNode({ required this.node, required this.children, @@ -716,7 +714,15 @@ class InspectorReferenceData { } // Production implementation of [WidgetInspectorService]. -class _WidgetInspectorService = Object with WidgetInspectorService; +class _WidgetInspectorService with WidgetInspectorService { + _WidgetInspectorService() { + selection.addListener(() { + if (selectionChangedCallback != null) { + selectionChangedCallback!(); + } + }); + } +} /// Service used by GUI tools to interact with the [WidgetInspector]. /// @@ -747,6 +753,15 @@ mixin WidgetInspectorService { /// The current [WidgetInspectorService]. static WidgetInspectorService get instance => _instance; static WidgetInspectorService _instance = _WidgetInspectorService(); + + /// Whether the inspector is in select mode. + /// + /// In select mode, pointer interactions trigger widget selection instead of + /// normal interactions. Otherwise the previously selected widget is + /// highlighted but the application can be interacted with normally. + @visibleForTesting + final ValueNotifier isSelectMode = ValueNotifier(true); + @protected static set instance(WidgetInspectorService instance) { _instance = instance; @@ -784,7 +799,6 @@ mixin WidgetInspectorService { bool _trackRebuildDirtyWidgets = false; bool _trackRepaintWidgets = false; - late RegisterServiceExtensionCallback _registerServiceExtensionCallback; /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"). /// @@ -797,8 +811,9 @@ mixin WidgetInspectorService { void registerServiceExtension({ required String name, required ServiceExtensionCallback callback, + required RegisterServiceExtensionCallback registerExtension, }) { - _registerServiceExtensionCallback( + registerExtension( name: 'inspector.$name', callback: callback, ); @@ -809,12 +824,14 @@ mixin WidgetInspectorService { void _registerSignalServiceExtension({ required String name, required FutureOr Function() callback, + required RegisterServiceExtensionCallback registerExtension, }) { registerServiceExtension( name: name, callback: (Map parameters) async { return {'result': await callback()}; }, + registerExtension: registerExtension, ); } @@ -827,12 +844,14 @@ mixin WidgetInspectorService { void _registerObjectGroupServiceExtension({ required String name, required FutureOr Function(String objectGroup) callback, + required RegisterServiceExtensionCallback registerExtension, }) { registerServiceExtension( name: name, callback: (Map parameters) async { return {'result': await callback(parameters['objectGroup']!)}; }, + registerExtension: registerExtension, ); } @@ -852,6 +871,7 @@ mixin WidgetInspectorService { required String name, required AsyncValueGetter getter, required AsyncValueSetter setter, + required RegisterServiceExtensionCallback registerExtension, }) { registerServiceExtension( name: name, @@ -863,6 +883,7 @@ mixin WidgetInspectorService { } return {'enabled': await getter() ? 'true' : 'false'}; }, + registerExtension: registerExtension, ); } @@ -893,6 +914,7 @@ mixin WidgetInspectorService { void _registerServiceExtensionWithArg({ required String name, required FutureOr Function(String? objectId, String objectGroup) callback, + required RegisterServiceExtensionCallback registerExtension, }) { registerServiceExtension( name: name, @@ -902,6 +924,7 @@ mixin WidgetInspectorService { 'result': await callback(parameters['arg'], parameters['objectGroup']!), }; }, + registerExtension: registerExtension, ); } @@ -911,6 +934,7 @@ mixin WidgetInspectorService { void _registerServiceExtensionVarArgs({ required String name, required FutureOr Function(List args) callback, + required RegisterServiceExtensionCallback registerExtension, }) { registerServiceExtension( name: name, @@ -931,6 +955,7 @@ mixin WidgetInspectorService { assert(index == parameters.length || (index == parameters.length - 1 && parameters.containsKey('isolateId'))); return {'result': await callback(args)}; }, + registerExtension: registerExtension, ); } @@ -943,7 +968,7 @@ mixin WidgetInspectorService { Future forceRebuild() { final WidgetsBinding binding = WidgetsBinding.instance; if (binding.rootElement != null) { - binding.buildOwner!.reassemble(binding.rootElement!, null); + binding.buildOwner!.reassemble(binding.rootElement!); return binding.endOfFrame; } return Future.value(); @@ -1011,13 +1036,12 @@ mixin WidgetInspectorService { /// * /// * [BindingBase.initServiceExtensions], which explains when service /// extensions can be used. - void initServiceExtensions(RegisterServiceExtensionCallback registerServiceExtensionCallback) { + void initServiceExtensions(RegisterServiceExtensionCallback registerExtension) { final FlutterExceptionHandler defaultExceptionHandler = FlutterError.presentError; if (isStructuredErrorsEnabled()) { FlutterError.presentError = _reportStructuredError; } - _registerServiceExtensionCallback = registerServiceExtensionCallback; assert(!_debugServiceExtensionsRegistered); assert(() { _debugServiceExtensionsRegistered = true; @@ -1033,6 +1057,7 @@ mixin WidgetInspectorService { FlutterError.presentError = value ? _reportStructuredError : defaultExceptionHandler; return Future.value(); }, + registerExtension: registerExtension, ); _registerBoolServiceExtension( @@ -1045,6 +1070,7 @@ mixin WidgetInspectorService { WidgetsApp.debugShowWidgetInspectorOverride = value; return forceRebuild(); }, + registerExtension: registerExtension, ); if (isWidgetCreationTracked()) { @@ -1071,6 +1097,7 @@ mixin WidgetInspectorService { return; } }, + registerExtension: registerExtension, ); _registerBoolServiceExtension( @@ -1091,12 +1118,12 @@ mixin WidgetInspectorService { renderObject.markNeedsPaint(); renderObject.visitChildren(markTreeNeedsPaint); } - final RenderObject root = RendererBinding.instance.renderView; - markTreeNeedsPaint(root); + RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint); } else { debugOnProfilePaint = null; } }, + registerExtension: registerExtension, ); } @@ -1106,6 +1133,7 @@ mixin WidgetInspectorService { disposeAllGroups(); return null; }, + registerExtension: registerExtension, ); _registerObjectGroupServiceExtension( name: WidgetInspectorServiceExtensions.disposeGroup.name, @@ -1113,10 +1141,12 @@ mixin WidgetInspectorService { disposeGroup(name); return null; }, + registerExtension: registerExtension, ); _registerSignalServiceExtension( name: WidgetInspectorServiceExtensions.isWidgetTreeReady.name, callback: isWidgetTreeReady, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.disposeId.name, @@ -1124,6 +1154,7 @@ mixin WidgetInspectorService { disposeId(objectId, objectGroup); return null; }, + registerExtension: registerExtension, ); _registerServiceExtensionVarArgs( name: WidgetInspectorServiceExtensions.setPubRootDirectories.name, @@ -1131,6 +1162,7 @@ mixin WidgetInspectorService { setPubRootDirectories(args); return null; }, + registerExtension: registerExtension, ); _registerServiceExtensionVarArgs( name: WidgetInspectorServiceExtensions.addPubRootDirectories.name, @@ -1138,6 +1170,7 @@ mixin WidgetInspectorService { addPubRootDirectories(args); return null; }, + registerExtension: registerExtension, ); _registerServiceExtensionVarArgs( name: WidgetInspectorServiceExtensions.removePubRootDirectories.name, @@ -1145,49 +1178,60 @@ mixin WidgetInspectorService { removePubRootDirectories(args); return null; }, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.getPubRootDirectories.name, callback: pubRootDirectories, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.setSelectionById.name, callback: setSelectionById, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getParentChain.name, callback: _getParentChain, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getProperties.name, callback: _getProperties, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getChildren.name, callback: _getChildren, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getChildrenSummaryTree.name, callback: _getChildrenSummaryTree, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getChildrenDetailsSubtree.name, callback: _getChildrenDetailsSubtree, + registerExtension: registerExtension, ); _registerObjectGroupServiceExtension( name: WidgetInspectorServiceExtensions.getRootWidget.name, callback: _getRootWidget, + registerExtension: registerExtension, ); _registerObjectGroupServiceExtension( name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name, callback: _getRootWidgetSummaryTree, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name, callback: _getRootWidgetSummaryTreeWithPreviews, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.getDetailsSubtree.name, @@ -1202,19 +1246,23 @@ mixin WidgetInspectorService { ), }; }, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getSelectedWidget.name, callback: _getSelectedWidget, + registerExtension: registerExtension, ); _registerServiceExtensionWithArg( name: WidgetInspectorServiceExtensions.getSelectedSummaryWidget.name, callback: _getSelectedSummaryWidget, + registerExtension: registerExtension, ); _registerSignalServiceExtension( name: WidgetInspectorServiceExtensions.isWidgetCreationTracked.name, callback: isWidgetCreationTracked, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.screenshot.name, @@ -1242,22 +1290,27 @@ mixin WidgetInspectorService { 'result': base64.encoder.convert(Uint8List.view(byteData!.buffer)), }; }, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.getLayoutExplorerNode.name, callback: _getLayoutExplorerNode, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.setFlexFit.name, callback: _setFlexFit, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.setFlexFactor.name, callback: _setFlexFactor, + registerExtension: registerExtension, ); registerServiceExtension( name: WidgetInspectorServiceExtensions.setFlexProperties.name, callback: _setFlexProperties, + registerExtension: registerExtension, ); } @@ -1524,18 +1577,7 @@ mixin WidgetInspectorService { selection.current = object! as RenderObject; _sendInspectEvent(selection.current); } - if (selectionChangedCallback != null) { - if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { - selectionChangedCallback!(); - } else { - // It isn't safe to trigger the selection change callback if we are in - // the middle of rendering the frame. - SchedulerBinding.instance.scheduleTask( - selectionChangedCallback!, - Priority.touch, - ); - } - } + return true; } return false; @@ -2065,24 +2107,35 @@ mixin WidgetInspectorService { summaryTree: true, subtreeDepth: subtreeDepth, service: this, - addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate delegate) { + addAdditionalPropertiesCallback: + (DiagnosticsNode node, InspectorSerializationDelegate delegate) { final Object? value = node.value; - final RenderObject? renderObject = value is Element ? value.renderObject : null; + final RenderObject? renderObject = + value is Element ? value.renderObject : null; if (renderObject == null) { return const {}; } - final DiagnosticsSerializationDelegate renderObjectSerializationDelegate = delegate.copyWith( + final DiagnosticsSerializationDelegate + renderObjectSerializationDelegate = delegate.copyWith( subtreeDepth: 0, includeProperties: true, expandPropertyValues: false, ); final Map additionalJson = { - 'renderObject': renderObject.toDiagnosticsNode().toJsonMap(renderObjectSerializationDelegate), + // Only include renderObject properties separately if this value is not already the renderObject. + // Only include if we are expanding property values to mitigate the risk of infinite loops if + // RenderObjects have properties that are Element objects. + if (value is! RenderObject && delegate.expandPropertyValues) + 'renderObject': renderObject + .toDiagnosticsNode() + .toJsonMap(renderObjectSerializationDelegate), }; final RenderObject? renderParent = renderObject.parent; - if (renderParent is RenderObject && subtreeDepth > 0) { + if (renderParent != null && + delegate.subtreeDepth > 0 && + delegate.expandPropertyValues) { final Object? parentCreator = renderParent.debugCreator; if (parentCreator is DebugCreator) { additionalJson['parentRenderElement'] = @@ -2103,7 +2156,7 @@ mixin WidgetInspectorService { if (!renderObject.debugNeedsLayout) { // ignore: invalid_use_of_protected_member final Constraints constraints = renderObject.constraints; - final MapconstraintsProperty = { + final Map constraintsProperty = { 'type': constraints.runtimeType.toString(), 'description': constraints.toString(), }; @@ -2613,8 +2666,6 @@ class _WidgetForTypeTests extends Widget { /// bottom left corner of the application switches back to select mode. class WidgetInspector extends StatefulWidget { /// Creates a widget that enables inspection for the child. - /// - /// The [child] argument must not be null. const WidgetInspector({ super.key, required this.child, @@ -2637,18 +2688,13 @@ class WidgetInspector extends StatefulWidget { class _WidgetInspectorState extends State with WidgetsBindingObserver { - _WidgetInspectorState() : selection = WidgetInspectorService.instance.selection; + _WidgetInspectorState(); Offset? _lastPointerLocation; - final InspectorSelection selection; + late InspectorSelection selection; - /// Whether the inspector is in select mode. - /// - /// In select mode, pointer interactions trigger widget selection instead of - /// normal interactions. Otherwise the previously selected widget is - /// highlighted but the application can be interacted with normally. - bool isSelectMode = true; + late bool isSelectMode; final GlobalKey _ignorePointerKey = GlobalKey(); @@ -2656,28 +2702,32 @@ class _WidgetInspectorState extends State /// as selecting the edge of the bounding box. static const double _edgeHitMargin = 2.0; - InspectorSelectionChangedCallback? _selectionChangedCallback; @override void initState() { super.initState(); - _selectionChangedCallback = () { - setState(() { - // The [selection] property which the build method depends on has - // changed. - }); - }; - WidgetInspectorService.instance.selectionChangedCallback = _selectionChangedCallback; + WidgetInspectorService.instance.selection + .addListener(_selectionInformationChanged); + WidgetInspectorService.instance.isSelectMode + .addListener(_selectionInformationChanged); + selection = WidgetInspectorService.instance.selection; + isSelectMode = WidgetInspectorService.instance.isSelectMode.value; } @override void dispose() { - if (WidgetInspectorService.instance.selectionChangedCallback == _selectionChangedCallback) { - WidgetInspectorService.instance.selectionChangedCallback = null; - } + WidgetInspectorService.instance.selection + .removeListener(_selectionInformationChanged); + WidgetInspectorService.instance.isSelectMode + .removeListener(_selectionInformationChanged); super.dispose(); } + void _selectionInformationChanged() => setState((){ + selection = WidgetInspectorService.instance.selection; + isSelectMode = WidgetInspectorService.instance.isSelectMode.value; + }); + bool _hitTestHelper( List hits, List edgeHits, @@ -2764,9 +2814,7 @@ class _WidgetInspectorState extends State final RenderObject userRender = ignorePointer.child!; final List selected = hitTest(position, userRender); - setState(() { - selection.candidates = selected; - }); + selection.candidates = selected; } void _handlePanDown(DragDownDetails event) { @@ -2788,9 +2836,7 @@ class _WidgetInspectorState extends State final ui.FlutterView view = View.of(context); final Rect bounds = (Offset.zero & (view.physicalSize / view.devicePixelRatio)).deflate(_kOffScreenMargin); if (!bounds.contains(_lastPointerLocation!)) { - setState(() { - selection.clear(); - }); + selection.clear(); } } @@ -2802,18 +2848,15 @@ class _WidgetInspectorState extends State _inspectAt(_lastPointerLocation!); WidgetInspectorService.instance._sendInspectEvent(selection.current); } - setState(() { - // Only exit select mode if there is a button to return to select mode. - if (widget.selectButtonBuilder != null) { - isSelectMode = false; - } - }); + + // Only exit select mode if there is a button to return to select mode. + if (widget.selectButtonBuilder != null) { + WidgetInspectorService.instance.isSelectMode.value = false; + } } void _handleEnableSelect() { - setState(() { - isSelectMode = true; - }); + WidgetInspectorService.instance.isSelectMode.value = true; } @override @@ -2847,7 +2890,14 @@ class _WidgetInspectorState extends State } /// Mutable selection state of the inspector. -class InspectorSelection { +class InspectorSelection with ChangeNotifier { + /// Creates an instance of [InspectorSelection]. + InspectorSelection() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + /// Render objects that are candidates to be selected. /// /// Tools may wish to iterate through the list of candidates. @@ -2886,6 +2936,7 @@ class InspectorSelection { if (_current != value) { _current = value; _currentElement = (value?.debugCreator as DebugCreator?)?.element; + notifyListeners(); } } @@ -2903,11 +2954,13 @@ class InspectorSelection { if (element?.debugIsDefunct ?? false) { _currentElement = null; _current = null; + notifyListeners(); return; } if (currentElement != element) { _currentElement = element; _current = element!.findRenderObject(); + notifyListeners(); } } @@ -2915,9 +2968,11 @@ class InspectorSelection { if (_index < candidates.length) { _current = candidates[index]; _currentElement = (_current?.debugCreator as DebugCreator?)?.element; + notifyListeners(); } else { _current = null; _currentElement = null; + notifyListeners(); } } @@ -2945,7 +3000,6 @@ class _InspectorOverlay extends LeafRenderObjectWidget { } class _RenderInspectorOverlay extends RenderBox { - /// The arguments must not be null. _RenderInspectorOverlay({ required InspectorSelection selection }) : _selection = selection; @@ -3196,7 +3250,10 @@ class _InspectorOverlayLayer extends Layer { Rect targetRect, ) { canvas.save(); - final double maxWidth = size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding); + final double maxWidth = math.max( + size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding), + 0, + ); final TextSpan? textSpan = _textPainter?.text as TextSpan?; if (_textPainter == null || textSpan!.text != message || _textPainterMaxWidth != maxWidth) { _textPainterMaxWidth = maxWidth; @@ -3484,16 +3541,12 @@ Iterable _describeRelevantUserCode( /// /// The [value] for this property is a string representation of the Flutter /// DevTools url. -/// -/// Properties `description` and `url` must not be null. class DevToolsDeepLinkProperty extends DiagnosticsProperty { /// Creates a diagnostics property that displays a deep link to Flutter DevTools. /// /// The [value] of this property will return a map of data for the Flutter /// DevTools deep link, including the full `url`, the Flutter DevTools `screenId`, /// and the `objectId` in Flutter DevTools that this diagnostic references. - /// - /// The `description` and `url` arguments must not be null. DevToolsDeepLinkProperty(String description, String url) : super('', url, description: description, level: DiagnosticLevel.info); } diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart index e15ace6655a42..5ec5e99cd2b7f 100644 --- a/packages/flutter/lib/src/widgets/widget_span.dart +++ b/packages/flutter/lib/src/widgets/widget_span.dart @@ -10,6 +10,8 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'framework.dart'; +const double _kEngineDefaultFontSize = 14.0; + // Examples can assume: // late WidgetSpan myWidgetSpan; @@ -67,10 +69,10 @@ import 'framework.dart'; class WidgetSpan extends PlaceholderSpan { /// Creates a [WidgetSpan] with the given values. /// - /// The [child] property must be non-null. [WidgetSpan] is a leaf node in - /// the [InlineSpan] tree. Child widgets are constrained by the width of the - /// paragraph they occupy. Child widget heights are unconstrained, and may - /// cause the text to overflow and be ellipsized/truncated. + /// [WidgetSpan] is a leaf node in the [InlineSpan] tree. Child widgets are + /// constrained by the width of the paragraph they occupy. Child widget + /// heights are unconstrained, and may cause the text to overflow and be + /// ellipsized/truncated. /// /// A [TextStyle] may be provided with the [style] property, but only the /// decoration, foreground, background, and spacing options will be used. @@ -90,17 +92,28 @@ class WidgetSpan extends PlaceholderSpan { /// Helper function for extracting [WidgetSpan]s in preorder, from the given /// [InlineSpan] as a list of widgets. /// - /// The `textScaleFactor` is the the number of font pixels for each logical - /// pixel. + /// The `textScaler` is the scaling strategy for scaling the content. /// /// This function is used by [EditableText] and [RichText] so calling it /// directly is rarely necessary. - static List extractFromInlineSpan(InlineSpan span, double textScaleFactor) { + static List extractFromInlineSpan(InlineSpan span, TextScaler textScaler) { final List widgets = []; + // _kEngineDefaultFontSize is the default font size to use when none of the + // ancestor spans specifies one. + final List fontSizeStack = [_kEngineDefaultFontSize]; int index = 0; // This assumes an InlineSpan tree's logical order is equivalent to preorder. - span.visitChildren((InlineSpan span) { + bool visitSubtree(InlineSpan span) { + final double? fontSizeToPush = switch (span.style?.fontSize) { + final double size when size != fontSizeStack.last => size, + _ => null, + }; + if (fontSizeToPush != null) { + fontSizeStack.add(fontSizeToPush); + } if (span is WidgetSpan) { + final double fontSize = fontSizeStack.last; + final double textScaleFactor = fontSize == 0 ? 0 : textScaler.scale(fontSize) / fontSize; widgets.add( _WidgetSpanParentData( span: span, @@ -115,8 +128,15 @@ class WidgetSpan extends PlaceholderSpan { span is WidgetSpan || span is! PlaceholderSpan, '$span is a PlaceholderSpan but not a WidgetSpan subclass. This is currently not supported.', ); + span.visitDirectChildren(visitSubtree); + if (fontSizeToPush != null) { + final double poppedFontSize = fontSizeStack.removeLast(); + assert(fontSizeStack.isNotEmpty); + assert(poppedFontSize == fontSizeToPush); + } return true; - }); + } + visitSubtree(span); return widgets; } @@ -130,14 +150,17 @@ class WidgetSpan extends PlaceholderSpan { /// in-order mapping of widget to laid-out dimensions. If no such dimension /// is provided, the widget will be skipped. /// - /// The `textScaleFactor` will be applied to the laid-out size of the widget. + /// The `textScaler` will be applied to the laid-out size of the widget. @override - void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List? dimensions }) { + void build(ui.ParagraphBuilder builder, { + TextScaler textScaler = TextScaler.noScaling, + List? dimensions, + }) { assert(debugAssertIsValid()); assert(dimensions != null); final bool hasStyle = style != null; if (hasStyle) { - builder.pushStyle(style!.getTextStyle(textScaleFactor: textScaleFactor)); + builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); } assert(builder.placeholderCount < dimensions!.length); final PlaceholderDimensions currentDimensions = dimensions![builder.placeholderCount]; @@ -267,7 +290,7 @@ class _WidgetSpanParentData extends ParentDataWidget { } @override - Type get debugTypicalAncestorWidgetClass => RenderInlineChildrenContainerDefaults; + Type get debugTypicalAncestorWidgetClass => RichText; } // A RenderObjectWidget that automatically applies text scaling on inline diff --git a/packages/flutter/lib/src/widgets/will_pop_scope.dart b/packages/flutter/lib/src/widgets/will_pop_scope.dart index ab90c7f49de01..81b59454861c3 100644 --- a/packages/flutter/lib/src/widgets/will_pop_scope.dart +++ b/packages/flutter/lib/src/widgets/will_pop_scope.dart @@ -9,26 +9,23 @@ import 'routes.dart'; /// Registers a callback to veto attempts by the user to dismiss the enclosing /// [ModalRoute]. /// -/// {@tool dartpad} -/// Whenever the back button is pressed, you will get a callback at [onWillPop], -/// which returns a [Future]. If the [Future] returns true, the screen is -/// popped. -/// -/// ** See code in examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart ** -/// {@end-tool} -/// /// See also: /// /// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback], /// which this widget uses to register and unregister [onWillPop]. /// * [Form], which provides an `onWillPop` callback that enables the form /// to veto a `pop` initiated by the app's back button. -/// +@Deprecated( + 'Use PopScope instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', +) class WillPopScope extends StatefulWidget { /// Creates a widget that registers a callback to veto attempts by the user to /// dismiss the enclosing [ModalRoute]. - /// - /// The [child] argument must not be null. + @Deprecated( + 'Use PopScope instead. ' + 'This feature was deprecated after v3.12.0-1.0.pre.', + ) const WillPopScope({ super.key, required this.child, diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index fd2cf457c4110..cc188608a8519 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -18,6 +18,7 @@ export 'package:vector_math/vector_math_64.dart' show Matrix4; export 'foundation.dart' show UniqueKey; export 'rendering.dart' show TextSelectionHandleType; export 'src/widgets/actions.dart'; +export 'src/widgets/adapter.dart'; export 'src/widgets/animated_cross_fade.dart'; export 'src/widgets/animated_scroll_view.dart'; export 'src/widgets/animated_size.dart'; @@ -80,6 +81,7 @@ export 'src/widgets/media_query.dart'; export 'src/widgets/modal_barrier.dart'; export 'src/widgets/navigation_toolbar.dart'; export 'src/widgets/navigator.dart'; +export 'src/widgets/navigator_pop_handler.dart'; export 'src/widgets/nested_scroll_view.dart'; export 'src/widgets/notification_listener.dart'; export 'src/widgets/orientation_builder.dart'; @@ -94,6 +96,7 @@ export 'src/widgets/placeholder.dart'; export 'src/widgets/platform_menu_bar.dart'; export 'src/widgets/platform_selectable_region_context_menu.dart'; export 'src/widgets/platform_view.dart'; +export 'src/widgets/pop_scope.dart'; export 'src/widgets/preferred_size.dart'; export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/raw_keyboard_listener.dart'; @@ -133,13 +136,13 @@ export 'src/widgets/sliver_fill.dart'; export 'src/widgets/sliver_layout_builder.dart'; export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_prototype_extent_list.dart'; +export 'src/widgets/sliver_varied_extent_list.dart'; export 'src/widgets/slotted_render_object_widget.dart'; export 'src/widgets/snapshot_widget.dart'; export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/table.dart'; -export 'src/widgets/tap_and_drag_gestures.dart'; export 'src/widgets/tap_region.dart'; export 'src/widgets/text.dart'; export 'src/widgets/text_editing_intents.dart'; diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index eb0f026887878..1431dd7eaf0dd 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -3,16 +3,16 @@ description: A framework for writing Flutter applications homepage: https://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". characters: 1.3.0 - collection: 1.17.2 + collection: 1.18.0 material_color_utilities: 0.5.0 - meta: 1.9.1 + meta: 1.10.0 vector_math: 2.1.4 - web: 0.1.4-beta + web: 0.3.0 sky_engine: sdk: flutter @@ -22,11 +22,11 @@ dev_dependencies: flutter_goldens: sdk: flutter fake_async: 1.3.1 - leak_tracker: 7.0.4 - leak_tracker_testing: 1.0.0 + leak_tracker: 9.0.7 + leak_tracker_flutter_testing: 1.0.5 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -42,13 +42,14 @@ dev_dependencies: intl: 0.18.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker_testing: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -59,18 +60,18 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test: 1.24.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test: 1.24.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 155f +# PUBSPEC CHECKSUM: 45cb diff --git a/packages/flutter/test/animation/animation_sheet_test.dart b/packages/flutter/test/animation/animation_sheet_test.dart index 4c26669abec4f..c04bb535c5a38 100644 --- a/packages/flutter/test/animation/animation_sheet_test.dart +++ b/packages/flutter/test/animation/animation_sheet_test.dart @@ -7,12 +7,10 @@ @Tags(['reduced-test-set']) library; -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../foundation/leak_tracking.dart'; void main() { /* @@ -20,8 +18,26 @@ void main() { * because [matchesGoldenFile] does not use Skia Gold in its native package. */ - testWidgetsWithLeakTracking('correctly records frames using collate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('recording disposes images', + (WidgetTester tester) async { final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size); + addTearDown(builder.dispose); + + await tester.pumpFrames( + builder.record( + const _DecuplePixels(Duration(seconds: 1)), + ), + const Duration(milliseconds: 200), + const Duration(milliseconds: 100), + ); + }, + skip: isBrowser, // [intended] https://github.com/flutter/flutter/issues/56001 + ); + + testWidgetsWithLeakTracking('correctly records frames using collate', + (WidgetTester tester) async { + final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size); + addTearDown(builder.dispose); await tester.pumpFrames( builder.record( @@ -48,20 +64,20 @@ void main() { const Duration(milliseconds: 100), ); - final ui.Image image = await builder.collate(5); - await expectLater( - image, + builder.collate(5), matchesGoldenFile('test.animation_sheet_builder.collate.png'), ); - image.dispose(); - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + }, + skip: isBrowser, // [intended] https://github.com/flutter/flutter/issues/56001 + ); // https://github.com/flutter/flutter/issues/56001 testWidgetsWithLeakTracking('use allLayers to record out-of-subtree contents', (WidgetTester tester) async { final AnimationSheetBuilder builder = AnimationSheetBuilder( frameSize: const Size(8, 2), allLayers: true, ); + addTearDown(builder.dispose); // The `record` (sized 8, 2) is placed on top of `_DecuplePixels` // (sized 12, 3), aligned at its top left. @@ -82,14 +98,13 @@ void main() { const Duration(milliseconds: 100), ); - final ui.Image image = await builder.collate(5); - await expectLater( - image, + builder.collate(5), matchesGoldenFile('test.animation_sheet_builder.out_of_tree.png'), ); - image.dispose(); - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + }, + skip: isBrowser, // [intended] https://github.com/flutter/flutter/issues/56001 + ); } // An animation of a yellow pixel moving from left to right, in a container of diff --git a/packages/flutter/test/animation/curves_test.dart b/packages/flutter/test/animation/curves_test.dart index 62f426870f55e..f7b0757fd5d8e 100644 --- a/packages/flutter/test/animation/curves_test.dart +++ b/packages/flutter/test/animation/curves_test.dart @@ -305,6 +305,28 @@ void main() { expect(() { CatmullRomSpline(const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], tension: 2.0); }, throwsAssertionError); + expect(() { + CatmullRomSpline( + const [Offset(double.infinity, 0.0), Offset.zero, Offset.zero, Offset.zero], + ).generateSamples(); + }, throwsAssertionError); + expect(() { + CatmullRomSpline( + const [Offset(0.0, double.infinity), Offset.zero, Offset.zero, Offset.zero], + ).generateSamples(); + }, throwsAssertionError); + expect(() { + CatmullRomSpline( + startHandle: const Offset(0.0, double.infinity), + const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], + ).generateSamples(); + }, throwsAssertionError); + expect(() { + CatmullRomSpline( + endHandle: const Offset(0.0, double.infinity), + const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], + ).generateSamples(); + }, throwsAssertionError); }); test('CatmullRomSpline interpolates values properly when precomputed', () { @@ -353,6 +375,24 @@ void main() { expect(() { CatmullRomSpline.precompute(const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], tension: 2.0); }, throwsAssertionError); + expect(() { + CatmullRomSpline.precompute(const [Offset(double.infinity, 0.0), Offset.zero, Offset.zero, Offset.zero]); + }, throwsAssertionError); + expect(() { + CatmullRomSpline.precompute(const [Offset(0.0, double.infinity), Offset.zero, Offset.zero, Offset.zero]); + }, throwsAssertionError); + expect(() { + CatmullRomSpline.precompute( + startHandle: const Offset(0.0, double.infinity), + const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], + ); + }, throwsAssertionError); + expect(() { + CatmullRomSpline.precompute( + endHandle: const Offset(0.0, double.infinity), + const [Offset.zero, Offset.zero, Offset.zero, Offset.zero], + ); + }, throwsAssertionError); }); test('CatmullRomCurve interpolates given points correctly', () { diff --git a/packages/flutter/test/animation/futures_test.dart b/packages/flutter/test/animation/futures_test.dart index 5a33543024d29..6ffee8e16445a 100644 --- a/packages/flutter/test/animation/futures_test.dart +++ b/packages/flutter/test/animation/futures_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/animation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('awaiting animation controllers - using direct future', (WidgetTester tester) async { diff --git a/packages/flutter/test/animation/iteration_patterns_test.dart b/packages/flutter/test/animation/iteration_patterns_test.dart index 42b4e916da476..0705333d3f99a 100644 --- a/packages/flutter/test/animation/iteration_patterns_test.dart +++ b/packages/flutter/test/animation/iteration_patterns_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { setUp(() { diff --git a/packages/flutter/test/animation/live_binding_test.dart b/packages/flutter/test/animation/live_binding_test.dart index 4928e6a6762f0..751f0886a47f6 100644 --- a/packages/flutter/test/animation/live_binding_test.dart +++ b/packages/flutter/test/animation/live_binding_test.dart @@ -7,12 +7,9 @@ @Tags(['reduced-test-set']) library; -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { /* @@ -24,6 +21,7 @@ void main() { testWidgetsWithLeakTracking('Should show event indicator for pointer events', (WidgetTester tester) async { final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true); + addTearDown(animationSheet.dispose); final List taps = []; Widget target({bool recording = true}) => Container( padding: const EdgeInsets.fromLTRB(20, 10, 25, 20), @@ -82,6 +80,7 @@ void main() { testWidgetsWithLeakTracking('Should show event indicator for pointer events with setSurfaceSize', (WidgetTester tester) async { final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true); + addTearDown(animationSheet.dispose); final List taps = []; Widget target({bool recording = true}) => Container( padding: const EdgeInsets.fromLTRB(20, 10, 25, 20), @@ -132,12 +131,13 @@ void main() { await tester.pumpFrames(target(), const Duration(milliseconds: 50)); expect(taps, isEmpty); - final ui.Image image = await animationSheet.collate(6); - await expectLater( - image, + animationSheet.collate(6), matchesGoldenFile('LiveBinding.press.animation.2.png'), ); - image.dispose(); - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + }, + skip: isBrowser, // [intended] https://github.com/flutter/flutter/issues/56001 + // TODO(polina-c): remove after fixing https://github.com/flutter/flutter/issues/133071 + leakTrackingTestConfig: const LeakTrackingTestConfig(allowAllNotDisposed: true), + ); } diff --git a/packages/flutter/test/cupertino/action_sheet_test.dart b/packages/flutter/test/cupertino/action_sheet_test.dart index b7ef4cde77a39..0dffe2d74bc0d 100644 --- a/packages/flutter/test/cupertino/action_sheet_test.dart +++ b/packages/flutter/test/cupertino/action_sheet_test.dart @@ -9,11 +9,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Verify that a tap on modal barrier dismisses an action sheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a tap on modal barrier dismisses an action sheet', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( const CupertinoActionSheet( @@ -32,7 +33,7 @@ void main() { expect(find.text('Action Sheet'), findsNothing); }); - testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( const CupertinoActionSheet( @@ -52,7 +53,7 @@ void main() { expect(find.text('Action Sheet'), findsOneWidget); }); - testWidgets('Action sheet destructive text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet destructive text style', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( CupertinoActionSheetAction( @@ -73,7 +74,7 @@ void main() { )); }); - testWidgets('Action sheet dark mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet dark mode', (WidgetTester tester) async { final Widget action = CupertinoActionSheetAction( child: const Text('action'), onPressed: () {}, @@ -130,7 +131,7 @@ void main() { ); }); - testWidgets('Action sheet default text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet default text style', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( CupertinoActionSheetAction( @@ -146,7 +147,7 @@ void main() { expect(widget.style.fontWeight, equals(FontWeight.w600)); }); - testWidgets('Action sheet text styles are correct when both title and message are included', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet text styles are correct when both title and message are included', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( const CupertinoActionSheet( @@ -166,7 +167,7 @@ void main() { expect(messageStyle.style.fontWeight, FontWeight.w400); }); - testWidgets('Action sheet text styles are correct when title but no message is included', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet text styles are correct when title but no message is included', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( const CupertinoActionSheet( @@ -183,7 +184,7 @@ void main() { expect(titleStyle.style.fontWeight, FontWeight.w400); }); - testWidgets('Action sheet text styles are correct when message but no title is included', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet text styles are correct when message but no title is included', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( const CupertinoActionSheet( @@ -200,8 +201,9 @@ void main() { expect(messageStyle.style.fontWeight, FontWeight.w600); }); - testWidgets('Content section but no actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Content section but no actions', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -237,8 +239,9 @@ void main() { ); }); - testWidgets('Actions but no content section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions but no content section', (WidgetTester tester) async { final ScrollController actionScrollController = ScrollController(); + addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -285,8 +288,9 @@ void main() { ); }); - testWidgets('Action section is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action section is scrollable', (WidgetTester tester) async { final ScrollController actionScrollController = ScrollController(); + addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( Builder(builder: (BuildContext context) { @@ -350,8 +354,9 @@ void main() { expect(tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Five')).height, equals(92.0)); }); - testWidgets('Content section is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Content section is scrollable', (WidgetTester tester) async { final ScrollController messageScrollController = ScrollController(); + addTearDown(messageScrollController.dispose); late double screenHeight; await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( @@ -392,7 +397,7 @@ void main() { expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight); }); - testWidgets('CupertinoActionSheet scrollbars controllers should be different', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoActionSheet scrollbars controllers should be different', (WidgetTester tester) async { // https://github.com/flutter/flutter/pull/81278 await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( @@ -422,7 +427,7 @@ void main() { expect(scrollbars[0].controller != scrollbars[1].controller, isTrue); }); - testWidgets('Tap on button calls onPressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap on button calls onPressed', (WidgetTester tester) async { bool wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( @@ -459,7 +464,7 @@ void main() { expect(find.text('One'), findsNothing); }); - testWidgets('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( Row( @@ -487,7 +492,7 @@ void main() { expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0); }); - testWidgets('Action sheet height is correct when given infinite vertical space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet height is correct when given infinite vertical space', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( Column( @@ -515,7 +520,7 @@ void main() { expect(tester.getSize(find.byType(CupertinoActionSheet)).height, moreOrLessEquals(132.33333333333334)); }); - testWidgets('1 action button with cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('1 action button with cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -542,7 +547,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0); }); - testWidgets('2 action buttons with cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('2 action buttons with cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -572,7 +577,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(112.33333333333331)); }); - testWidgets('3 action buttons with cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('3 action buttons with cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -606,7 +611,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(168.66666666666669)); }); - testWidgets('4+ action buttons with cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('4+ action buttons with cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -644,7 +649,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.33333333333337)); }); - testWidgets('1 action button without cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('1 action button without cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -666,7 +671,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, 56.0); }); - testWidgets('2+ action buttons without cancel button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('2+ action buttons without cancel button', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -692,7 +697,7 @@ void main() { expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.33333333333337)); }); - testWidgets('Action sheet with just cancel button is correct', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet with just cancel button is correct', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -712,7 +717,7 @@ void main() { expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0); }); - testWidgets('Cancel button tap calls onPressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cancel button tap calls onPressed', (WidgetTester tester) async { bool wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( @@ -747,7 +752,7 @@ void main() { expect(find.text('Cancel'), findsNothing); }); - testWidgets('Layout is correct when cancel button is present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Layout is correct when cancel button is present', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -784,7 +789,7 @@ void main() { expect(tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy, 526.0); }); - testWidgets('Enter/exit animation is correct', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Enter/exit animation is correct', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -861,7 +866,7 @@ void main() { expect(find.byType(CupertinoActionSheet), findsNothing); }); - testWidgets('Modal barrier is pressed during transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Modal barrier is pressed during transition', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( @@ -919,7 +924,7 @@ void main() { }); - testWidgets('Action sheet semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action sheet semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1028,9 +1033,10 @@ void main() { semantics.dispose(); }); - testWidgets('Conflicting scrollbars are not applied by ScrollBehavior to CupertinoActionSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Conflicting scrollbars are not applied by ScrollBehavior to CupertinoActionSheet', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/83819 final ScrollController actionScrollController = ScrollController(); + addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( Builder(builder: (BuildContext context) { @@ -1068,7 +1074,7 @@ void main() { expect(find.byType(CupertinoScrollbar), findsNWidgets(2)); }, variant: TargetPlatformVariant.all()); - testWidgets('Hovering over Cupertino action sheet action updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over Cupertino action sheet action updates cursor to clickable on Web', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesActionSheet( CupertinoActionSheet( diff --git a/packages/flutter/test/cupertino/activity_indicator_test.dart b/packages/flutter/test/cupertino/activity_indicator_test.dart index 3151fc1887ae7..1db90ea77ce7b 100644 --- a/packages/flutter/test/cupertino/activity_indicator_test.dart +++ b/packages/flutter/test/cupertino/activity_indicator_test.dart @@ -10,11 +10,10 @@ library; import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Activity indicator animate property works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activity indicator animate property works', (WidgetTester tester) async { await tester.pumpWidget(buildCupertinoActivityIndicator()); expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); @@ -30,7 +29,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); }); - testWidgets('Activity indicator dark mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activity indicator dark mode', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( @@ -79,7 +78,7 @@ void main() { ); }); - testWidgets('Activity indicator 0% in progress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activity indicator 0% in progress', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( @@ -101,7 +100,7 @@ void main() { ); }); - testWidgets('Activity indicator 30% in progress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activity indicator 30% in progress', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( @@ -123,7 +122,7 @@ void main() { ); }); - testWidgets('Activity indicator 100% in progress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activity indicator 100% in progress', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( @@ -144,7 +143,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/41345. - testWidgets('has the correct corner radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has the correct corner radius', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoActivityIndicator(animating: false, radius: 100), ); @@ -160,7 +159,7 @@ void main() { ); }); - testWidgets('Can specify color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can specify color', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( diff --git a/packages/flutter/test/cupertino/adaptive_text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/adaptive_text_selection_toolbar_test.dart index d0c6c1a57855f..fd5b2c93935d8 100644 --- a/packages/flutter/test/cupertino/adaptive_text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/adaptive_text_selection_toolbar_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/live_text_utils.dart'; @@ -30,7 +31,14 @@ void main() { ); }); - testWidgets('Builds the right toolbar on each platform, including web, and shows buttonItems', (WidgetTester tester) async { + Finder findOverflowNextButton() { + return find.byWidgetPredicate((Widget widget) => + widget is CustomPaint && + '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', + ); + } + + testWidgetsWithLeakTracking('Builds the right toolbar on each platform, including web, and shows buttonItems', (WidgetTester tester) async { const String buttonText = 'Click me'; await tester.pumpWidget( @@ -71,7 +79,7 @@ void main() { skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/108382 ); - testWidgets('Can build children directly as well', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can build children directly as well', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -94,17 +102,21 @@ void main() { skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/108382 ); - testWidgets('Can build from EditableTextState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can build from EditableTextState', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); await tester.pumpWidget(CupertinoApp( home: Align( alignment: Alignment.topLeft, child: SizedBox( width: 400, child: EditableText( - controller: TextEditingController(), + controller: controller, backgroundCursorColor: const Color(0xff00ffff), - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(), cursorColor: const Color(0xff00ffff), selectionControls: cupertinoTextSelectionHandleControls, @@ -153,7 +165,7 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('Can build for editable text from raw parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can build for editable text from raw parameters', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(CupertinoApp( home: Center( @@ -168,6 +180,9 @@ void main() { onPaste: () {}, onSelectAll: () {}, onLiveTextInput: () {}, + onLookUp: () {}, + onSearchWeb: () {}, + onShare: () {}, ), ), )); @@ -177,26 +192,30 @@ void main() { expect(find.text('Cut'), findsOneWidget); expect(find.text('Select All'), findsOneWidget); expect(find.text('Paste'), findsOneWidget); + expect(find.text('Look Up'), findsOneWidget); switch (defaultTargetPlatform) { case TargetPlatform.android: - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(6)); case TargetPlatform.fuchsia: - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(6)); case TargetPlatform.iOS: - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(6)); + expect(findOverflowNextButton(), findsOneWidget); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); + await tester.pumpAndSettle(); expect(findLiveTextButton(), findsOneWidget); case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: - expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(8)); } }, skip: kIsWeb, // [intended] on web the browser handles the context menu. variant: TargetPlatformVariant.all(), ); - testWidgets('Builds the correct button per-platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Builds the correct button per-platform', (WidgetTester tester) async { const String buttonText = 'Click me'; await tester.pumpWidget( diff --git a/packages/flutter/test/cupertino/app_test.dart b/packages/flutter/test/cupertino/app_test.dart index d61df5b38cb6d..ea384af7feba9 100644 --- a/packages/flutter/test/cupertino/app_test.dart +++ b/packages/flutter/test/cupertino/app_test.dart @@ -7,9 +7,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Heroes work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes work', (WidgetTester tester) async { await tester.pumpWidget(CupertinoApp( home: ListView(children: [ const Hero(tag: 'a', child: Text('foo')), @@ -39,7 +40,7 @@ void main() { expect(find.widgetWithText(Navigator, 'foo'), findsOneWidget); }); - testWidgets('Has default cupertino localizations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Has default cupertino localizations', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder( @@ -61,7 +62,7 @@ void main() { expect(find.text('Thu Oct 4 '), findsOneWidget); }); - testWidgets('Can use dynamic color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can use dynamic color', (WidgetTester tester) async { const CupertinoDynamicColor dynamicColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF000000), darkColor: Color(0xFF000001), @@ -83,7 +84,7 @@ void main() { expect(tester.widget(find.byType(Title)).color.value, 0xFF000001); }); - testWidgets('Can customize initial routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can customize initial routes', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( CupertinoApp( @@ -130,7 +131,7 @@ void main() { expect(find.text('regular page two'), findsNothing); }); - testWidgets('CupertinoApp.navigatorKey can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoApp.navigatorKey can be updated', (WidgetTester tester) async { final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>(); await tester.pumpWidget(CupertinoApp( navigatorKey: key1, @@ -146,12 +147,13 @@ void main() { expect(key1.currentState, isNull); }); - testWidgets('CupertinoApp.router works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoApp.router works', (WidgetTester tester) async { final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -163,6 +165,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); await tester.pumpWidget(CupertinoApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), @@ -175,9 +178,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('CupertinoApp.router route information parser is optional', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('CupertinoApp.router route information parser is optional', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -189,6 +197,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget(CupertinoApp.router( routerDelegate: delegate, @@ -200,9 +209,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('CupertinoApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('CupertinoApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -214,12 +228,14 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); await tester.pumpWidget(CupertinoApp.router( routeInformationProvider: provider, routerDelegate: delegate, @@ -227,7 +243,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('CupertinoApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -239,6 +255,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>(routerDelegate: delegate); await tester.pumpWidget(CupertinoApp.router( @@ -248,15 +265,19 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('CupertinoApp.router router config works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoApp.router router config works', (WidgetTester tester) async { + late SimpleNavigatorRouterDelegate delegate; + addTearDown(() => delegate.dispose()); + final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( + initialRouteInformation: RouteInformation( + uri: Uri.parse('initial'), + ), + ); + addTearDown(provider.dispose); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( - routeInformationProvider: PlatformRouteInformationProvider( - initialRouteInformation: RouteInformation( - uri: Uri.parse('initial'), - ), - ), + routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), - routerDelegate: SimpleNavigatorRouterDelegate( + routerDelegate: delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, @@ -279,9 +300,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('CupertinoApp has correct default ScrollBehavior', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('CupertinoApp has correct default ScrollBehavior', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( @@ -296,7 +322,7 @@ void main() { expect(ScrollConfiguration.of(capturedContext).runtimeType, CupertinoScrollBehavior); }); - testWidgets('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( @@ -314,7 +340,7 @@ void main() { expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); }); - testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { late BuildContext capturedContext; final UniqueKey uniqueKey = UniqueKey(); await tester.pumpWidget( @@ -334,7 +360,7 @@ void main() { expect(capturedContext.dependOnInheritedWidgetOfExactType<MediaQuery>()?.key, uniqueKey); }); - testWidgets('Text color is correctly resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text color is correctly resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { debugBrightnessOverride = Brightness.dark; await tester.pumpWidget( @@ -373,7 +399,7 @@ void main() { debugBrightnessOverride = null; }); - testWidgets('Cursor color is resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor color is resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { debugBrightnessOverride = Brightness.dark; RenderEditable findRenderEditable(WidgetTester tester) { @@ -394,6 +420,11 @@ void main() { return renderEditable!; } + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( @@ -405,8 +436,8 @@ void main() { return EditableText( backgroundCursorColor: DefaultSelectionStyle.of(context).selectionColor!, cursorColor: DefaultSelectionStyle.of(context).cursorColor!, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), ); }, @@ -425,7 +456,7 @@ void main() { debugBrightnessOverride = null; }); - testWidgets('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = CupertinoScrollBehavior(); late BuildContext capturedContext; diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index 04576a904d604..20c8b054aa976 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; import '../widgets/semantics_tester.dart'; @@ -29,7 +30,7 @@ Future<void> pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async Future<void> main() async { - testWidgets('Need at least 2 tabs', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Need at least 2 tabs', (WidgetTester tester) async { await expectLater( () => pumpWidgetWithBoilerplate(tester, CupertinoTabBar( items: <BottomNavigationBarItem>[ @@ -47,7 +48,7 @@ Future<void> main() async { ); }); - testWidgets('Active and inactive colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Active and inactive colors', (WidgetTester tester) async { await pumpWidgetWithBoilerplate(tester, MediaQuery( data: const MediaQueryData(), child: CupertinoTabBar( @@ -81,7 +82,7 @@ Future<void> main() async { }); - testWidgets('BottomNavigationBar.label will create a text widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.label will create a text widget', (WidgetTester tester) async { await pumpWidgetWithBoilerplate(tester, MediaQuery( data: const MediaQueryData(), child: CupertinoTabBar( @@ -103,7 +104,7 @@ Future<void> main() async { expect(find.text('Tab 2'), findsOneWidget); }); - testWidgets('Active and inactive colors dark mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Active and inactive colors dark mode', (WidgetTester tester) async { const CupertinoDynamicColor dynamicActiveColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF000000), darkColor: Color(0xFF000001), @@ -191,7 +192,7 @@ Future<void> main() async { expect(decoration2.border!.top.color.value, 0x29000000); }); - testWidgets('Tabs respects themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tabs respects themes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabBar( @@ -255,7 +256,7 @@ Future<void> main() async { expect(actualActive.text.style!.color, isSameColorAs(CupertinoColors.activeBlue.darkColor)); }); - testWidgets('Use active icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use active icon', (WidgetTester tester) async { final MemoryImage activeIcon = MemoryImage(Uint8List.fromList(kBlueSquarePng)); final MemoryImage inactiveIcon = MemoryImage(Uint8List.fromList(kTransparentImage)); @@ -288,7 +289,7 @@ Future<void> main() async { expect(image.image, activeIcon); }); - testWidgets('Adjusts height to account for bottom padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adjusts height to account for bottom padding', (WidgetTester tester) async { final CupertinoTabBar tabBar = CupertinoTabBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem( @@ -327,7 +328,7 @@ Future<void> main() async { expect(tester.getSize(find.byType(CupertinoTabBar)).height, 90.0); }); - testWidgets('Set custom height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Set custom height', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/51704 const double tabBarHeight = 56.0; final CupertinoTabBar tabBar = CupertinoTabBar( @@ -370,7 +371,7 @@ Future<void> main() async { expect(tester.getSize(find.byType(CupertinoTabBar)).height, tabBarHeight + bottomPadding); }); - testWidgets('Ensure bar height will not change when toggle keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ensure bar height will not change when toggle keyboard', (WidgetTester tester) async { const double tabBarHeight = 56.0; final CupertinoTabBar tabBar = CupertinoTabBar( height: tabBarHeight, @@ -424,7 +425,7 @@ Future<void> main() async { expect(tester.getSize(find.byType(CupertinoTabBar)).height, tabBarHeight + bottomPadding); }); - testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Opaque background does not add blur effects', (WidgetTester tester) async { await pumpWidgetWithBoilerplate(tester, MediaQuery( data: const MediaQueryData(), child: CupertinoTabBar( @@ -463,7 +464,7 @@ Future<void> main() async { expect(find.byType(BackdropFilter), findsNothing); }); - testWidgets('Tap callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap callback', (WidgetTester tester) async { late int callbackTab; await pumpWidgetWithBoilerplate(tester, MediaQuery( @@ -491,7 +492,7 @@ Future<void> main() async { expect(callbackTab, 1); }); - testWidgets('tabs announce semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tabs announce semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await pumpWidgetWithBoilerplate(tester, MediaQuery( @@ -526,7 +527,7 @@ Future<void> main() async { semantics.dispose(); }); - testWidgets('Label of items should be nullable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Label of items should be nullable', (WidgetTester tester) async { final MemoryImage iconProvider = MemoryImage(Uint8List.fromList(kTransparentImage)); final List<int> itemsTapped = <int>[]; @@ -563,7 +564,7 @@ Future<void> main() async { expect(itemsTapped, <int>[1]); }); - testWidgets('Hide border hides the top border of the tabBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hide border hides the top border of the tabBar', (WidgetTester tester) async { await pumpWidgetWithBoilerplate( tester, MediaQuery( @@ -623,7 +624,7 @@ Future<void> main() async { expect(boxDecorationHiddenBorder.border, isNull); }); - testWidgets('Hovering over tab bar item updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over tab bar item updates cursor to clickable on Web', (WidgetTester tester) async { await pumpWidgetWithBoilerplate( tester, MediaQuery( diff --git a/packages/flutter/test/cupertino/button_test.dart b/packages/flutter/test/cupertino/button_test.dart index e5e0cae05a4bb..23640455febff 100644 --- a/packages/flutter/test/cupertino/button_test.dart +++ b/packages/flutter/test/cupertino/button_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; @@ -17,7 +18,7 @@ const TextStyle testStyle = TextStyle( ); void main() { - testWidgets('Default layout minimum size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default layout minimum size', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(child: const CupertinoButton( onPressed: null, @@ -32,7 +33,7 @@ void main() { ); }); - testWidgets('Minimum size parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Minimum size parameter', (WidgetTester tester) async { const double minSize = 60.0; await tester.pumpWidget( boilerplate(child: const CupertinoButton( @@ -49,7 +50,7 @@ void main() { ); }); - testWidgets('Size grows with text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Size grows with text', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(child: const CupertinoButton( onPressed: null, @@ -67,7 +68,7 @@ void main() { // TODO(LongCatIsLoong): Uncomment once https://github.com/flutter/flutter/issues/44115 // is fixed. /* - testWidgets( + testWidgetsWithLeakTracking( 'CupertinoButton.filled default color contrast meets guideline', (WidgetTester tester) async { // The native color combination systemBlue text over white background fails @@ -102,7 +103,7 @@ void main() { }); */ - testWidgets('Button child alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button child alignment', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoButton( @@ -129,7 +130,7 @@ void main() { expect(align.alignment, Alignment.centerLeft); }); - testWidgets('Button with background is wider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button with background is wider', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: const CupertinoButton( onPressed: null, color: Color(0xFFFFFFFF), @@ -143,7 +144,7 @@ void main() { ); }); - testWidgets('Custom padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom padding', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: const CupertinoButton( onPressed: null, padding: EdgeInsets.all(100.0), @@ -156,7 +157,7 @@ void main() { ); }); - testWidgets('Button takes taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button takes taps', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( StatefulBuilder( @@ -184,7 +185,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); }); - testWidgets("Disabled button doesn't animate", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Disabled button doesn't animate", (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: const CupertinoButton( onPressed: null, child: Text('Tap me'), @@ -195,7 +196,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Enabled button animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Enabled button animates', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: CupertinoButton( child: const Text('Tap me'), onPressed: () { }, @@ -231,7 +232,7 @@ void main() { expect(transition.opacity.value, moreOrLessEquals(1.0, epsilon: 0.001)); }); - testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pressedOpacity defaults to 0.1', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: CupertinoButton( child: const Text('Tap me'), onPressed: () { }, @@ -250,7 +251,7 @@ void main() { expect(opacity.opacity.value, 0.4); }); - testWidgets('pressedOpacity parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pressedOpacity parameter', (WidgetTester tester) async { const double pressedOpacity = 0.5; await tester.pumpWidget(boilerplate(child: CupertinoButton( pressedOpacity: pressedOpacity, @@ -271,7 +272,7 @@ void main() { expect(opacity.opacity.value, pressedOpacity); }); - testWidgets('Cupertino button is semantically a button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cupertino button is semantically a button', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( boilerplate( @@ -302,7 +303,7 @@ void main() { semantics.dispose(); }); - testWidgets('Can specify colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can specify colors', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(child: CupertinoButton( color: const Color(0x000000FF), disabledColor: const Color(0x0000FF00), @@ -330,7 +331,7 @@ void main() { expect(boxDecoration.color, const Color(0x0000FF00)); }); - testWidgets('Can specify dynamic colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can specify dynamic colors', (WidgetTester tester) async { const Color bgColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF123456), darkColor: Color(0xFF654321), @@ -379,7 +380,7 @@ void main() { expect(boxDecoration.color!.value, 0xFF111111); }); - testWidgets('Button respects themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button respects themes', (WidgetTester tester) async { late TextStyle textStyle; await tester.pumpWidget( @@ -453,7 +454,7 @@ void main() { expect(decoration.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); }); - testWidgets('Hovering over Cupertino button updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over Cupertino button updates cursor to clickable on Web', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( diff --git a/packages/flutter/test/cupertino/checkbox_test.dart b/packages/flutter/test/cupertino/checkbox_test.dart index 6fc2a2f6f31ad..7f047e62cf7d3 100644 --- a/packages/flutter/test/cupertino/checkbox_test.dart +++ b/packages/flutter/test/cupertino/checkbox_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -16,7 +16,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('CupertinoCheckbox semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoCheckbox semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( @@ -156,7 +156,7 @@ void main() { handle.dispose(); }); - testWidgets('Can wrap CupertinoCheckbox with Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can wrap CupertinoCheckbox with Semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( @@ -184,7 +184,7 @@ void main() { handle.dispose(); }); - testWidgets('CupertinoCheckbox tristate: true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoCheckbox tristate: true', (WidgetTester tester) async { bool? checkBoxValue; await tester.pumpWidget( @@ -228,7 +228,7 @@ void main() { expect(checkBoxValue, null); }); - testWidgets('has semantics for tristate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantics for tristate', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( CupertinoApp( @@ -295,7 +295,7 @@ void main() { semantics.dispose(); }); - testWidgets('has semantic events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantic events', (WidgetTester tester) async { dynamic semanticEvent; bool? checkboxValue = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { @@ -335,7 +335,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp({bool enabled = true}) { @@ -372,7 +372,7 @@ void main() { expect(value, isTrue); }); - testWidgets('Checkbox respects shape and side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox respects shape and side', (WidgetTester tester) async { const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))); diff --git a/packages/flutter/test/cupertino/colors_test.dart b/packages/flutter/test/cupertino/colors_test.dart index 91e10b59ccfc0..a98a856a24d0d 100644 --- a/packages/flutter/test/cupertino/colors_test.dart +++ b/packages/flutter/test/cupertino/colors_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class DependentWidget extends StatelessWidget { const DependentWidget({ @@ -202,7 +201,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Dynamic colors that are not actually dynamic should not claim dependencies', (WidgetTester tester) async { await tester.pumpWidget(const DependentWidget(color: notSoDynamicColor1)); @@ -212,7 +211,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Dynamic colors that are only dependent on vibrancy should not claim unnecessary dependencies, ' 'and its resolved color should change when its dependency changes', (WidgetTester tester) async { @@ -256,7 +255,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Dynamic colors that are only dependent on accessibility contrast should not claim unnecessary dependencies, ' 'and its resolved color should change when its dependency changes', (WidgetTester tester) async { @@ -285,7 +284,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Dynamic colors that are only dependent on elevation level should not claim unnecessary dependencies, ' 'and its resolved color should change when its dependency changes', (WidgetTester tester) async { @@ -314,7 +313,7 @@ void main() { }, ); - testWidgets('Dynamic color with all 3 dependencies works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dynamic color with all 3 dependencies works', (WidgetTester tester) async { const Color dynamicRainbowColor1 = CupertinoDynamicColor( color: color0, darkColor: color1, @@ -415,7 +414,7 @@ void main() { expect(find.byType(DependentWidget), paints..rect(color: color7)); }); - testWidgets('CupertinoDynamicColor used in a CupertinoTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoDynamicColor used in a CupertinoTheme', (WidgetTester tester) async { late CupertinoDynamicColor color; await tester.pumpWidget( CupertinoApp( @@ -500,7 +499,7 @@ void main() { Color? color; setUp(() { color = null; }); - testWidgets('dynamic color works in cupertino override theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dynamic color works in cupertino override theme', (WidgetTester tester) async { CupertinoDynamicColor typedColor() => color! as CupertinoDynamicColor; await tester.pumpWidget( @@ -557,7 +556,7 @@ void main() { expect(typedColor().value, dynamicColor.darkHighContrastElevatedColor.value); }); - testWidgets('dynamic color does not work in a material theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dynamic color does not work in a material theme', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( // This will create a MaterialBasedCupertinoThemeData with primaryColor set to `dynamicColor`. diff --git a/packages/flutter/test/cupertino/context_menu_action_test.dart b/packages/flutter/test/cupertino/context_menu_action_test.dart index 26c8dd78ec1ab..535d502a69a94 100644 --- a/packages/flutter/test/cupertino/context_menu_action_test.dart +++ b/packages/flutter/test/cupertino/context_menu_action_test.dart @@ -7,8 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Constants taken from _ContextMenuActionState. @@ -73,7 +72,7 @@ void main() { return icon; } - testWidgets('responds to taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('responds to taps', (WidgetTester tester) async { bool wasPressed = false; await tester.pumpWidget(getApp(onPressed: () { wasPressed = true; @@ -84,7 +83,7 @@ void main() { expect(wasPressed, true); }); - testWidgets('turns grey when pressed and held', (WidgetTester tester) async { + testWidgetsWithLeakTracking('turns grey when pressed and held', (WidgetTester tester) async { await tester.pumpWidget(getApp()); expect(find.byType(CupertinoContextMenuAction), paints..rect(color: kBackgroundColor.color)); @@ -119,27 +118,27 @@ void main() { paints..rect(color: kBackgroundColor.darkColor)); }); - testWidgets('icon and textStyle colors are correct out of the box', + testWidgetsWithLeakTracking('icon and textStyle colors are correct out of the box', (WidgetTester tester) async { await tester.pumpWidget(getApp()); expect(getTextStyle(tester).color, CupertinoColors.label); expect(getIcon(tester).color, CupertinoColors.label); }); - testWidgets('icon and textStyle colors are correct for destructive actions', + testWidgetsWithLeakTracking('icon and textStyle colors are correct for destructive actions', (WidgetTester tester) async { await tester.pumpWidget(getApp(isDestructiveAction: true)); expect(getTextStyle(tester).color, kDestructiveActionColor); expect(getIcon(tester).color, kDestructiveActionColor); }); - testWidgets('textStyle is correct for defaultAction', + testWidgetsWithLeakTracking('textStyle is correct for defaultAction', (WidgetTester tester) async { await tester.pumpWidget(getApp(isDefaultAction: true)); expect(getTextStyle(tester).fontWeight, kDefaultActionWeight); }); - testWidgets( + testWidgetsWithLeakTracking( 'Hovering over Cupertino context menu action updates cursor to clickable on Web', (WidgetTester tester) async { /// Cupertino context menu action without "onPressed" callback. diff --git a/packages/flutter/test/cupertino/context_menu_test.dart b/packages/flutter/test/cupertino/context_menu_test.dart index 146b15192b6e0..c6874be188f83 100644 --- a/packages/flutter/test/cupertino/context_menu_test.dart +++ b/packages/flutter/test/cupertino/context_menu_test.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:clock/clock.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); @@ -119,7 +121,7 @@ void main() { } group('CupertinoContextMenu before and during opening', () { - testWidgets('An unopened CupertinoContextMenu renders child in the same place as without', (WidgetTester tester) async { + testWidgetsWithLeakTracking('An unopened CupertinoContextMenu renders child in the same place as without', (WidgetTester tester) async { // Measure the child in the scene with no CupertinoContextMenu. final Widget child = getChild(); await tester.pumpWidget( @@ -139,7 +141,7 @@ void main() { expect(tester.getRect(find.byWidget(child)), childRect); }); - testWidgets('Can open CupertinoContextMenu by tap and hold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can open CupertinoContextMenu by tap and hold', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); expect(find.byWidget(child), findsOneWidget); @@ -175,7 +177,7 @@ void main() { expect(findStatic(), findsOneWidget); }); - testWidgets('CupertinoContextMenu is in the correct position when within a nested navigator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoContextMenu is in the correct position when within a nested navigator', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( @@ -240,7 +242,7 @@ void main() { expect(findStatic(), findsOneWidget); }); - testWidgets('CupertinoContextMenu with a basic builder opens and closes the same as when providing a child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoContextMenu with a basic builder opens and closes the same as when providing a child', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getBuilderContextMenu(builder: (BuildContext context, Animation<double> animation) { return child; @@ -278,7 +280,7 @@ void main() { expect(findStatic(), findsOneWidget); }); - testWidgets('CupertinoContextMenu with a builder can change the animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoContextMenu with a builder can change the animation', (WidgetTester tester) async { await tester.pumpWidget(getBuilderContextMenu(builder: (BuildContext context, Animation<double> animation) { return Container( width: 300.0, @@ -318,7 +320,7 @@ void main() { expect(decoyLaterDecoration?.borderRadius, isNot(equals(BorderRadius.circular(0)))); }); - testWidgets('Hovering over Cupertino context menu updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over Cupertino context menu updates cursor to clickable on Web', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( @@ -349,7 +351,7 @@ void main() { ); }); - testWidgets('CupertinoContextMenu is in the correct position when within a Transform.scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoContextMenu is in the correct position when within a Transform.scale', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( @@ -407,7 +409,7 @@ void main() { }); group('CupertinoContextMenu when open', () { - testWidgets('Last action does not have border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Last action does not have border', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( @@ -466,7 +468,7 @@ void main() { expect(findStaticChildDecoration(tester), findsNWidgets(3)); }); - testWidgets('Can close CupertinoContextMenu by background tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can close CupertinoContextMenu by background tap', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); @@ -484,7 +486,7 @@ void main() { expect(findStatic(), findsNothing); }); - testWidgets('Can close CupertinoContextMenu by dragging down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can close CupertinoContextMenu by dragging down', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); @@ -526,7 +528,7 @@ void main() { expect(findStatic(), findsNothing); }); - testWidgets('Can close CupertinoContextMenu by flinging down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can close CupertinoContextMenu by flinging down', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); @@ -551,7 +553,7 @@ void main() { expect(findStatic(), findsNothing); }); - testWidgets("Backdrop is added using ModalRoute's filter parameter", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Backdrop is added using ModalRoute's filter parameter", (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); expect(find.byType(BackdropFilter), findsNothing); @@ -566,7 +568,7 @@ void main() { expect(find.byType(BackdropFilter), findsOneWidget); }); - testWidgets('Preview widget should have the correct border radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preview widget should have the correct border radius', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); @@ -584,7 +586,7 @@ void main() { expect(previewWidget.borderRadius, equals(BorderRadius.circular(12.0))); }); - testWidgets('CupertinoContextMenu width is correct', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoContextMenu width is correct', (WidgetTester tester) async { final Widget child = getChild(); await tester.pumpWidget(getContextMenu(child: child)); expect(find.byWidget(child), findsOneWidget); @@ -627,7 +629,7 @@ void main() { } }); - testWidgets("ContextMenu route animation doesn't throw exception on dismiss", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ContextMenu route animation doesn't throw exception on dismiss", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/124597. final List<int> items = List<int>.generate(2, (int index) => index).toList(); @@ -674,7 +676,7 @@ void main() { }); group("Open layout differs depending on child's position on screen", () { - testWidgets('Portrait', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Portrait', (WidgetTester tester) async { const Size portraitScreenSize = Size(600.0, 800.0); await binding.setSurfaceSize(portraitScreenSize); @@ -746,7 +748,7 @@ void main() { await binding.setSurfaceSize(const Size(800.0, 600.0)); }); - testWidgets('Landscape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Landscape', (WidgetTester tester) async { // Pump a CupertinoContextMenu in the center of the screen and open it. final Widget child = getChild(); await tester.pumpWidget(getContextMenu( @@ -810,4 +812,74 @@ void main() { expect(right.dx, lessThan(left.dx)); }); }); + + testWidgetsWithLeakTracking('Conflicting gesture detectors', (WidgetTester tester) async { + int? onPointerDownTime; + int? onPointerUpTime; + bool insideTapTriggered = false; + // The required duration of the route to be pushed in is [500, 900]ms. + // 500ms is calculated from kPressTimeout+_previewLongPressTimeout/2. + // 900ms is calculated from kPressTimeout+_previewLongPressTimeout. + const Duration pressDuration = Duration(milliseconds: 501); + + int now() => clock.now().millisecondsSinceEpoch; + + await tester.pumpWidget(Listener( + onPointerDown: (PointerDownEvent event) => onPointerDownTime = now(), + onPointerUp: (PointerUpEvent event) => onPointerUpTime = now(), + child: CupertinoApp( + home: Align( + child: CupertinoContextMenu( + actions: const <CupertinoContextMenuAction>[ + CupertinoContextMenuAction( + child: Text('CupertinoContextMenuAction'), + ), + ], + child: GestureDetector( + onTap: () => insideTapTriggered = true, + child: Container( + width: 200, + height: 200, + key: const Key('container'), + color: const Color(0xFF00FF00), + ), + ), + ), + ), + ), + )); + + // Start a press on the child. + final TestGesture gesture = await tester.createGesture(); + await gesture.down(tester.getCenter(find.byKey(const Key('container')))); + // Simulate the actual situation: + // the user keeps pressing and requesting frames. + // If there is only one frame, + // the animation is mutant and cannot drive the value of the animation controller. + for (int i = 0; i < 100; i++) { + await tester.pump(pressDuration ~/ 100); + } + await gesture.up(); + // Await pushing route. + await tester.pumpAndSettle(); + + // Judge whether _ContextMenuRouteStatic present on the screen. + final Finder routeStatic = find.byWidgetPredicate( + (Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic', + ); + + // The insideTap and the route should not be triggered at the same time. + if (insideTapTriggered) { + // Calculate the actual duration. + final int actualDuration = onPointerUpTime! - onPointerDownTime!; + + expect(routeStatic, findsNothing, + reason: 'When actualDuration($actualDuration) is in the range of 500ms~900ms, ' + 'which means the route is pushed, ' + 'but insideTap should not be triggered at the same time.'); + } else { + // The route should be pushed when the insideTap is not triggered. + expect(routeStatic, findsOneWidget); + } + }); } diff --git a/packages/flutter/test/cupertino/debug_test.dart b/packages/flutter/test/cupertino/debug_test.dart index 85dca9e404539..f392a5a59ffb5 100644 --- a/packages/flutter/test/cupertino/debug_test.dart +++ b/packages/flutter/test/cupertino/debug_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('debugCheckHasCupertinoLocalizations throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasCupertinoLocalizations throws', (WidgetTester tester) async { final GlobalKey noLocalizationsAvailable = GlobalKey(); final GlobalKey localizationsAvailable = GlobalKey(); diff --git a/packages/flutter/test/cupertino/desktop_text_selection_toolbar_button_test.dart b/packages/flutter/test/cupertino/desktop_text_selection_toolbar_button_test.dart index 4ca0433e3cf1e..107caba6f0e2f 100644 --- a/packages/flutter/test/cupertino/desktop_text_selection_toolbar_button_test.dart +++ b/packages/flutter/test/cupertino/desktop_text_selection_toolbar_button_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('can press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can press', (WidgetTester tester) async { bool pressed = false; await tester.pumpWidget( CupertinoApp( @@ -30,7 +31,7 @@ void main() { expect(pressed, true); }); - testWidgets('keeps contrast with background on hover', + testWidgetsWithLeakTracking('keeps contrast with background on hover', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( @@ -70,7 +71,7 @@ void main() { ); }); - testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pressedOpacity defaults to 0.1', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -114,7 +115,7 @@ void main() { expect(opacity.opacity.value, 1.0); }); - testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passing null to onPressed disables the button', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( diff --git a/packages/flutter/test/cupertino/desktop_text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/desktop_text_selection_toolbar_test.dart index c3519e6a8addf..3536d68de5863 100644 --- a/packages/flutter/test/cupertino/desktop_text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/desktop_text_selection_toolbar_test.dart @@ -7,11 +7,12 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('has correct backdrop filters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has correct backdrop filters', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -53,7 +54,7 @@ void main() { ); }); - testWidgets('has shadow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has shadow', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -83,7 +84,7 @@ void main() { ); }); - testWidgets('is translucent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is translucent', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -116,7 +117,7 @@ void main() { ); }); - testWidgets('positions itself at the anchor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at the anchor', (WidgetTester tester) async { // An arbitrary point on the screen to position at. const Offset anchor = Offset(30.0, 40.0); diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart index 64b15d38ca4ed..7ae06059ca5dd 100644 --- a/packages/flutter/test/cupertino/dialog_test.dart +++ b/packages/flutter/test/cupertino/dialog_test.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -345,11 +344,9 @@ void main() { // regular font. However, when using the test font, "Cancel" becomes 2 lines which // is why the height we're verifying for "Cancel" is larger than "OK". - // TODO(yjbanov): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - expect(tester.getSize(find.text('The Title')), equals(const Size(270.0, hasIssue99933 ? 133 : 132.0))); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getSize(find.text('The Title')), equals(const Size(270.0, 132.0))); + } expect(tester.getTopLeft(find.text('The Title')), equals(const Offset(265.0, 80.0 + 24.0))); expect(tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Cancel')), equals(const Size(310.0, 148.0))); expect(tester.getSize(find.widgetWithText(CupertinoDialogAction, 'OK')), equals(const Size(310.0, 98.0))); diff --git a/packages/flutter/test/cupertino/form_row_test.dart b/packages/flutter/test/cupertino/form_row_test.dart index 9cb079905a6dc..926f0002f9195 100644 --- a/packages/flutter/test/cupertino/form_row_test.dart +++ b/packages/flutter/test/cupertino/form_row_test.dart @@ -6,9 +6,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Shows prefix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows prefix', (WidgetTester tester) async { const Widget prefix = Text('Enter Value'); await tester.pumpWidget( @@ -25,7 +26,7 @@ void main() { expect(prefix, tester.widget(find.byType(Text))); }); - testWidgets('Shows child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows child', (WidgetTester tester) async { const Widget child = CupertinoTextField(); await tester.pumpWidget( @@ -41,7 +42,7 @@ void main() { expect(child, tester.widget(find.byType(CupertinoTextField))); }); - testWidgets('RTL puts prefix after child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RTL puts prefix after child', (WidgetTester tester) async { const Widget prefix = Text('Enter Value'); const Widget child = CupertinoTextField(); @@ -62,7 +63,7 @@ void main() { expect(tester.getTopLeft(find.byType(Text)).dx > tester.getTopLeft(find.byType(CupertinoTextField)).dx, true); }); - testWidgets('LTR puts child after prefix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LTR puts child after prefix', (WidgetTester tester) async { const Widget prefix = Text('Enter Value'); const Widget child = CupertinoTextField(); @@ -83,7 +84,7 @@ void main() { expect(tester.getTopLeft(find.byType(Text)).dx > tester.getTopLeft(find.byType(CupertinoTextField)).dx, false); }); - testWidgets('Shows error widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows error widget', (WidgetTester tester) async { const Widget error = Text('Error'); await tester.pumpWidget( @@ -100,7 +101,7 @@ void main() { expect(error, tester.widget(find.byType(Text))); }); - testWidgets('Shows helper widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows helper widget', (WidgetTester tester) async { const Widget helper = Text('Helper'); await tester.pumpWidget( @@ -117,7 +118,7 @@ void main() { expect(helper, tester.widget(find.byType(Text))); }); - testWidgets('Shows helper text above error text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows helper text above error text', (WidgetTester tester) async { const Widget helper = Text('Helper'); const Widget error = CupertinoActivityIndicator(); @@ -139,7 +140,7 @@ void main() { ); }); - testWidgets('Shows helper in label color and error text in red color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows helper in label color and error text in red color', (WidgetTester tester) async { const Widget helper = Text('Helper'); const Widget error = Text('Error'); @@ -166,7 +167,7 @@ void main() { expect(errorTextStyle.style.color, CupertinoColors.destructiveRed); }); - testWidgets('CupertinoFormRow adapts to MaterialApp dark mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoFormRow adapts to MaterialApp dark mode', (WidgetTester tester) async { const Widget prefix = Text('Prefix'); const Widget helper = Text('Helper'); diff --git a/packages/flutter/test/cupertino/form_section_test.dart b/packages/flutter/test/cupertino/form_section_test.dart index 7a9e4f0539b35..da86fb9e898c9 100644 --- a/packages/flutter/test/cupertino/form_section_test.dart +++ b/packages/flutter/test/cupertino/form_section_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Shows header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows header', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -22,7 +23,7 @@ void main() { expect(find.text('Header'), findsOneWidget); }); - testWidgets('Shows footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows footer', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -37,7 +38,7 @@ void main() { expect(find.text('Footer'), findsOneWidget); }); - testWidgets('Shows long dividers in edge-to-edge section part 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows long dividers in edge-to-edge section part 1', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -54,7 +55,7 @@ void main() { expect(childrenColumn.children.length, 3); }); - testWidgets('Shows long dividers in edge-to-edge section part 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shows long dividers in edge-to-edge section part 2', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -75,7 +76,7 @@ void main() { expect(childrenColumn.children.length, 5); }); - testWidgets('Does not show long dividers in insetGrouped section part 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not show long dividers in insetGrouped section part 1', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -93,7 +94,7 @@ void main() { expect(childrenColumn.children.length, 1); }); - testWidgets('Does not show long dividers in insetGrouped section part 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not show long dividers in insetGrouped section part 2', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( restorationScopeId: 'App', @@ -115,7 +116,7 @@ void main() { expect(childrenColumn.children.length, 3); }); - testWidgets('Sets background color for section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sets background color for section', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemBlue; await tester.pumpWidget( @@ -138,7 +139,7 @@ void main() { expect(boxDecoration.color, backgroundColor); }); - testWidgets('Setting clipBehavior clips children section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting clipBehavior clips children section', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -153,7 +154,7 @@ void main() { expect(find.byType(ClipRRect), findsOneWidget); }); - testWidgets('Not setting clipBehavior does not produce a RenderClipRRect object', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Not setting clipBehavior does not produce a RenderClipRRect object', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( diff --git a/packages/flutter/test/cupertino/icon_theme_data_test.dart b/packages/flutter/test/cupertino/icon_theme_data_test.dart index dda4b4b5c11ba..75fff4b62ea0a 100644 --- a/packages/flutter/test/cupertino/icon_theme_data_test.dart +++ b/packages/flutter/test/cupertino/icon_theme_data_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('IconTheme.of works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme.of works', (WidgetTester tester) async { const IconThemeData data = IconThemeData( size: 16.0, fill: 0.0, diff --git a/packages/flutter/test/cupertino/list_section_test.dart b/packages/flutter/test/cupertino/list_section_test.dart index 735c9614ca9e7..0119947b5b668 100644 --- a/packages/flutter/test/cupertino/list_section_test.dart +++ b/packages/flutter/test/cupertino/list_section_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('shows header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows header', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -23,7 +24,7 @@ void main() { expect(find.text('Header'), findsOneWidget); }); - testWidgets('shows footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows footer', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -40,7 +41,7 @@ void main() { expect(find.text('Footer'), findsOneWidget); }); - testWidgets('shows long dividers in edge-to-edge section part 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows long dividers in edge-to-edge section part 1', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -59,7 +60,7 @@ void main() { expect(childrenColumn.children.length, 3); }); - testWidgets('shows long dividers in edge-to-edge section part 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows long dividers in edge-to-edge section part 2', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -80,7 +81,7 @@ void main() { expect(childrenColumn.children.length, 5); }); - testWidgets('does not show long dividers in insetGrouped section part 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not show long dividers in insetGrouped section part 1', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -100,7 +101,7 @@ void main() { expect(childrenColumn.children.length, 1); }); - testWidgets('does not show long dividers in insetGrouped section part 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not show long dividers in insetGrouped section part 2', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -121,7 +122,7 @@ void main() { expect(childrenColumn.children.length, 3); }); - testWidgets('sets background color for section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sets background color for section', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemBlue; await tester.pumpWidget( @@ -144,7 +145,7 @@ void main() { expect(boxDecoration.color, backgroundColor); }); - testWidgets('setting clipBehavior clips children section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setting clipBehavior clips children section', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -161,7 +162,7 @@ void main() { expect(find.byType(ClipRRect), findsOneWidget); }); - testWidgets('not setting clipBehavior does not clip children section', (WidgetTester tester) async { + testWidgetsWithLeakTracking('not setting clipBehavior does not clip children section', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -177,7 +178,7 @@ void main() { expect(find.byType(ClipRRect), findsNothing); }); - testWidgets('CupertinoListSection respects separatorColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoListSection respects separatorColor', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -200,7 +201,7 @@ void main() { } }); - testWidgets('CupertinoListSection.separatorColor defaults CupertinoColors.separator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoListSection.separatorColor defaults CupertinoColors.separator', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( diff --git a/packages/flutter/test/cupertino/list_tile_test.dart b/packages/flutter/test/cupertino/list_tile_test.dart index 9ff90e82faadf..0501613bbde49 100644 --- a/packages/flutter/test/cupertino/list_tile_test.dart +++ b/packages/flutter/test/cupertino/list_tile_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('shows title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows title', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); await tester.pumpWidget( @@ -23,7 +24,7 @@ void main() { expect(find.text('CupertinoListTile'), findsOneWidget); }); - testWidgets('shows subtitle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows subtitle', (WidgetTester tester) async { const Widget subtitle = Text('CupertinoListTile subtitle'); await tester.pumpWidget( @@ -41,7 +42,7 @@ void main() { expect(find.text('CupertinoListTile subtitle'), findsOneWidget); }); - testWidgets('shows additionalInfo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows additionalInfo', (WidgetTester tester) async { const Widget additionalInfo = Text('Not Connected'); await tester.pumpWidget( @@ -59,7 +60,7 @@ void main() { expect(find.text('Not Connected'), findsOneWidget); }); - testWidgets('shows trailing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows trailing', (WidgetTester tester) async { const Widget trailing = CupertinoListTileChevron(); await tester.pumpWidget( @@ -76,7 +77,7 @@ void main() { expect(tester.widget<CupertinoListTileChevron>(find.byType(CupertinoListTileChevron)), trailing); }); - testWidgets('shows leading', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows leading', (WidgetTester tester) async { const Widget leading = Icon(CupertinoIcons.add); await tester.pumpWidget( @@ -93,7 +94,7 @@ void main() { expect(tester.widget<Icon>(find.byType(Icon)), leading); }); - testWidgets('sets backgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sets backgroundColor', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemRed; await tester.pumpWidget( @@ -118,7 +119,7 @@ void main() { expect(container.color, backgroundColor); }); - testWidgets('does not change backgroundColor when tapped if onTap is not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not change backgroundColor when tapped if onTap is not provided', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemBlue; const Color backgroundColorActivated = CupertinoColors.systemRed; @@ -148,7 +149,7 @@ void main() { expect(container.color, backgroundColor); }); - testWidgets('changes backgroundColor when tapped if onTap is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('changes backgroundColor when tapped if onTap is provided', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemBlue; const Color backgroundColorActivated = CupertinoColors.systemRed; @@ -187,7 +188,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('does not contain GestureDetector if onTap is not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not contain GestureDetector if onTap is not provided', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -208,7 +209,7 @@ void main() { expect(find.byType(GestureDetector), findsNothing); }); - testWidgets('contains GestureDetector if onTap is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contains GestureDetector if onTap is provided', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -230,7 +231,7 @@ void main() { expect(find.byType(GestureDetector), findsOneWidget); }); - testWidgets('resets the background color when navigated back', (WidgetTester tester) async { + testWidgetsWithLeakTracking('resets the background color when navigated back', (WidgetTester tester) async { const Color backgroundColor = CupertinoColors.systemBlue; const Color backgroundColorActivated = CupertinoColors.systemRed; @@ -279,7 +280,7 @@ void main() { }); group('alignment of widgets for left-to-right', () { - testWidgets('leading is on the left of title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('leading is on the left of title', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget leading = Icon(CupertinoIcons.add); @@ -303,7 +304,7 @@ void main() { expect(foundTitle.dx > foundLeading.dx, true); }); - testWidgets('subtitle is placed below title and aligned on left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('subtitle is placed below title and aligned on left', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile title'); const Widget subtitle = Text('CupertinoListTile subtitle'); @@ -328,7 +329,7 @@ void main() { expect(foundTitle.dy < foundSubtitle.dy, isTrue); }); - testWidgets('additionalInfo is on the right of title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('additionalInfo is on the right of title', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget additionalInfo = Text('Not Connected'); @@ -352,7 +353,7 @@ void main() { expect(foundTitle.dx < foundInfo.dx, isTrue); }); - testWidgets('trailing is on the right of additionalInfo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trailing is on the right of additionalInfo', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget additionalInfo = Text('Not Connected'); const Widget trailing = CupertinoListTileChevron(); @@ -380,7 +381,7 @@ void main() { }); group('alignment of widgets for right-to-left', () { - testWidgets('leading is on the right of title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('leading is on the right of title', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget leading = Icon(CupertinoIcons.add); @@ -404,7 +405,7 @@ void main() { expect(foundTitle.dx < foundLeading.dx, true); }); - testWidgets('subtitle is placed below title and aligned on right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('subtitle is placed below title and aligned on right', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile title'); const Widget subtitle = Text('CupertinoListTile subtitle'); @@ -429,7 +430,7 @@ void main() { expect(foundTitle.dy < foundSubtitle.dy, isTrue); }); - testWidgets('additionalInfo is on the left of title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('additionalInfo is on the left of title', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget additionalInfo = Text('Not Connected'); @@ -453,7 +454,7 @@ void main() { expect(foundTitle.dx > foundInfo.dx, isTrue); }); - testWidgets('trailing is on the left of additionalInfo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trailing is on the left of additionalInfo', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); const Widget additionalInfo = Text('Not Connected'); const Widget trailing = CupertinoListTileChevron(); @@ -480,7 +481,7 @@ void main() { }); }); - testWidgets('onTap with delay does not throw an exception', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap with delay does not throw an exception', (WidgetTester tester) async { const Widget title = Text('CupertinoListTile'); bool showTile = true; @@ -520,7 +521,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('title does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('title does not overflow', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( @@ -534,7 +535,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('subtitle does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('subtitle does not overflow', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( diff --git a/packages/flutter/test/cupertino/localizations_test.dart b/packages/flutter/test/cupertino/localizations_test.dart index 86b6e4fe00b19..fd1f94a63ff31 100644 --- a/packages/flutter/test/cupertino/localizations_test.dart +++ b/packages/flutter/test/cupertino/localizations_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('English translations exist for all CupertinoLocalization properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('English translations exist for all CupertinoLocalization properties', (WidgetTester tester) async { const CupertinoLocalizations localizations = DefaultCupertinoLocalizations(); expect(localizations.datePickerYear(2018), isNotNull); @@ -36,7 +37,7 @@ void main() { expect(localizations.noSpellCheckReplacementsLabel, isNotNull); }); - testWidgets('CupertinoLocalizations.of throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoLocalizations.of throws', (WidgetTester tester) async { final GlobalKey noLocalizationsAvailable = GlobalKey(); final GlobalKey localizationsAvailable = GlobalKey(); diff --git a/packages/flutter/test/cupertino/material/tab_scaffold_test.dart b/packages/flutter/test/cupertino/material/tab_scaffold_test.dart index ed211846d60e2..7f15919b3ef65 100644 --- a/packages/flutter/test/cupertino/material/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/material/tab_scaffold_test.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../../image_data.dart'; @@ -17,9 +18,14 @@ void main() { selectedTabs = <int>[]; }); - testWidgets('Last tab gets focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Last tab gets focus', (WidgetTester tester) async { // 2 nodes for 2 tabs - final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; + final List<FocusNode> focusNodes = <FocusNode>[]; + for (int i = 0; i < 2; i++) { + final FocusNode focusNode = FocusNode(); + focusNodes.add(focusNode); + addTearDown(focusNode.dispose); + } await tester.pumpWidget( MaterialApp( @@ -52,10 +58,13 @@ void main() { expect(focusNodes[1].hasFocus, isFalse); }); - testWidgets('Do not affect focus order in the route', (WidgetTester tester) async { - final List<FocusNode> focusNodes = <FocusNode>[ - FocusNode(), FocusNode(), FocusNode(), FocusNode(), - ]; + testWidgetsWithLeakTracking('Do not affect focus order in the route', (WidgetTester tester) async { + final List<FocusNode> focusNodes = <FocusNode>[]; + for (int i = 0; i < 4; i++) { + final FocusNode focusNode = FocusNode(); + focusNodes.add(focusNode); + addTearDown(focusNode.dispose); + } await tester.pumpWidget( MaterialApp( @@ -118,7 +127,7 @@ void main() { ); }); - testWidgets('Tab bar respects themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab bar respects themes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabScaffold( @@ -176,7 +185,7 @@ void main() { expect(tab2.text.style!.color!.value, CupertinoColors.systemRed.darkColor.value); }); - testWidgets('dark mode background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dark mode background color', (WidgetTester tester) async { const CupertinoDynamicColor backgroundColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF123456), darkColor: Color(0xFF654321), @@ -229,7 +238,7 @@ void main() { expect(tabDecoration.color!.value, backgroundColor.darkColor.value); }); - testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not lose state when focusing on text input', (WidgetTester tester) async { // Regression testing for https://github.com/flutter/flutter/issues/28457. await tester.pumpWidget( @@ -275,7 +284,7 @@ void main() { expect(find.text("don't lose me"), findsOneWidget); }); - testWidgets('textScaleFactor is set to 1.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('textScaleFactor is set to 1.0', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Builder(builder: (BuildContext context) { @@ -311,10 +320,10 @@ void main() { ); expect(barItems.length, greaterThan(0)); - expect(barItems.any((RichText t) => t.textScaleFactor != 1), isFalse); + expect(barItems, isNot(contains(predicate((RichText t) => t.textScaler != TextScaler.noScaling)))); expect(contents.length, greaterThan(0)); - expect(contents.any((RichText t) => t.textScaleFactor != 99), isFalse); + expect(contents, isNot(contains(predicate((RichText t) => t.textScaler != const TextScaler.linear(99.0))))); }); } diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 9b7532afd0619..fa6d3019993b2 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; int count = 0; @@ -1230,10 +1229,10 @@ void main() { ); expect(barItems.length, greaterThan(0)); - expect(barItems.any((RichText t) => t.textScaleFactor != 1), isFalse); + expect(barItems, isNot(contains(predicate((RichText t) => t.textScaler != TextScaler.noScaling)))); expect(contents.length, greaterThan(0)); - expect(contents.any((RichText t) => t.textScaleFactor != 99), isFalse); + expect(contents, isNot(contains(predicate((RichText t) => t.textScaler != const TextScaler.linear(99.0))))); // Also works with implicitly added widgets. tester.state<NavigatorState>(find.byType(Navigator)).push(CupertinoPageRoute<void>( diff --git a/packages/flutter/test/cupertino/nav_bar_transition_test.dart b/packages/flutter/test/cupertino/nav_bar_transition_test.dart index cdefa99dc59b5..662fcb5300c6c 100644 --- a/packages/flutter/test/cupertino/nav_bar_transition_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_transition_test.dart @@ -146,14 +146,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 342.547737105096302912 : 342.33420100808144, + 342.547737105096302912, 13.5, ), ); expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 342.547737105096302912 : 342.33420100808144, + 342.547737105096302912, 13.5, ), ); @@ -173,14 +173,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 357.912261979376353338 : 357.66579899191856, + 357.912261979376353338, 13.5, ), ); expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 357.912261979376353338 : 357.66579899191856, + 357.912261979376353338, 13.5, ), ); @@ -372,7 +372,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 342.547737105096302912 : 342.33420100808144, + 342.547737105096302912, 13.5, ), ); @@ -385,7 +385,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 342.547737105096302912 : 342.33420100808144, + 342.547737105096302912, 13.5, ), ); @@ -423,7 +423,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 357.912261979376353338 : 357.66579899191856, + 357.912261979376353338, 13.5, ), ); @@ -436,7 +436,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 357.912261979376353338 : 357.66579899191856, + 357.912261979376353338, 13.5, ), ); @@ -737,14 +737,14 @@ void main() { // Come in from the right and fade in. checkOpacity(tester, backChevron, 0.0); expect(tester.getTopLeft(backChevron), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 87.2460581221158690823 : 88.04496401548386, + 87.2460581221158690823, 7.0, )); await tester.pump(const Duration(milliseconds: 200)); checkOpacity(tester, backChevron, 0.09497911669313908); expect(tester.getTopLeft(backChevron), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 30.8718595298545324113 : 31.055883467197418, + 30.8718595298545324113, 7.0, )); }); @@ -785,7 +785,7 @@ void main() { expect( tester.getTopRight(backChevron), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 687.163941725296126606 : 685.9550359845161, + 687.163941725296126606, 7.0, ), ); @@ -795,7 +795,7 @@ void main() { expect( tester.getTopRight(backChevron), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 743.538140317557690651 : 742.9441165328026, + 743.538140317557690651, 7.0, ), ); @@ -900,7 +900,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('custom'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 684.459999084472656250 : 684.0, + 684.459999084472656250, 13.5, ), ); @@ -910,7 +910,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('custom'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 684.459999084472656250 : 684.0, + 684.459999084472656250, 13.5, ), ); @@ -942,7 +942,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 41.3003370761871337891 : 41.71033692359924, + 41.3003370761871337891, 13.5, ), ); @@ -952,7 +952,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? -258.642192125320434570 : -258.2321922779083, + -258.642192125320434570, 13.5, ), ); @@ -985,7 +985,7 @@ void main() { expect( tester.getTopRight(flying(tester, find.text('Page 1'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 758.699662923812866211 : 758.2896630764008, + 758.699662923812866211, 13.5, ), ); @@ -996,7 +996,7 @@ void main() { tester.getTopRight(flying(tester, find.text('Page 1'))), // >1000. It's now off the screen. const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 1058.64219212532043457 : 1058.2321922779083, + 1058.64219212532043457, 13.5, ), ); @@ -1022,14 +1022,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 16.9155227761479522997 : 16.926069676876068, + 16.9155227761479522997, 52.73951627314091, ), ); expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 16.9155227761479522997 : 16.926069676876068, + 16.9155227761479522997, 52.73951627314091, ), ); @@ -1041,14 +1041,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 1')).first), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 43.6029094262710827934 : 43.92089730501175, + 43.6029094262710827934, 22.49655644595623, ), ); expect( tester.getTopLeft(flying(tester, find.text('Page 1')).last), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 43.6029094262710827934 : 43.92089730501175, + 43.6029094262710827934, 22.49655644595623, ), ); @@ -1073,14 +1073,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('A title too long to fit'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 16.9155227761479522997 : 16.926069676876068, + 16.9155227761479522997, 52.73951627314091, ), ); expect( tester.getTopLeft(flying(tester, find.text('Back'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 16.9155227761479522997 : 16.926069676876068, + 16.9155227761479522997, 52.73951627314091, ), ); @@ -1091,14 +1091,14 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('A title too long to fit'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 43.6029094262710827934 : 43.92089730501175, + 43.6029094262710827934, 22.49655644595623, ), ); expect( tester.getTopLeft(flying(tester, find.text('Back'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 43.6029094262710827934 : 43.92089730501175, + 43.6029094262710827934, 22.49655644595623, ), ); @@ -1157,7 +1157,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 739.940336465835571289 : 739.7103369235992, + 739.940336465835571289, 13.5, ), ); @@ -1168,7 +1168,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 504.880443334579467773 : 504.65044379234314, + 504.880443334579467773, 13.5, ), ); @@ -1213,7 +1213,7 @@ void main() { expect( tester.getTopRight(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 60.0596635341644287109 : 60.28966307640076, + 60.0596635341644287109, 13.5, ), ); @@ -1224,7 +1224,7 @@ void main() { expect( tester.getTopRight(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 295.119556665420532227 : 295.34955620765686, + 295.119556665420532227, 13.5, ), ); @@ -1351,7 +1351,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 353.810205429792404175 : 353.5802058875561, + 353.810205429792404175, 13.5, ), ); @@ -1369,7 +1369,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 655.435583114624023438 : 655.2055835723877, + 655.435583114624023438, 13.5, ), ); @@ -1377,7 +1377,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 749.863556146621704102 : 749.6335566043854, + 749.863556146621704102, 13.5, ), ); @@ -1422,7 +1422,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 353.810205429792404175 : 353.5802058875561, + 353.810205429792404175, 13.5, ), ); @@ -1434,7 +1434,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 353.810205429792404175 : 353.5802058875561, + 353.810205429792404175, 13.5, ), ); @@ -1442,7 +1442,7 @@ void main() { expect( tester.getTopLeft(flying(tester, find.text('Page 2'))), const Offset( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 350.231143206357955933 : 350.0011436641216, + 350.231143206357955933, 13.5, ), ); diff --git a/packages/flutter/test/cupertino/picker_test.dart b/packages/flutter/test/cupertino/picker_test.dart index 1be766651335b..2522ada1c8fdb 100644 --- a/packages/flutter/test/cupertino/picker_test.dart +++ b/packages/flutter/test/cupertino/picker_test.dart @@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart'; class SpyFixedExtentScrollController extends FixedExtentScrollController { diff --git a/packages/flutter/test/cupertino/radio_test.dart b/packages/flutter/test/cupertino/radio_test.dart index 343d97b525944..0a7d3460c3ea2 100644 --- a/packages/flutter/test/cupertino/radio_test.dart +++ b/packages/flutter/test/cupertino/radio_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { diff --git a/packages/flutter/test/cupertino/refresh_test.dart b/packages/flutter/test/cupertino/refresh_test.dart index 5601753a4710f..cc4b757ee396c 100644 --- a/packages/flutter/test/cupertino/refresh_test.dart +++ b/packages/flutter/test/cupertino/refresh_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { late FakeBuilder mockHelper; @@ -34,7 +35,7 @@ void main() { } void uiTestGroup() { - testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async { + testWidgetsWithLeakTracking("doesn't invoke anything without user interaction", (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -56,7 +57,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('calls the indicator builder when starting to overscroll', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -90,7 +91,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets( + testWidgetsWithLeakTracking( "don't call the builder if overscroll doesn't move slivers like on Android", (WidgetTester tester) async { await tester.pumpWidget( @@ -124,7 +125,7 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.android), ); - testWidgets('let the builder update as canceled drag scrolls away', (WidgetTester tester) async { + testWidgetsWithLeakTracking('let the builder update as canceled drag scrolls away', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -186,7 +187,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('drag past threshold triggers refresh task', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag past threshold triggers refresh task', (WidgetTester tester) async { final List<MethodCall> platformCallLog = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { @@ -268,7 +269,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets( + testWidgetsWithLeakTracking( 'refreshing task keeps the sliver expanded forever until done', (WidgetTester tester) async { await tester.pumpWidget( @@ -343,7 +344,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'refreshing task keeps the sliver expanded forever until completes with error', (WidgetTester tester) async { final FlutterError error = FlutterError('Oops'); @@ -431,7 +432,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('expanded refreshing sliver scrolls normally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('expanded refreshing sliver scrolls normally', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); await tester.pumpWidget( @@ -520,7 +521,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('expanded refreshing sliver goes away when done', (WidgetTester tester) async { + testWidgetsWithLeakTracking('expanded refreshing sliver goes away when done', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); await tester.pumpWidget( @@ -588,7 +589,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('builder still called when sliver snapped back more than 90%', (WidgetTester tester) async { + testWidgetsWithLeakTracking('builder still called when sliver snapped back more than 90%', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); await tester.pumpWidget( @@ -686,7 +687,7 @@ void main() { expect(find.text('-1'), findsOneWidget); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets( + testWidgetsWithLeakTracking( 'retracting sliver during done cannot be pulled to refresh again until fully retracted', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); @@ -791,7 +792,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'sliver held in overscroll when task finishes completes normally', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); @@ -843,7 +844,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'sliver scrolled away when task completes properly removes itself', (WidgetTester tester) async { if (testListLength < 4) { @@ -929,7 +930,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( "don't do anything unless it can be overscrolled at the start of the list", (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); @@ -957,7 +958,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'without an onRefresh, builder is called with arm for one frame then sliver goes away', (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); @@ -1014,7 +1015,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('Should not crash when dragged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Should not crash when dragged', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -1039,7 +1040,7 @@ void main() { // Test to make sure the refresh sliver's overscroll isn't eaten by the // nav bar sliver https://github.com/flutter/flutter/issues/74516. - testWidgets( + testWidgetsWithLeakTracking( 'properly displays when the refresh sliver is behind the large title nav bar sliver', (WidgetTester tester) async { await tester.pumpWidget( @@ -1082,7 +1083,7 @@ void main() { } void stateMachineTestGroup() { - testWidgets('starts in inactive state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('starts in inactive state', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -1102,7 +1103,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -1132,7 +1133,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('goes to armed the frame it passes the threshold', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CustomScrollView( @@ -1167,7 +1168,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets( + testWidgetsWithLeakTracking( 'goes to refresh the frame it crossed back the refresh threshold', (WidgetTester tester) async { await tester.pumpWidget( @@ -1216,7 +1217,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'goes to done internally as soon as the task finishes', (WidgetTester tester) async { await tester.pumpWidget( @@ -1264,7 +1265,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'goes back to inactive when retracting back past 10% of arming distance', (WidgetTester tester) async { await tester.pumpWidget( @@ -1349,7 +1350,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'goes back to inactive if already scrolled away when task completes', (WidgetTester tester) async { await tester.pumpWidget( @@ -1413,7 +1414,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( "don't have to build any indicators or occupy space during refresh", (WidgetTester tester) async { mockHelper.refreshIndicator = const Center(child: Text('-1')); @@ -1463,7 +1464,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('buildRefreshIndicator progress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('buildRefreshIndicator progress', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder( @@ -1510,7 +1511,7 @@ void main() { expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 100.0 / 100.0); }); - testWidgets('indicator should not become larger when overscrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('indicator should not become larger when overscrolled', (WidgetTester tester) async { // test for https://github.com/flutter/flutter/issues/79841 await tester.pumpWidget( Directionality( @@ -1546,7 +1547,7 @@ void main() { // correct by coincidence. group('state machine test short list', stateMachineTestGroup); - testWidgets( + testWidgetsWithLeakTracking( 'Does not crash when paintExtent > remainingPaintExtent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/46871. diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart index f3b66c8a72206..e19a390229dbd 100644 --- a/packages/flutter/test/cupertino/route_test.dart +++ b/packages/flutter/test/cupertino/route_test.dart @@ -12,8 +12,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -23,7 +23,7 @@ void main() { navigatorObserver = MockNavigatorObserver(); }); - testWidgets('Middle auto-populates with title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Middle auto-populates with title', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Placeholder(), @@ -53,7 +53,7 @@ void main() { expect(tester.getCenter(find.text('An iPod')).dx, 400.0); }); - testWidgets('Large title auto-populates with title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Large title auto-populates with title', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Placeholder(), @@ -118,7 +118,7 @@ void main() { expect(tester.getCenter(find.byWidget(titles[0].widget)).dx, 400.0); }); - testWidgets('Leading auto-populates with back button with previous title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leading auto-populates with back button with previous title', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Placeholder(), @@ -165,7 +165,7 @@ void main() { expect(tester.getTopLeft(find.text('An iPod')).dx, moreOrLessEquals(8.0 + 4.0 + 34.0 + 6.0, epsilon: 0.5)); }); - testWidgets('Previous title is correct on first transition frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Previous title is correct on first transition frame', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Placeholder(), @@ -208,7 +208,7 @@ void main() { expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget); }); - testWidgets('Previous title stays up to date with changing routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Previous title stays up to date with changing routes', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Placeholder(), @@ -272,7 +272,7 @@ void main() { expect(tester.getTopLeft(find.text('Back')).dx, moreOrLessEquals(8.0 + 4.0 + 34.0 + 6.0, epsilon: 0.5)); }); - testWidgets('Back swipe dismiss interrupted by route push', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Back swipe dismiss interrupted by route push', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28728 final GlobalKey scaffoldKey = GlobalKey(); @@ -378,7 +378,7 @@ void main() { ); }); - testWidgets('Fullscreen route animates correct transform values over time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fullscreen route animates correct transform values over time', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder( @@ -563,11 +563,11 @@ void main() { expect(tester.getTopLeft(find.byType(Placeholder)).dx, moreOrLessEquals(-0.0, epsilon: 1.0)); } - testWidgets('CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async { await testParallax(tester, fromFullscreenDialog: false); }); - testWidgets('FullscreenDialog CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FullscreenDialog CupertinoPageRoute has parallax when non fullscreenDialog route is pushed on top', (WidgetTester tester) async { await testParallax(tester, fromFullscreenDialog: true); }); @@ -652,15 +652,15 @@ void main() { expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0); } - testWidgets('CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async { await testNoParallax(tester, fromFullscreenDialog: false); }); - testWidgets('FullscreenDialog CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FullscreenDialog CupertinoPageRoute has no parallax when fullscreenDialog route is pushed on top', (WidgetTester tester) async { await testNoParallax(tester, fromFullscreenDialog: true); }); - testWidgets('Animated push/pop is not linear', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animated push/pop is not linear', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Text('1'), @@ -712,7 +712,7 @@ void main() { expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(607, epsilon: 1)); }); - testWidgets('Dragged pop gesture is linear', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dragged pop gesture is linear', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Text('1'), @@ -758,7 +758,7 @@ void main() { expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(300)); }); - testWidgets('Pop gesture snapping is not linear', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pop gesture snapping is not linear', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Text('1'), @@ -805,7 +805,7 @@ void main() { ); }); - testWidgets('Snapped drags forwards and backwards should signal didStart/StopUserGesture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snapped drags forwards and backwards should signal didStart/StopUserGesture', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( @@ -859,7 +859,7 @@ void main() { }); /// Regression test for https://github.com/flutter/flutter/issues/29596. - testWidgets('test edge swipe then drop back at ending point works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test edge swipe then drop back at ending point works', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( navigatorObservers: <NavigatorObserver>[navigatorObserver], @@ -896,7 +896,7 @@ void main() { expect(navigatorObserver.invocations.removeLast(), NavigatorInvocation.didPop); }); - testWidgets('test edge swipe then drop back at starting point works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test edge swipe then drop back at starting point works', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( navigatorObservers: <NavigatorObserver>[navigatorObserver], @@ -953,7 +953,7 @@ void main() { ); } - testWidgets('when route is not fullscreenDialog, it has a barrierColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when route is not fullscreenDialog, it has a barrierColor', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: SizedBox.expand(), @@ -968,7 +968,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, const Color(0x18000000)); }); - testWidgets('when route is a fullscreenDialog, it has no barrierColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when route is a fullscreenDialog, it has no barrierColor', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: SizedBox.expand(), @@ -983,7 +983,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, isNull); }); - testWidgets('when route is not fullscreenDialog, it has a _CupertinoEdgeShadowDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when route is not fullscreenDialog, it has a _CupertinoEdgeShadowDecoration', (WidgetTester tester) async { PaintPattern paintsShadowRect({required double dx, required Color color}) { return paints..everything((Symbol methodName, List<dynamic> arguments) { if (methodName != #drawRect) { @@ -1073,7 +1073,7 @@ void main() { expect(box, paintsShadowRect(dx: 754, color: const Color(0x00000000))); }); - testWidgets('when route is fullscreenDialog, it has no visible _CupertinoEdgeShadowDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when route is fullscreenDialog, it has no visible _CupertinoEdgeShadowDecoration', (WidgetTester tester) async { PaintPattern paintsNoShadows() { return paints..everything((Symbol methodName, List<dynamic> arguments) { if (methodName != #drawRect) { @@ -1115,7 +1115,7 @@ void main() { }); }); - testWidgets('ModalPopup overlay dark mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ModalPopup overlay dark mode', (WidgetTester tester) async { late StateSetter stateSetter; Brightness brightness = Brightness.light; @@ -1188,7 +1188,7 @@ void main() { ); }); - testWidgets('During back swipe the route ignores input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('During back swipe the route ignores input', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/39989 final GlobalKey homeScaffoldKey = GlobalKey(); @@ -1251,7 +1251,7 @@ void main() { expect(pageTapCount, 1); }); - testWidgets('showCupertinoModalPopup uses root navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup uses root navigator by default', (WidgetTester tester) async { final PopupObserver rootObserver = PopupObserver(); final PopupObserver nestedObserver = PopupObserver(); @@ -1284,7 +1284,7 @@ void main() { expect(nestedObserver.popupCount, 0); }); - testWidgets('back swipe to screen edges does not dismiss the hero animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('back swipe to screen edges does not dismiss the hero animation', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); final UniqueKey container = UniqueKey(); await tester.pumpWidget(CupertinoApp( @@ -1354,7 +1354,7 @@ void main() { expect(firstPosition, greaterThan(thirdPosition)); }); - testWidgets('showCupertinoModalPopup uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final PopupObserver rootObserver = PopupObserver(); final PopupObserver nestedObserver = PopupObserver(); @@ -1388,7 +1388,7 @@ void main() { expect(nestedObserver.popupCount, 1); }); - testWidgets('showCupertinoDialog uses root navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoDialog uses root navigator by default', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -1421,7 +1421,7 @@ void main() { expect(nestedObserver.dialogCount, 0); }); - testWidgets('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -1455,7 +1455,7 @@ void main() { expect(nestedObserver.dialogCount, 1); }); - testWidgets('showCupertinoModalPopup does not allow for semantics dismiss by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup does not allow for semantics dismiss by default', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(CupertinoApp( @@ -1490,7 +1490,7 @@ void main() { semantics.dispose(); }); - testWidgets('showCupertinoModalPopup allows for semantics dismiss when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup allows for semantics dismiss when set', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(CupertinoApp( @@ -1526,7 +1526,7 @@ void main() { semantics.dispose(); }); - testWidgets('showCupertinoModalPopup passes RouteSettings to PopupRoute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup passes RouteSettings to PopupRoute', (WidgetTester tester) async { final RouteSettingsObserver routeSettingsObserver = RouteSettingsObserver(); await tester.pumpWidget(CupertinoApp( @@ -1557,7 +1557,7 @@ void main() { expect(routeSettingsObserver.routeName, '/modal'); }); - testWidgets('showCupertinoModalPopup transparent barrier color is transparent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup transparent barrier color is transparent', (WidgetTester tester) async { const Color kTransparentColor = Color(0x00000000); await tester.pumpWidget(CupertinoApp( @@ -1583,7 +1583,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, null); }); - testWidgets('showCupertinoModalPopup null barrier color must be default gray barrier color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup null barrier color must be default gray barrier color', (WidgetTester tester) async { // Barrier color for a Cupertino modal barrier. // Extracted from https://developer.apple.com/design/resources/. const Color kModalBarrierColor = CupertinoDynamicColor.withBrightness( @@ -1613,7 +1613,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, kModalBarrierColor); }); - testWidgets('showCupertinoModalPopup custom barrier color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup custom barrier color', (WidgetTester tester) async { const Color customColor = Color(0x11223344); await tester.pumpWidget(CupertinoApp( @@ -1639,7 +1639,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, customColor); }); - testWidgets('showCupertinoModalPopup barrier dismissible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup barrier dismissible', (WidgetTester tester) async { await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( child: Builder(builder: (BuildContext context) { @@ -1664,7 +1664,7 @@ void main() { expect(find.text('Visible'), findsNothing); }); - testWidgets('showCupertinoModalPopup barrier not dismissible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showCupertinoModalPopup barrier not dismissible', (WidgetTester tester) async { await tester.pumpWidget(CupertinoApp( home: CupertinoPageScaffold( child: Builder(builder: (BuildContext context) { @@ -1690,7 +1690,7 @@ void main() { expect(find.text('Visible'), findsOneWidget); }); - testWidgets('CupertinoPage works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoPage works', (WidgetTester tester) async { final LocalKey pageKey = UniqueKey(); final TransitionDetector detector = TransitionDetector(); List<Page<void>> myPages = <Page<void>>[ @@ -1751,7 +1751,7 @@ void main() { expect(find.widgetWithText(CupertinoNavigationBar, 'title two'), findsOneWidget); }); - testWidgets('CupertinoPage can toggle MaintainState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoPage can toggle MaintainState', (WidgetTester tester) async { final LocalKey pageKeyOne = UniqueKey(); final LocalKey pageKeyTwo = UniqueKey(); final TransitionDetector detector = TransitionDetector(); @@ -1800,7 +1800,7 @@ void main() { expect(find.text('second'), findsOneWidget); }); - testWidgets('Popping routes should cancel down events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popping routes should cancel down events', (WidgetTester tester) async { await tester.pumpWidget(const _TestPostRouteCancel()); final TestGesture gesture = await tester.createGesture(); @@ -1819,7 +1819,7 @@ void main() { expect(find.text('PointerCancelEvents: 1'), findsOneWidget); }); - testWidgets('Popping routes during back swipe should not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popping routes during back swipe should not crash', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/63984#issuecomment-675679939 final CupertinoPageRoute<void> r = CupertinoPageRoute<void>(builder: (BuildContext context) { @@ -1869,7 +1869,7 @@ void main() { await tester.pump(); }); - testWidgets('CupertinoModalPopupRoute is state restorable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoModalPopupRoute is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( restorationScopeId: 'app', @@ -1900,7 +1900,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 group('showCupertinoDialog avoids overlapping display features', () { - testWidgets('positioning with anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { @@ -1938,7 +1938,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning with Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with Directionality', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { @@ -1978,7 +1978,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning by default', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { @@ -2017,7 +2017,7 @@ void main() { }); group('showCupertinoModalPopup avoids overlapping display features', () { - testWidgets('positioning using anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning using anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { @@ -2055,7 +2055,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); }); - testWidgets('positioning using Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning using Directionality', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { @@ -2095,7 +2095,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); }); - testWidgets('default positioning', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default positioning', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { diff --git a/packages/flutter/test/cupertino/scaffold_test.dart b/packages/flutter/test/cupertino/scaffold_test.dart index 41b40c07b1fd9..2dd4f74dd374a 100644 --- a/packages/flutter/test/cupertino/scaffold_test.dart +++ b/packages/flutter/test/cupertino/scaffold_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import '../image_data.dart'; -import '../rendering/mock_canvas.dart'; /// Integration tests testing both [CupertinoPageScaffold] and [CupertinoTabScaffold]. void main() { @@ -554,6 +553,6 @@ void main() { expect(richTextList.length, greaterThan(0)); expect(richTextList.any((RichText text) => text.textScaleFactor != 1), isFalse); - expect(tester.widget<RichText>(find.descendant(of: find.text('content'), matching: find.byType(RichText))).textScaleFactor, 99); + expect(tester.widget<RichText>(find.descendant(of: find.text('content'), matching: find.byType(RichText))).textScaler, const TextScaler.linear(99.0)); }); } diff --git a/packages/flutter/test/cupertino/scrollbar_paint_test.dart b/packages/flutter/test/cupertino/scrollbar_paint_test.dart index a8189246a1bf8..f9bc1822ac1e7 100644 --- a/packages/flutter/test/cupertino/scrollbar_paint_test.dart +++ b/packages/flutter/test/cupertino/scrollbar_paint_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color _kScrollbarColor = Color(0x59000000); @@ -15,7 +14,7 @@ const Offset _kGestureOffset = Offset(0, -25); const Radius _kScrollbarRadius = Radius.circular(1.5); void main() { - testWidgets('Paints iOS spec', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paints iOS spec', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -53,7 +52,7 @@ void main() { )); }); - testWidgets('Paints iOS spec with nav bar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paints iOS spec with nav bar', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: MediaQuery( @@ -98,7 +97,7 @@ void main() { )); }); - testWidgets("should not paint when there isn't enough space", (WidgetTester tester) async { + testWidgetsWithLeakTracking("should not paint when there isn't enough space", (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: MediaQuery( diff --git a/packages/flutter/test/cupertino/scrollbar_test.dart b/packages/flutter/test/cupertino/scrollbar_test.dart index 84ff11748ce3a..e8a1277ef7494 100644 --- a/packages/flutter/test/cupertino/scrollbar_test.dart +++ b/packages/flutter/test/cupertino/scrollbar_test.dart @@ -9,8 +9,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - const CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness( color: Color(0x59000000), darkColor: Color(0x80FFFFFF), diff --git a/packages/flutter/test/cupertino/segmented_control_test.dart b/packages/flutter/test/cupertino/segmented_control_test.dart index 42775489f9d38..84fd8c2493672 100644 --- a/packages/flutter/test/cupertino/segmented_control_test.dart +++ b/packages/flutter/test/cupertino/segmented_control_test.dart @@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; @@ -93,7 +94,7 @@ Color getBackgroundColor(WidgetTester tester, int childIndex) { } void main() { - testWidgets('Tap changes toggle state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap changes toggle state', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -127,7 +128,7 @@ void main() { expect(sharedValue, 1); }); - testWidgets('Need at least 2 children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Need at least 2 children', (WidgetTester tester) async { await expectLater( () => tester.pumpWidget( boilerplate( @@ -161,7 +162,7 @@ void main() { ); }); - testWidgets('Padding works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding works', (WidgetTester tester) async { const Key key = Key('Container'); final Map<int, Widget> children = <int, Widget>{}; @@ -248,7 +249,7 @@ void main() { await verifyPadding(padding: const EdgeInsets.fromLTRB(1, 3, 5, 7)); }); - testWidgets('Value attribute must be the key of one of the children widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Value attribute must be the key of one of the children widgets', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -271,7 +272,7 @@ void main() { ); }); - testWidgets('Widgets have correct default text/icon styles, change correctly on selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widgets have correct default text/icon styles, change correctly on selection', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Icon(IconData(1)); @@ -314,7 +315,7 @@ void main() { expect(iconTheme.data.color, isSameColorAs(CupertinoColors.white)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Segmented controls respects themes', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; @@ -360,7 +361,7 @@ void main() { }, ); - testWidgets('SegmentedControl is correct when user provides custom colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedControl is correct when user provides custom colors', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Icon(IconData(1)); @@ -417,7 +418,7 @@ void main() { expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen.color); }); - testWidgets('Widgets are centered within segments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widgets are centered within segments', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -444,7 +445,7 @@ void main() { expect(tester.getCenter(find.text('Child 2')), const Offset(142.0, 100.0)); }); - testWidgets('Tap calls onValueChanged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap calls onValueChanged', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -473,7 +474,7 @@ void main() { expect(value, isTrue); }); - testWidgets('State does not change if onValueChanged does not call setState()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('State does not change if onValueChanged does not call setState()', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -504,7 +505,7 @@ void main() { expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Background color of child should change on selection, ' 'and should not change when tapped again', (WidgetTester tester) async { @@ -524,7 +525,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Children can be non-Text or Icon widgets (in this case, ' 'a Container or Placeholder widget)', (WidgetTester tester) async { @@ -557,7 +558,7 @@ void main() { }, ); - testWidgets('Passed in value is child initially selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passed in value is child initially selected', (WidgetTester tester) async { await tester.pumpWidget(setupSimpleSegmentedControl()); expect(getSelectedIndex(tester), 0); @@ -566,7 +567,7 @@ void main() { expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Null input for value results in no child initially selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Null input for value results in no child initially selected', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -597,7 +598,7 @@ void main() { expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Long press changes background color of not-selected child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Long press changes background color of not-selected child', (WidgetTester tester) async { await tester.pumpWidget(setupSimpleSegmentedControl()); expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue); @@ -611,7 +612,7 @@ void main() { expect(getBackgroundColor(tester, 1), const Color(0x33007aff)); }); - testWidgets('Long press does not change background color of currently-selected child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Long press does not change background color of currently-selected child', (WidgetTester tester) async { await tester.pumpWidget(setupSimpleSegmentedControl()); expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue); @@ -625,7 +626,7 @@ void main() { expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Height of segmented control is determined by tallest widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Height of segmented control is determined by tallest widget', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = Container( constraints: const BoxConstraints.tightFor(height: 100.0), @@ -656,7 +657,7 @@ void main() { expect(buttonBox.size.height, 400.0); }); - testWidgets('Width of each segmented control segment is determined by widest widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Width of each segmented control segment is determined by widest widget', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = Container( constraints: const BoxConstraints.tightFor(width: 50.0), @@ -695,7 +696,7 @@ void main() { expect(childWidth, getSurroundingRect(tester, child: 2).width); }); - testWidgets('Width is finite in unbounded space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Width is finite in unbounded space', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -723,7 +724,7 @@ void main() { expect(segmentedControl.size.width.isFinite, isTrue); }); - testWidgets('Directionality test - RTL should reverse order of widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality test - RTL should reverse order of widgets', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -743,7 +744,7 @@ void main() { expect(tester.getTopRight(find.text('Child 1')).dx > tester.getTopRight(find.text('Child 2')).dx, isTrue); }); - testWidgets('Correct initial selection and toggling behavior - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Correct initial selection and toggling behavior - RTL', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -786,7 +787,7 @@ void main() { expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue); }); - testWidgets('Segmented control semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Segmented control semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map<int, Widget> children = <int, Widget>{}; @@ -889,7 +890,7 @@ void main() { semantics.dispose(); }); - testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Non-centered taps work on smaller widgets', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -931,7 +932,7 @@ void main() { expect(sharedValue, 0); }); - testWidgets('Hit-tests report accurate local position in segments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hit-tests report accurate local position in segments', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; late TapDownDetails tapDownDetails; children[0] = GestureDetector( @@ -971,7 +972,7 @@ void main() { expect(tapDownDetails.globalPosition, segment0GlobalOffset + const Offset(7, 11)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Segment still hittable with a child that has no hitbox', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/57326. @@ -1010,7 +1011,7 @@ void main() { }, ); - testWidgets('Animation is correct when the selected segment changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animation is correct when the selected segment changes', (WidgetTester tester) async { await tester.pumpWidget(setupSimpleSegmentedControl()); await tester.tap(find.text('Child 2')); @@ -1040,7 +1041,7 @@ void main() { expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue); }); - testWidgets('Animation is correct when widget is rebuilt', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animation is correct when widget is rebuilt', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('Child 1'); children[1] = const Text('Child 2'); @@ -1192,7 +1193,7 @@ void main() { expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue); }); - testWidgets('Multiple segments are pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiple segments are pressed', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1234,7 +1235,7 @@ void main() { expect(getBackgroundColor(tester, 2), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Transition is triggered while a transition is already occurring', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Transition is triggered while a transition is already occurring', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1296,7 +1297,7 @@ void main() { expect(getBackgroundColor(tester, 2), CupertinoColors.activeBlue); }); - testWidgets('Segment is selected while it is transitioning to unselected state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Segment is selected while it is transitioning to unselected state', (WidgetTester tester) async { await tester.pumpWidget(setupSimpleSegmentedControl()); await tester.tap(find.text('Child 2')); @@ -1324,7 +1325,7 @@ void main() { expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Add segment while animation is running', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Add segment while animation is running', (WidgetTester tester) async { Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1372,7 +1373,7 @@ void main() { expect(getBackgroundColor(tester, 3), isSameColorAs(CupertinoColors.white)); }); - testWidgets('Remove segment while animation is running', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Remove segment while animation is running', (WidgetTester tester) async { Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1417,7 +1418,7 @@ void main() { expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue); }); - testWidgets('Remove currently animating segment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Remove currently animating segment', (WidgetTester tester) async { Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1469,7 +1470,7 @@ void main() { }); // Regression test: https://github.com/flutter/flutter/issues/43414. - testWidgets("Quick double tap doesn't break the internal state", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Quick double tap doesn't break the internal state", (WidgetTester tester) async { const Map<int, Widget> children = <int, Widget>{ 0: Text('A'), 1: Text('B'), @@ -1509,7 +1510,7 @@ void main() { expect(sharedValue, 2); }); - testWidgets('Golden Test Placeholder Widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Golden Test Placeholder Widget', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = Container(); children[1] = const Placeholder(); @@ -1543,7 +1544,7 @@ void main() { ); }); - testWidgets('Golden Test Pressed State', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Golden Test Pressed State', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); @@ -1581,7 +1582,7 @@ void main() { ); }); - testWidgets('Hovering over Cupertino segmented control updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over Cupertino segmented control updates cursor to clickable on Web', (WidgetTester tester) async { final Map<int, Widget> children = <int, Widget>{}; children[0] = const Text('A'); children[1] = const Text('B'); diff --git a/packages/flutter/test/cupertino/slider_test.dart b/packages/flutter/test/cupertino/slider_test.dart index d0f67ebd827a4..38fd62e11c6d2 100644 --- a/packages/flutter/test/cupertino/slider_test.dart +++ b/packages/flutter/test/cupertino/slider_test.dart @@ -9,8 +9,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; const CupertinoDynamicColor _kSystemFill = CupertinoDynamicColor( @@ -33,7 +33,7 @@ void main() { return tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0)); } - testWidgets('Slider does not move when tapped (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider does not move when tapped (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; @@ -69,7 +69,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider does not move when tapped (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider does not move when tapped (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; @@ -105,7 +105,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider calls onChangeStart once when interaction begins', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider calls onChangeStart once when interaction begins', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; int numberOfTimesOnChangeStartIsCalled = 0; @@ -146,7 +146,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider calls onChangeEnd once after interaction has ended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider calls onChangeEnd once after interaction has ended', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; int numberOfTimesOnChangeEndIsCalled = 0; @@ -187,7 +187,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider moves when dragged (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; late double startValue; @@ -241,7 +241,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider moves when dragged (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; late double startValue; @@ -295,7 +295,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -359,7 +359,7 @@ void main() { semantics.dispose(); }); - testWidgets('Slider Semantics can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider Semantics can be updated', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); double value = 0.5; await tester.pumpWidget( @@ -410,7 +410,7 @@ void main() { handle.dispose(); }); - testWidgets('Slider respects themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider respects themes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -445,7 +445,7 @@ void main() { ); }); - testWidgets('Themes can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Themes can be overridden', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), @@ -464,7 +464,7 @@ void main() { ); }); - testWidgets('Themes can be overridden by dynamic colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Themes can be overridden by dynamic colors', (WidgetTester tester) async { const CupertinoDynamicColor activeColor = CupertinoDynamicColor( color: Color(0x00000001), darkColor: Color(0x00000002), @@ -520,7 +520,7 @@ void main() { expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.highContrastElevatedColor)); }); - testWidgets('track color is dynamic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('track color is dynamic', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.light), @@ -568,7 +568,7 @@ void main() { ); }); - testWidgets('Thumb color can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Thumb color can be overridden', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -616,7 +616,7 @@ void main() { ); }); - testWidgets('Hovering over Cupertino slider thumb updates cursor to clickable on Web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovering over Cupertino slider thumb updates cursor to clickable on Web', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; diff --git a/packages/flutter/test/cupertino/switch_test.dart b/packages/flutter/test/cupertino/switch_test.dart index f91fd0af87aa7..3956227161dae 100644 --- a/packages/flutter/test/cupertino/switch_test.dart +++ b/packages/flutter/test/cupertino/switch_test.dart @@ -15,8 +15,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { testWidgets('Switch can toggle on tap', (WidgetTester tester) async { final Key switchKey = UniqueKey(); diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart index 9e76489704ff4..22270d83f0792 100644 --- a/packages/flutter/test/cupertino/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; - import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../image_data.dart'; import '../rendering/rendering_tester.dart' show TestCallbackPainter; +import '../widgets/navigator_utils.dart'; late List<int> selectedTabs; @@ -1106,10 +1107,10 @@ void main() { ); expect(barItems.length, greaterThan(0)); - expect(barItems.any((RichText t) => t.textScaleFactor != 1), isFalse); + expect(barItems, isNot(contains(predicate((RichText t) => t.textScaler != TextScaler.noScaling)))); expect(contents.length, greaterThan(0)); - expect(contents.any((RichText t) => t.textScaleFactor != 99), isFalse); + expect(contents, isNot(contains(predicate((RichText t) => t.textScaler != const TextScaler.linear(99.0))))); }); testWidgets('state restoration', (WidgetTester tester) async { @@ -1215,6 +1216,133 @@ void main() { expect(find.text('Content 2'), findsNothing); expect(find.text('Content 3'), findsNothing); }); + + group('Android Predictive Back', () { + bool? lastFrameworkHandlesBack; + setUp(() async { + lastFrameworkHandlesBack = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA<bool>()); + lastFrameworkHandlesBack = methodCall.arguments as bool; + } + return; + }); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/lifecycle', + const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), + (ByteData? data) {}, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + testWidgets('System back navigation inside of tabs', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: MediaQuery( + data: const MediaQueryData( + viewInsets: EdgeInsets.only(bottom: 200), + ), + child: CupertinoTabScaffold( + tabBar: _buildTabBar(), + tabBuilder: (BuildContext context, int index) { + return CupertinoTabView( + builder: (BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text('Page 1 of tab ${index + 1}'), + ), + child: Center( + child: CupertinoButton( + child: const Text('Next page'), + onPressed: () { + Navigator.of(context).push( + CupertinoPageRoute<void>( + builder: (BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text('Page 2 of tab ${index + 1}'), + ), + child: Center( + child: CupertinoButton( + child: const Text('Back'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + ), + ); + }, + ), + ), + ); + }, + ); + }, + ), + ), + ), + ); + + expect(find.text('Page 1 of tab 1'), findsOneWidget); + expect(find.text('Page 2 of tab 1'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 1'), findsNothing); + expect(find.text('Page 2 of tab 1'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 1'), findsOneWidget); + expect(find.text('Page 2 of tab 1'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 1'), findsNothing); + expect(find.text('Page 2 of tab 1'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Tab 2')); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 2'), findsOneWidget); + expect(find.text('Page 2 of tab 2'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Tab 1')); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 1'), findsNothing); + expect(find.text('Page 2 of tab 1'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 1'), findsOneWidget); + expect(find.text('Page 2 of tab 1'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Tab 2')); + await tester.pumpAndSettle(); + expect(find.text('Page 1 of tab 2'), findsOneWidget); + expect(find.text('Page 2 of tab 2'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: kIsWeb, // [intended] frameworkHandlesBack not used on web. + ); + }); } CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) { diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart index 63001a64f3ffc..c2f6ede7ada93 100644 --- a/packages/flutter/test/cupertino/tab_test.dart +++ b/packages/flutter/test/cupertino/tab_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Use home', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use home', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabView( @@ -18,7 +19,7 @@ void main() { expect(find.text('home'), findsOneWidget); }); - testWidgets('Use routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use routes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabView( @@ -32,7 +33,7 @@ void main() { expect(find.text('first route'), findsOneWidget); }); - testWidgets('Use home and named routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use home and named routes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabView( @@ -58,7 +59,7 @@ void main() { expect(find.text('second named route'), findsOneWidget); }); - testWidgets('Use onGenerateRoute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use onGenerateRoute', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabView( @@ -80,7 +81,7 @@ void main() { expect(find.text('generated home'), findsOneWidget); }); - testWidgets('Use onUnknownRoute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Use onUnknownRoute', (WidgetTester tester) async { late String unknownForRouteCalled; await tester.pumpWidget( CupertinoApp( @@ -101,7 +102,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('Can use navigatorKey to navigate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can use navigatorKey to navigate', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey(); await tester.pumpWidget( CupertinoApp( @@ -122,7 +123,7 @@ void main() { expect(find.text('second route'), findsOneWidget); }); - testWidgets('Changing the key resets the navigator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing the key resets the navigator', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey(); await tester.pumpWidget( CupertinoApp( @@ -172,7 +173,7 @@ void main() { expect(find.text('second route'), findsNothing); }); - testWidgets('Throws FlutterError when onUnknownRoute is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws FlutterError when onUnknownRoute is null', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey(); await tester.pumpWidget( CupertinoApp( @@ -209,7 +210,7 @@ void main() { ); }); - testWidgets('Throws FlutterError when onUnknownRoute returns null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws FlutterError when onUnknownRoute returns null', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); await tester.pumpWidget( CupertinoApp( @@ -239,7 +240,7 @@ void main() { ); }); - testWidgets('Navigator of CupertinoTabView restores state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigator of CupertinoTabView restores state', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( restorationScopeId: 'app', diff --git a/packages/flutter/test/cupertino/text_field_restoration_test.dart b/packages/flutter/test/cupertino/text_field_restoration_test.dart index ad6c8e12f896b..b892984d0d195 100644 --- a/packages/flutter/test/cupertino/text_field_restoration_test.dart +++ b/packages/flutter/test/cupertino/text_field_restoration_test.dart @@ -5,12 +5,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const String text = 'Hello World! How are you? Life is good!'; const String alternativeText = 'Everything is awesome!!'; void main() { - testWidgets('CupertinoTextField restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTextField restoration', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( restorationScopeId: 'app', @@ -21,7 +22,7 @@ void main() { await restoreAndVerify(tester); }); - testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTextField restoration with external controller', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( restorationScopeId: 'app', diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index ab0b3fbf86dad..fe4a90fc084ba 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -18,7 +18,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart' show OverflowWidgetTextEditingController; import '../widgets/live_text_utils.dart'; @@ -201,6 +200,7 @@ void main() { setUp(() async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); + EditableText.debugDeterministicCursor = false; // Fill the clipboard so that the Paste option is available in the text // selection menu. @@ -250,6 +250,153 @@ void main() { }, ); + testWidgets('Look Up shows up on iOS only', (WidgetTester tester) async { + String? lastLookUp; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'LookUp.invoke') { + expect(methodCall.arguments, isA<String>()); + lastLookUp = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = TextEditingController( + text: 'Test', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Look Up'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Look Up')); + expect(lastLookUp, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgets('Search Web shows up on iOS only', (WidgetTester tester) async { + String? lastSearch; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SearchWeb.invoke') { + expect(methodCall.arguments, isA<String>()); + lastSearch = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = TextEditingController( + text: 'Test', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Search Web'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Search Web')); + expect(lastSearch, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgets('Share shows up on iOS only', (WidgetTester tester) async { + String? lastShare; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'Share.invoke') { + expect(methodCall.arguments, isA<String>()); + lastShare = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = TextEditingController( + text: 'Test', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Share...'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Share...')); + expect(lastShare, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', @@ -924,7 +1071,8 @@ void main() { await tester.enterText(find.byType(CupertinoTextField), 'input'); await tester.pump(); - expect(find.text('placeholder'), findsNothing); + final Element element = tester.element(find.text('placeholder')); + expect(Visibility.of(element), false); }, ); @@ -1817,7 +1965,9 @@ void main() { expect(find.text('field 1'), findsOneWidget); expect(find.text("j'aime la poutine"), findsOneWidget); - expect(find.text('field 2'), findsNothing); + + final Element placeholder2Element = tester.element(find.text('field 2')); + expect(Visibility.of(placeholder2Element), false); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. testWidgets( @@ -1859,7 +2009,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -1879,10 +2029,10 @@ void main() { // Plain collapsed selection. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 6); // Toolbar shows on mobile. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformMobile ? findsNWidgets(2) : findsNothing); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(2) : findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets( @@ -2013,8 +2163,8 @@ void main() { const TextSelection(baseOffset: 24, extentOffset: 35), ); - // Selected text shows 3 toolbar buttons. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + // Selected text shows 5 toolbar buttons. + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Tap the selected word to hide the toolbar and retain the selection. await tester.tapAt(vPos); @@ -2032,7 +2182,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Tap past the selected word to move the cursor and hide the toolbar. await tester.tapAt(ePos); @@ -2088,7 +2238,7 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); - // Selected text shows 3 toolbar buttons. + // Selected text shows 4 toolbar buttons. expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4)); // Tap somewhere else to move the cursor. @@ -2145,13 +2295,13 @@ void main() { } else { switch (defaultTargetPlatform) { case TargetPlatform.macOS: - case TargetPlatform.iOS: expect(find.byType(CupertinoButton), findsNWidgets(3)); + case TargetPlatform.iOS: case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - expect(find.byType(CupertinoButton), findsNWidgets(4)); + expect(find.byType(CupertinoButton), findsNWidgets(6)); } } }, @@ -2329,7 +2479,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2351,7 +2501,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -2363,7 +2513,7 @@ void main() { ); // Selected text shows 3 toolbar buttons. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets( @@ -2433,7 +2583,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2451,7 +2601,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -2464,7 +2614,7 @@ void main() { // you tapped instead of the edge like every other single tap. This is // likely a bug in iOS 12 and not present in other versions. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 6); // No toolbar. expect(find.byType(CupertinoButton), findsNothing); @@ -2949,18 +3099,18 @@ void main() { await tester.longPressAt(ePos); await tester.pump(const Duration(milliseconds: 50)); - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; if (kIsWeb) { expect(find.byType(CupertinoButton), findsNothing); } else { - expect(find.byType(CupertinoButton), findsNWidgets(isTargetPlatformMobile ? 2 : 1)); + expect(find.byType(CupertinoButton), findsNWidgets(isTargetPlatformIOS ? 2 : 1)); } expect(controller.selection.isCollapsed, isTrue); expect(controller.selection.baseOffset, 6); // Tap in a slightly different position to avoid hitting the context menu // on desktop. - final Offset secondTapPos = isTargetPlatformMobile + final Offset secondTapPos = isTargetPlatformIOS ? ePos : ePos + const Offset(-1.0, 0.0); await tester.tapAt(secondTapPos); @@ -3308,7 +3458,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -3326,7 +3476,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor to the beginning of the second word. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -3360,7 +3510,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -3388,7 +3538,7 @@ void main() { if (isContextMenuProvidedByPlatform) { expect(find.byType(CupertinoButton), findsNothing); } else { - expect(find.byType(CupertinoButton), isTargetPlatformMobile ? findsNWidgets(2) : findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(2) : findsNWidgets(1)); } await tester.tapAt(pPos); @@ -3397,7 +3547,7 @@ void main() { // First tap moved the cursor. expect(find.byType(CupertinoButton), findsNothing); expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -3408,7 +3558,7 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); // Shows toolbar. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets( @@ -3428,6 +3578,7 @@ void main() { ); final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(textFieldStart + const Offset(50.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3441,7 +3592,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Double tap selecting the same word somewhere else is fine. await tester.tapAt(textFieldStart + const Offset(100.0, 5.0)); @@ -3461,7 +3612,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3477,7 +3628,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); group('Triple tap/click', () { @@ -3607,6 +3758,121 @@ void main() { variant: TargetPlatformVariant.mobile(), ); + testWidgets( + 'Triple click at the beginning of a line should not select the previous paragraph', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132126 + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + maxLines: null, + ), + ), + ), + ); + + await tester.enterText(find.byType(CupertinoTextField), testValueB); + // Skip past scrolling animation. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + expect(controller.value.text, testValueB); + + final Offset thirdLinePos = textOffsetToPosition(tester, 38); + + // Click on text field to gain focus, and move the selection. + final TestGesture gesture = await tester.startGesture(thirdLinePos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.isCollapsed, true); + expect(controller.selection.baseOffset, 38); + + // Here we click on same position again, to register a double click. This will select + // the word at the clicked position. + await gesture.down(thirdLinePos); + await gesture.up(); + + expect(controller.selection.baseOffset, 38); + expect(controller.selection.extentOffset, 40); + + // Here we click on same position again, to register a triple click. This will select + // the paragraph at the clicked position. + await gesture.down(thirdLinePos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(controller.selection.baseOffset, 38); + expect(controller.selection.extentOffset, 57); + }, + variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), + ); + + testWidgets( + 'Triple click at the end of text should select the previous paragraph', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132126. + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + maxLines: null, + ), + ), + ), + ); + + await tester.enterText(find.byType(CupertinoTextField), testValueB); + // Skip past scrolling animation. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + expect(controller.value.text, testValueB); + + final Offset endOfTextPos = textOffsetToPosition(tester, 74); + + // Click on text field to gain focus, and move the selection. + final TestGesture gesture = await tester.startGesture(endOfTextPos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.isCollapsed, true); + expect(controller.selection.baseOffset, 74); + + // Here we click on same position again, to register a double click. + await gesture.down(endOfTextPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.baseOffset, 74); + expect(controller.selection.extentOffset, 74); + + // Here we click on same position again, to register a triple click. This will select + // the paragraph at the clicked position. + await gesture.down(endOfTextPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(controller.selection.baseOffset, 57); + expect(controller.selection.extentOffset, 74); + }, + variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), + ); + testWidgets( 'triple tap chains work on Non-Apple mobile platforms', (WidgetTester tester) async { @@ -3717,6 +3983,7 @@ void main() { ); final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3729,7 +3996,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); await tester.pumpAndSettle(kDoubleTapTimeout); @@ -3755,7 +4022,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); // Third tap shows the toolbar and selects the paragraph. await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); @@ -3764,7 +4031,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 36), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); await tester.tapAt(textfieldStart + const Offset(150.0, 25.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3782,7 +4049,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 44, extentOffset: 50), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); // Third tap selects the paragraph and shows the toolbar. await tester.tapAt(textfieldStart + const Offset(150.0, 25.0)); @@ -3791,7 +4058,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 36, extentOffset: 66), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : (isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3))); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); @@ -4794,7 +5061,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -4824,7 +5091,7 @@ void main() { // Fall back to a single tap which selects the edge of the word on iOS, and // a precise position on macOS. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 12 : 9); await tester.pump(); // Falling back to a single tap doesn't trigger a toolbar. @@ -4837,7 +5104,7 @@ void main() { ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. // On macOS, we select the precise position of the tap. - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( @@ -4856,7 +5123,7 @@ void main() { await tester.tapAt(ePos, pointer: 7); await tester.pump(const Duration(milliseconds: 50)); expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 5); + expect(controller.selection.baseOffset, isTargetPlatformIOS ? 7 : 5); await tester.tapAt(ePos, pointer: 7); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 4); @@ -6568,8 +6835,9 @@ void main() { bottomLeftSelectionPosition.translate(0, 8 + 0.1), ], includes: <Offset> [ - // Expected center of the arrow. - Offset(26.0, bottomLeftSelectionPosition.dy + 8 + 0.1), + // Expected center of the arrow. The arrow should stay clear of + // the edges of the selection toolbar. + Offset(26.0, bottomLeftSelectionPosition.dy + 8.0 + 0.1), ], ), ), @@ -6582,7 +6850,7 @@ void main() { topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), leftMatcher: moreOrLessEquals(8), rightMatcher: lessThanOrEqualTo(400 - 8), - bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01), + bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01), ), ), ); @@ -6642,7 +6910,7 @@ void main() { pathMatcher: PathBoundsMatcher( topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), rightMatcher: moreOrLessEquals(400.0 - 8), - bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01), + bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01), leftMatcher: greaterThanOrEqualTo(8), ), ), @@ -6695,7 +6963,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01), - topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01), + topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01), rightMatcher: lessThanOrEqualTo(400 - 8), leftMatcher: greaterThanOrEqualTo(8), ), @@ -6764,7 +7032,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01), - topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01), + topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01), rightMatcher: lessThanOrEqualTo(400 - 8), leftMatcher: greaterThanOrEqualTo(8), ), @@ -6837,7 +7105,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01), - topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01), + topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01), rightMatcher: lessThanOrEqualTo(400 - 8), leftMatcher: greaterThanOrEqualTo(8), ), @@ -7832,9 +8100,7 @@ void main() { await tester.pumpWidget( const CupertinoApp( home: Center( - child: CupertinoTextField( - enabled: true, - ), + child: CupertinoTextField(), ), ), ); @@ -8142,7 +8408,7 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -8167,7 +8433,7 @@ void main() { kind: PointerDeviceKind.mouse, ); await tester.pumpAndSettle(); - if (isTargetPlatformMobile) { + if (isTargetPlatformIOS) { await gesture.up(); // Not a double tap + drag. await tester.pumpAndSettle(kDoubleTapTimeout); @@ -8176,7 +8442,7 @@ void main() { expect(controller.selection.extentOffset, 23); // Expand the selection a bit. - if (isTargetPlatformMobile) { + if (isTargetPlatformIOS) { await gesture.down(textOffsetToPosition(tester, 24)); await tester.pumpAndSettle(); } @@ -8355,7 +8621,7 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); - final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -8380,7 +8646,7 @@ void main() { kind: PointerDeviceKind.mouse, ); await tester.pumpAndSettle(); - if (isTargetPlatformMobile) { + if (isTargetPlatformIOS) { await gesture.up(); // Not a double tap + drag. await tester.pumpAndSettle(kDoubleTapTimeout); @@ -8390,7 +8656,7 @@ void main() { expect(controller.selection.extentOffset, 8); // Expand the selection a bit. - if (isTargetPlatformMobile) { + if (isTargetPlatformIOS) { await gesture.down(textOffsetToPosition(tester, 7)); await tester.pumpAndSettle(); } @@ -9603,4 +9869,59 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }), ); + + testWidgets('Does not shrink in height when enters text when there is large single-line placeholder', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/133241. + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + CupertinoApp( + home: Align( + alignment: Alignment.topCenter, + child: CupertinoTextField( + placeholderStyle: const TextStyle(fontSize: 100), + placeholder: 'p', + controller: controller, + ), + ), + ), + ); + + final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField)); + controller.value = const TextEditingValue(text: 'input'); + await tester.pump(); + + final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField)); + expect(rectWithPlaceholder, rectWithText); + }); + + testWidgets('Does not match the height of a multiline placeholder', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + CupertinoApp( + home: Align( + alignment: Alignment.topCenter, + child: CupertinoTextField( + placeholderStyle: const TextStyle(fontSize: 100), + placeholder: 'p' * 50, + maxLines: null, + controller: controller, + ), + ), + ), + ); + + final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField)); + controller.value = const TextEditingValue(text: 'input'); + await tester.pump(); + + final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField)); + // The text field is still top aligned. + expect(rectWithPlaceholder.top, rectWithText.top); + // But after entering text the text field should shrink since the + // placeholder text is huge and multiline. + expect(rectWithPlaceholder.height, greaterThan(rectWithText.height)); + // But still should be taller than or the same height of the first line of + // placeholder. + expect(rectWithText.height, greaterThan(100)); + }); } diff --git a/packages/flutter/test/cupertino/text_form_field_row_test.dart b/packages/flutter/test/cupertino/text_form_field_row_test.dart index 830431848e653..9ddff783b6cbd 100644 --- a/packages/flutter/test/cupertino/text_form_field_row_test.dart +++ b/packages/flutter/test/cupertino/text_form_field_row_test.dart @@ -6,8 +6,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { testWidgets('Passes textAlign to underlying CupertinoTextField', (WidgetTester tester) async { const TextAlign alignment = TextAlign.center; @@ -492,4 +490,43 @@ void main() { final CupertinoTextField rtlTextFieldWidget = tester.widget(rtlTextFieldFinder); expect(rtlTextFieldWidget.textDirection, TextDirection.rtl); }); + + testWidgets('CupertinoTextFormFieldRow onChanged is called when the form is reset', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/123009. + final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>(); + final GlobalKey<FormState> formKey = GlobalKey<FormState>(); + String value = 'initialValue'; + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: Form( + key: formKey, + child: CupertinoTextFormFieldRow( + key: stateKey, + initialValue: value, + onChanged: (String newValue) { + value = newValue; + }, + ), + ), + ), + ), + ); + + // Initial value is 'initialValue'. + expect(stateKey.currentState!.value, 'initialValue'); + expect(value, 'initialValue'); + + // Change value to 'changedValue'. + await tester.enterText(find.byType(CupertinoTextField), 'changedValue'); + expect(stateKey.currentState!.value,'changedValue'); + expect(value, 'changedValue'); + + // Should be back to 'initialValue' when the form is reset. + formKey.currentState!.reset(); + await tester.pump(); + expect(stateKey.currentState!.value,'initialValue'); + expect(value, 'initialValue'); + }); } diff --git a/packages/flutter/test/cupertino/text_selection_test.dart b/packages/flutter/test/cupertino/text_selection_test.dart index b679eb6d46f00..31843d7e5f42b 100644 --- a/packages/flutter/test/cupertino/text_selection_test.dart +++ b/packages/flutter/test/cupertino/text_selection_test.dart @@ -179,10 +179,13 @@ void main() { }); group('Text selection menu overflow (iOS)', () { - Finder findOverflowNextButton() => find.byWidgetPredicate((Widget widget) => + Finder findOverflowNextButton() { + return find.byWidgetPredicate((Widget widget) => widget is CustomPaint && - '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', - ); + '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', + ); + } + Finder findOverflowBackButton() => find.byWidgetPredicate((Widget widget) => widget is CustomPaint && '${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter', @@ -247,7 +250,7 @@ void main() { testWidgets("When a menu item doesn't fit, a second page is used.", (WidgetTester tester) async { // Set the screen size to more narrow, so that Paste can't fit. - tester.view.physicalSize = const Size(800, 800); + tester.view.physicalSize = const Size(1000, 800); addTearDown(tester.view.reset); final TextEditingController controller = TextEditingController(text: 'abc def ghi'); @@ -265,11 +268,23 @@ void main() { ), )); + Future<void> tapNextButton() async { + await tester.tapAt(tester.getCenter(findOverflowNextButton())); + await tester.pumpAndSettle(); + } + + Future<void> tapBackButton() async { + await tester.tapAt(tester.getCenter(findOverflowBackButton())); + await tester.pumpAndSettle(); + } + // Initially, the menu isn't shown at all. expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsNothing); @@ -285,27 +300,54 @@ void main() { expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsOneWidget); - // Tapping the next button shows the overflowing button and the next - // button is hidden as the last page is shown. - await tester.tapAt(tester.getCenter(findOverflowNextButton())); - await tester.pumpAndSettle(); + // Tapping the next button shows both the overflow, back, and next buttons. + await tapNextButton(); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); expect(findOverflowBackButton(), findsOneWidget); - expect(findOverflowNextButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); - // Tapping the back button shows the first page again with the next button. - await tester.tapAt(tester.getCenter(findOverflowBackButton())); - await tester.pumpAndSettle(); + // Tapping the next button shows the next, back, and Look Up button + await tapNextButton(); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsOneWidget); + expect(find.text('Search Web'), findsNothing); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); + + // Tapping the next button shows the back and Search Web button + await tapNextButton(); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsOneWidget); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); + + // Tapping the back button thrice shows the first page again with the next button. + await tapBackButton(); + await tapBackButton(); + await tapBackButton(); expect(find.text('Cut'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsOneWidget); }, @@ -334,12 +376,25 @@ void main() { ), )); + Future<void> tapNextButton() async { + await tester.tapAt(tester.getCenter(findOverflowNextButton())); + await tester.pumpAndSettle(); + } + + Future<void> tapBackButton() async { + await tester.tapAt(tester.getCenter(findOverflowBackButton())); + await tester.pumpAndSettle(); + } + // Initially, the menu isn't shown at all. expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsNothing); @@ -356,51 +411,88 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsOneWidget); // Tapping the next button shows Copy. - await tester.tapAt(tester.getCenter(findOverflowNextButton())); - await tester.pumpAndSettle(); + await tapNextButton(); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3)); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget); - // Tapping the next button again shows Paste and hides the next button as - // the last page is shown. - await tester.tapAt(tester.getCenter(findOverflowNextButton())); - await tester.pumpAndSettle(); - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2)); + // Tapping the next button again shows Paste + await tapNextButton(); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3)); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsOneWidget); - expect(findOverflowNextButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); - // Tapping the back button shows the second page again with the next button. - await tester.tapAt(tester.getCenter(findOverflowBackButton())); - await tester.pumpAndSettle(); - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3)); + // Tapping the next button again shows the Look Up Button. + await tapNextButton(); expect(find.text('Cut'), findsNothing); - expect(find.text('Copy'), findsOneWidget); + expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsOneWidget); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget); - // Tapping the back button again shows the first page again. - await tester.tapAt(tester.getCenter(findOverflowBackButton())); - await tester.pumpAndSettle(); + // Tapping the next button again shows the Search Web Button. + await tapNextButton(); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsOneWidget); + expect(find.text('Share...'), findsNothing); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); + + // Tapping the next button again shows the last page and the Share button + await tapNextButton(); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsOneWidget); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); + + // Tapping the back button 5 times shows the first page again. + await tapBackButton(); + await tapBackButton(); + await tapBackButton(); + await tapBackButton(); + await tapBackButton(); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2)); expect(find.text('Cut'), findsOneWidget); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); + expect(find.text('Look Up'), findsNothing); + expect(find.text('Search Web'), findsNothing); + expect(find.text('Share...'), findsNothing); expect(findOverflowBackButton(), findsNothing); expect(findOverflowNextButton(), findsOneWidget); }, @@ -485,7 +577,7 @@ void main() { expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget); - // Tap next to go to the third and final page. + // Tap twice to go to the third page. await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); @@ -493,7 +585,7 @@ void main() { expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(findOverflowBackButton(), findsOneWidget); - expect(findOverflowNextButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); // Tap back to go to the second page again. await tester.tapAt(tester.getCenter(findOverflowBackButton())); @@ -579,8 +671,8 @@ void main() { final Offset textFieldOffset = tester.getTopLeft(find.byType(CupertinoTextField)); - // 7.0 + 45.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding - expect(selectionOffset.dy + 7.0 + 45.0 + 8.0 - 8.0, equals(textFieldOffset.dy)); + // 7.0 + 44.0 + 8.0 - 8.0 = _kToolbarArrowSize + text_button_height + _kToolbarContentDistance - padding + expect(selectionOffset.dy + 7.0 + 44.0 + 8.0 - 8.0, equals(textFieldOffset.dy)); }, skip: isBrowser, // [intended] the selection menu isn't required by web variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart index 76eee86317a7f..575b284abdbca 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('can press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can press', (WidgetTester tester) async { bool pressed = false; await tester.pumpWidget( CupertinoApp( @@ -29,7 +30,7 @@ void main() { expect(pressed, true); }); - testWidgets('background darkens when pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('background darkens when pressed', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -75,7 +76,7 @@ void main() { expect(boxDecoration.color, const Color(0x00000000)); }); - testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passing null to onPressed disables the button', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart index 5c110b22e9620..dd87a1eb7fc55 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart @@ -2,17 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(<String>['reduced-test-set']) +library; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; // These constants are copied from cupertino/text_selection_toolbar.dart. const double _kArrowScreenPadding = 26.0; const double _kToolbarContentDistance = 8.0; -const double _kToolbarHeight = 45.0; +const Size _kToolbarArrowSize = Size(14.0, 7.0); // A custom text selection menu that just displays a single custom button. class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls { @@ -93,16 +98,21 @@ void main() { ..line(p1: const Offset(7.5, 0), p2: const Offset(2.5, 5)) ..line(p1: const Offset(2.5, 5), p2: const Offset(7.5, 10)); - Finder findOverflowNextButton() => find.byWidgetPredicate((Widget widget) => + Finder findOverflowNextButton() { + return find.byWidgetPredicate((Widget widget) => widget is CustomPaint && - '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', - ); - Finder findOverflowBackButton() => find.byWidgetPredicate((Widget widget) => + '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', + ); + } + + Finder findOverflowBackButton() { + return find.byWidgetPredicate((Widget widget) => widget is CustomPaint && - '${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter', - ); + '${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter', + ); + } - testWidgets('chevrons point to the correct side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('chevrons point to the correct side', (WidgetTester tester) async { // Add enough TestBoxes to need 3 pages. final List<Widget> children = List<Widget>.generate(15, (int i) => const TestBox()); await tester.pumpWidget( @@ -142,7 +152,7 @@ void main() { expect(findOverflowBackButton(), overflowBackPaintPattern()); }, skip: kIsWeb); // Path.combine is not implemented in the HTML backend https://github.com/flutter/flutter/issues/44572 - testWidgets('paginates children if they overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('paginates children if they overflow', (WidgetTester tester) async { late StateSetter setState; final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox()); await tester.pumpWidget( @@ -237,7 +247,7 @@ void main() { expect(findOverflowBackButton(), findsNothing); }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. - testWidgets('does not paginate if children fit with zero margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not paginate if children fit with zero margin', (WidgetTester tester) async { final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox()); final double spacerWidth = 1.0 / tester.view.devicePixelRatio; final double dividerWidth = 1.0 / tester.view.devicePixelRatio; @@ -264,9 +274,9 @@ void main() { expect(findOverflowBackButton(), findsNothing); }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. - testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at anchorAbove if it fits', (WidgetTester tester) async { late StateSetter setState; - const double height = _kToolbarHeight; + const double height = 50.0; const double anchorBelowY = 500.0; double anchorAboveY = 0.0; const double paddingAbove = 12.0; @@ -327,10 +337,10 @@ void main() { }); await tester.pump(); toolbarY = tester.getTopLeft(findToolbar()).dy; - expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance)); + expect(toolbarY, equals(anchorAboveY - height + _kToolbarArrowSize.height - _kToolbarContentDistance)); }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. - testWidgets('can create and use a custom toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can create and use a custom toolbar', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Select me custom menu', ); @@ -365,7 +375,7 @@ void main() { for (final Brightness? themeBrightness in <Brightness?>[...Brightness.values, null]) { for (final Brightness? mediaBrightness in <Brightness?>[...Brightness.values, null]) { - testWidgets('draws dark buttons in dark mode and light button in light mode when theme is $themeBrightness and MediaQuery is $mediaBrightness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('draws dark buttons in dark mode and light button in light mode when theme is $themeBrightness and MediaQuery is $mediaBrightness', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( theme: CupertinoThemeData( @@ -422,9 +432,9 @@ void main() { } } - testWidgets('draws a shadow below the toolbar in light mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('draws a shadow below the toolbar in light mode', (WidgetTester tester) async { late StateSetter setState; - const double height = _kToolbarHeight; + const double height = 50.0; double anchorAboveY = 0.0; await tester.pumpWidget( @@ -463,20 +473,15 @@ void main() { ), ); - // When the toolbar is below the content, the shadow hangs below the entire - // toolbar. - final Finder finder = find.descendant( - of: find.byType(CupertinoTextSelectionToolbar), - matching: find.byType(DecoratedBox), + final double dividerWidth = 1.0 / tester.view.devicePixelRatio; + + expect( + find.byType(CupertinoTextSelectionToolbar), + paints..rrect( + rrect: RRect.fromLTRBR(8.0, 515.0, 158.0 + 2 * dividerWidth, 558.0, const Radius.circular(8.0)), + color: const Color(0x33000000), + ), ); - expect(finder, findsOneWidget); - DecoratedBox decoratedBox = tester.widget(finder.first); - BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration; - List<BoxShadow>? shadows = boxDecoration.boxShadow; - expect(shadows, isNotNull); - expect(shadows, hasLength(1)); - BoxShadow shadow = boxDecoration.boxShadow!.first; - expect(shadow.offset.dy, equals(7.0)); // When the toolbar is above the content, the shadow sits around the arrow // with no offset. @@ -484,12 +489,60 @@ void main() { anchorAboveY = 80.0; }); await tester.pump(); - decoratedBox = tester.widget(finder.first); - boxDecoration = decoratedBox.decoration as BoxDecoration; - shadows = boxDecoration.boxShadow; - expect(shadows, isNotNull); - expect(shadows, hasLength(1)); - shadow = boxDecoration.boxShadow!.first; - expect(shadow.offset.dy, equals(0.0)); + + expect( + find.byType(CupertinoTextSelectionToolbar), + paints..rrect( + rrect: RRect.fromLTRBR(8.0, 29.0, 158.0 + 2 * dividerWidth, 72.0, const Radius.circular(8.0)), + color: const Color(0x33000000), + ), + ); + }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. + + testWidgetsWithLeakTracking('Basic golden tests', (WidgetTester tester) async { + final Key key = UniqueKey(); + Widget buildToolbar(Brightness brightness, Offset offset) { + final Widget toolbar = CupertinoTextSelectionToolbar( + anchorAbove: offset, + anchorBelow: offset, + children: <Widget>[ + CupertinoTextSelectionToolbarButton.text(onPressed: () {}, text: 'Lorem ipsum'), + CupertinoTextSelectionToolbarButton.text(onPressed: () {}, text: 'dolor sit amet'), + CupertinoTextSelectionToolbarButton.text(onPressed: () {}, text: 'Lorem ipsum \ndolor sit amet'), + CupertinoTextSelectionToolbarButton.buttonItem(buttonItem: ContextMenuButtonItem(onPressed: () {}, type: ContextMenuButtonType.copy)), + ], + ); + return CupertinoApp( + theme: CupertinoThemeData(brightness: brightness), + home: Center( + child: SizedBox( + height: 200, + child: RepaintBoundary(key: key, child: toolbar), + ), + ), + ); + } + + // The String describes the location of the toolbar in relation to the + // content the arrow points to. + const List<(String, Offset)> toolbarLocation = <(String, Offset)>[ + ('BottomRight', Offset.zero), + ('BottomLeft', Offset(100000, 0)), + ('TopRight', Offset(0, 100)), + ('TopLeft', Offset(100000, 100)), + ]; + + debugDisableShadows = false; + addTearDown(() => debugDisableShadows = true); + for (final Brightness brightness in Brightness.values) { + for (final (String location, Offset offset) in toolbarLocation) { + await tester.pumpWidget(buildToolbar(brightness, offset)); + await expectLater( + find.byKey(key), + matchesGoldenFile('cupertino_selection_toolbar.$location.$brightness.png'), + ); + } + } + debugDisableShadows = true; }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. } diff --git a/packages/flutter/test/cupertino/theme_test.dart b/packages/flutter/test/cupertino/theme_test.dart index 5f8b9a372f111..8ee7c9deb1de7 100644 --- a/packages/flutter/test/cupertino/theme_test.dart +++ b/packages/flutter/test/cupertino/theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; int buildCount = 0; CupertinoThemeData? actualTheme; @@ -46,7 +47,7 @@ void main() { actualIconTheme = null; }); - testWidgets('Default theme has defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default theme has defaults', (WidgetTester tester) async { final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData()); expect(theme.brightness, isNull); @@ -55,7 +56,7 @@ void main() { expect(theme.applyThemeToAll, false); }); - testWidgets('Theme attributes cascade', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme attributes cascade', (WidgetTester tester) async { final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData( primaryColor: CupertinoColors.systemRed, )); @@ -63,7 +64,7 @@ void main() { expect(theme.textTheme.actionTextStyle.color, isSameColorAs(CupertinoColors.systemRed.color)); }); - testWidgets('Dependent attribute can be overridden from cascaded value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dependent attribute can be overridden from cascaded value', (WidgetTester tester) async { final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData( brightness: Brightness.dark, textTheme: CupertinoTextThemeData( @@ -77,7 +78,7 @@ void main() { expect(theme.textTheme.textStyle.color, isSameColorAs(CupertinoColors.black)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Reading themes creates dependencies', (WidgetTester tester) async { // Reading the theme creates a dependency. @@ -118,7 +119,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'copyWith works', (WidgetTester tester) async { const CupertinoThemeData originalTheme = CupertinoThemeData( @@ -141,7 +142,7 @@ void main() { }, ); - testWidgets("Theme has default IconThemeData, which is derived from the theme's primary color", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Theme has default IconThemeData, which is derived from the theme's primary color", (WidgetTester tester) async { const CupertinoDynamicColor primaryColor = CupertinoColors.systemRed; const CupertinoThemeData themeData = CupertinoThemeData(primaryColor: primaryColor); @@ -158,7 +159,7 @@ void main() { expect(darkColor, isSameColorAs(primaryColor.darkColor)); }); - testWidgets('IconTheme.of creates a dependency on iconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme.of creates a dependency on iconTheme', (WidgetTester tester) async { IconThemeData iconTheme = await testIconTheme(tester, const CupertinoThemeData(primaryColor: CupertinoColors.destructiveRed)); expect(buildCount, 1); @@ -169,7 +170,7 @@ void main() { expect(iconTheme.color, CupertinoColors.activeOrange); }); - testWidgets('CupertinoTheme diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTheme diagnostics', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CupertinoThemeData().debugFillProperties(builder); @@ -201,7 +202,7 @@ void main() { ); }); - testWidgets('CupertinoTheme.toStringDeep uses single-line style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTheme.toStringDeep uses single-line style', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/47651. expect( const CupertinoTheme( @@ -212,7 +213,7 @@ void main() { ); }); - testWidgets('CupertinoThemeData equality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoThemeData equality', (WidgetTester tester) async { const CupertinoThemeData a = CupertinoThemeData(brightness: Brightness.dark); final CupertinoThemeData b = a.copyWith(); final CupertinoThemeData c = a.copyWith(brightness: Brightness.light); @@ -235,7 +236,7 @@ void main() { } void dynamicColorsTestGroup() { - testWidgets('CupertinoTheme.of resolves colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTheme.of resolves colors', (WidgetTester tester) async { final CupertinoThemeData data = CupertinoThemeData(brightness: currentBrightness, primaryColor: CupertinoColors.systemRed); final CupertinoThemeData theme = await testTheme(tester, data); @@ -243,7 +244,7 @@ void main() { colorMatches(theme.primaryColor, CupertinoColors.systemRed); }); - testWidgets('CupertinoTheme.of resolves default values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CupertinoTheme.of resolves default values', (WidgetTester tester) async { const CupertinoDynamicColor primaryColor = CupertinoColors.systemRed; final CupertinoThemeData data = CupertinoThemeData(brightness: currentBrightness, primaryColor: primaryColor); diff --git a/packages/flutter/test/flutter_test_config.dart b/packages/flutter/test/flutter_test_config.dart index 0be2e3a439b64..5600ab04609d6 100644 --- a/packages/flutter/test/flutter_test_config.dart +++ b/packages/flutter/test/flutter_test_config.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker/leak_tracker.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '_goldens_io.dart' if (dart.library.html) '_goldens_web.dart' as flutter_goldens; @@ -23,7 +24,10 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) { // receive the event. WidgetController.hitTestWarningShouldBeFatal = true; - LeakTrackingTestConfig.warnForNonSupportedPlatforms = false; + LeakTracking.warnForUnsupportedPlatforms = false; + setLeakTrackingTestSettings( + LeakTrackingTestSettings(switches: const Switches(disableNotGCed: true)) + ); // Enable golden file testing using Skia Gold. return flutter_goldens.testExecutable(testMain); diff --git a/packages/flutter/test/foundation/change_notifier_test.dart b/packages/flutter/test/foundation/change_notifier_test.dart index 661431e6c4a97..6bb678a957002 100644 --- a/packages/flutter/test/foundation/change_notifier_test.dart +++ b/packages/flutter/test/foundation/change_notifier_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestNotifier extends ChangeNotifier { void notify() { @@ -28,6 +27,12 @@ class A { } class B extends A with ChangeNotifier { + B() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + @override void test() { notifyListeners(); @@ -36,6 +41,12 @@ class B extends A with ChangeNotifier { } class Counter with ChangeNotifier { + Counter() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + int get value => _value; int _value = 0; set value(int value) { diff --git a/packages/flutter/test/foundation/leak_tracking.dart b/packages/flutter/test/foundation/leak_tracking.dart deleted file mode 100644 index 26a3dbd7a0bac..0000000000000 --- a/packages/flutter/test/foundation/leak_tracking.dart +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:core'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leak_tracker/leak_tracker.dart'; -import 'package:leak_tracker_testing/leak_tracker_testing.dart'; -import 'package:meta/meta.dart'; - -export 'package:leak_tracker/leak_tracker.dart' show LeakDiagnosticConfig, LeakTrackingTestConfig; - -/// Set of objects, that does not hold the objects from garbage collection. -/// -/// The objects are referenced by hash codes and can duplicate with low probability. -@visibleForTesting -class WeakSet { - final Set<String> _objectCodes = <String>{}; - - String _toCode(int hashCode, String type) => '$type-$hashCode'; - - void add(Object object) { - _objectCodes.add(_toCode(identityHashCode(object), object.runtimeType.toString())); - } - - void addByCode(int hashCode, String type) { - _objectCodes.add(_toCode(hashCode, type)); - } - - bool contains(int hashCode, String type) { - final bool result = _objectCodes.contains(_toCode(hashCode, type)); - return result; - } -} - -/// Wrapper for [testWidgets] with memory leak tracking. -/// -/// The method will fail if instrumented objects in [callback] are -/// garbage collected without being disposed. -/// -/// More about leak tracking: -/// https://github.com/dart-lang/leak_tracker. -/// -/// See https://github.com/flutter/devtools/issues/3951 for plans -/// on leak tracking. -@isTest -void testWidgetsWithLeakTracking( - String description, - WidgetTesterCallback callback, { - bool? skip, - Timeout? timeout, - bool semanticsEnabled = true, - TestVariant<Object?> variant = const DefaultTestVariant(), - dynamic tags, - LeakTrackingTestConfig leakTrackingConfig = const LeakTrackingTestConfig(), -}) { - Future<void> wrappedCallback(WidgetTester tester) async { - await _withFlutterLeakTracking( - () async => callback(tester), - tester, - leakTrackingConfig, - ); - } - - testWidgets( - description, - wrappedCallback, - skip: skip, - timeout: timeout, - semanticsEnabled: semanticsEnabled, - variant: variant, - tags: tags, - ); -} - -bool _webWarningPrinted = false; - -/// Runs [callback] with leak tracking. -/// -/// Wrapper for [withLeakTracking] with Flutter specific functionality. -/// -/// The method will fail if wrapped code contains memory leaks. -/// -/// See details in documentation for `withLeakTracking` at -/// https://github.com/dart-lang/leak_tracker/blob/main/lib/src/leak_tracking/orchestration.dart -/// -/// The Flutter related enhancements are: -/// 1. Listens to [MemoryAllocations] events. -/// 2. Uses `tester.runAsync` for leak detection if [tester] is provided. -/// -/// Pass [config] to troubleshoot or exempt leaks. See [LeakTrackingTestConfig] -/// for details. -Future<void> _withFlutterLeakTracking( - DartAsyncCallback callback, - WidgetTester tester, - LeakTrackingTestConfig config, -) async { - // Leak tracker does not work for web platform. - if (kIsWeb) { - final bool shouldPrintWarning = !_webWarningPrinted && LeakTrackingTestConfig.warnForNonSupportedPlatforms; - if (shouldPrintWarning) { - _webWarningPrinted = true; - debugPrint('Leak tracking is not supported on web platform.\nTo turn off this message, set `LeakTrackingTestConfig.warnForNonSupportedPlatforms` to false.'); - } - await callback(); - return; - } - - void flutterEventToLeakTracker(ObjectEvent event) { - return dispatchObjectEvent(event.toMap()); - } - - return TestAsyncUtils.guard<void>(() async { - MemoryAllocations.instance.addListener(flutterEventToLeakTracker); - Future<void> asyncCodeRunner(DartAsyncCallback action) async => tester.runAsync(action); - - try { - Leaks leaks = await withLeakTracking( - callback, - asyncCodeRunner: asyncCodeRunner, - leakDiagnosticConfig: config.leakDiagnosticConfig, - shouldThrowOnLeaks: false, - ); - - leaks = LeakCleaner(config).clean(leaks); - - if (leaks.total > 0) { - config.onLeaks?.call(leaks); - if (config.failTestOnLeaks) { - expect(leaks, isLeakFree); - } - } - } finally { - MemoryAllocations.instance.removeListener(flutterEventToLeakTracker); - } - }); -} - -/// Cleans leaks that are allowed by [config]. -@visibleForTesting -class LeakCleaner { - LeakCleaner(this.config); - - final LeakTrackingTestConfig config; - - static Map<(String, LeakType), int> _countByClassAndType(Leaks leaks) { - final Map<(String, LeakType), int> result = <(String, LeakType), int>{}; - - for (final MapEntry<LeakType, List<LeakReport>> entry in leaks.byType.entries) { - for (final LeakReport leak in entry.value) { - final (String, LeakType) classAndType = (leak.type, entry.key); - result[classAndType] = (result[classAndType] ?? 0) + 1; - } - } - return result; - } - - Leaks clean(Leaks leaks) { - final Map<(String, LeakType), int> countByClassAndType = _countByClassAndType(leaks); - - final Leaks result = Leaks(<LeakType, List<LeakReport>>{ - for (final LeakType leakType in leaks.byType.keys) - leakType: leaks.byType[leakType]!.where((LeakReport leak) => _shouldReportLeak(leakType, leak, countByClassAndType)).toList() - }); - return result; - } - - /// Returns true if [leak] should be reported as failure. - bool _shouldReportLeak(LeakType leakType, LeakReport leak, Map<(String, LeakType), int> countByClassAndType) { - // Tracking for non-GCed is temporarily disabled. - // TODO(polina-c): turn on tracking for non-GCed after investigating existing leaks. - if (leakType != LeakType.notDisposed) { - return false; - } - - final String leakingClass = leak.type; - final (String, LeakType) classAndType = (leakingClass, leakType); - - bool isAllowedForClass(Map<String, int?> allowList) { - if (!allowList.containsKey(leakingClass)) { - return false; - } - final int? allowedCount = allowList[leakingClass]; - if (allowedCount == null) { - return true; - } - return allowedCount >= countByClassAndType[classAndType]!; - } - - switch (leakType) { - case LeakType.notDisposed: - return !isAllowedForClass(config.notDisposedAllowList); - case LeakType.notGCed: - case LeakType.gcedLate: - return !isAllowedForClass(config.notGCedAllowList); - } - } -} diff --git a/packages/flutter/test/foundation/leak_tracking_test.dart b/packages/flutter/test/foundation/leak_tracking_test.dart deleted file mode 100644 index fc71814d260ff..0000000000000 --- a/packages/flutter/test/foundation/leak_tracking_test.dart +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leak_tracker/leak_tracker.dart'; -import 'package:leak_tracker_testing/leak_tracker_testing.dart'; - -import 'leak_tracking.dart'; - -final String _leakTrackedClassName = '$_LeakTrackedClass'; - -Leaks _leaksOfAllTypes() => Leaks(<LeakType, List<LeakReport>> { - LeakType.notDisposed: <LeakReport>[LeakReport(code: 1, context: <String, dynamic>{}, type:'myNotDisposedClass', trackedClass: 'myTrackedClass')], - LeakType.notGCed: <LeakReport>[LeakReport(code: 2, context: <String, dynamic>{}, type:'myNotGCedClass', trackedClass: 'myTrackedClass')], - LeakType.gcedLate: <LeakReport>[LeakReport(code: 3, context: <String, dynamic>{}, type:'myGCedLateClass', trackedClass: 'myTrackedClass')], -}); - -Future<void> main() async { - test('Trivial $LeakCleaner returns only non-disposed leaks.', () { - final LeakCleaner leakCleaner = LeakCleaner(const LeakTrackingTestConfig()); - final Leaks leaks = _leaksOfAllTypes(); - final int leakTotal = leaks.total; - - final Leaks cleanedLeaks = leakCleaner.clean(leaks); - - expect(leaks.total, leakTotal); - expect(cleanedLeaks.total, 1); - }); - - test('$LeakCleaner catches extra leaks', () { - Leaks leaks = _leaksOfAllTypes(); - final LeakReport leak = leaks.notDisposed.first; - leaks.notDisposed.add(leak); - - final LeakTrackingTestConfig config = LeakTrackingTestConfig( - notDisposedAllowList: <String, int?>{leak.type: 1}, - ); - leaks = LeakCleaner(config).clean(leaks); - - expect(leaks.notDisposed, hasLength(2)); - }); - - group('Leak tracking works for non-web, and', () { - testWidgetsWithLeakTracking( - 'respects all allow lists', - (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - leakTrackingConfig: LeakTrackingTestConfig( - notDisposedAllowList: <String, int?>{_leakTrackedClassName: null}, - notGCedAllowList: <String, int?>{_leakTrackedClassName: null}, - ), - ); - - testWidgetsWithLeakTracking( - 'respects count in allow lists', - (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - leakTrackingConfig: LeakTrackingTestConfig( - notDisposedAllowList: <String, int?>{_leakTrackedClassName: 1}, - notGCedAllowList: <String, int?>{_leakTrackedClassName: 1}, - ), - ); - - group('fails if number or leaks is more than allowed', () { - // This test cannot run inside other tests because test nesting is forbidden. - // So, `expect` happens outside the tests, in `tearDown`. - late Leaks leaks; - - testWidgetsWithLeakTracking( - 'for $_StatelessLeakingWidget', - (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - leakTrackingConfig: LeakTrackingTestConfig( - onLeaks: (Leaks theLeaks) { - leaks = theLeaks; - }, - failTestOnLeaks: false, - notDisposedAllowList: <String, int?>{_leakTrackedClassName: 1}, - ), - ); - - tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 2)); - }); - - group('respects notGCed allow lists', () { - // These tests cannot run inside other tests because test nesting is forbidden. - // So, `expect` happens outside the tests, in `tearDown`. - late Leaks leaks; - - testWidgetsWithLeakTracking( - 'when $_StatelessLeakingWidget leaks', - (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - leakTrackingConfig: LeakTrackingTestConfig( - onLeaks: (Leaks theLeaks) { - leaks = theLeaks; - }, - failTestOnLeaks: false, - notGCedAllowList: <String, int?>{_leakTrackedClassName: null}, - ), - ); - - tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1)); - }); - - group('catches that', () { - // These test cannot run inside other tests because test nesting is forbidden. - // So, `expect` happens outside the tests, in `tearDown`. - late Leaks leaks; - - testWidgetsWithLeakTracking( - '$_StatelessLeakingWidget leaks', - (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - leakTrackingConfig: LeakTrackingTestConfig( - onLeaks: (Leaks theLeaks) { - leaks = theLeaks; - }, - failTestOnLeaks: false, - ), - ); - - tearDown(() => _verifyLeaks(leaks, expectedNotDisposed: 1)); - }); - }, - skip: isBrowser); // [intended] Leak detection is off for web. - - testWidgetsWithLeakTracking('Leak tracking is no-op for web', (WidgetTester tester) async { - await tester.pumpWidget(_StatelessLeakingWidget()); - }, - skip: !isBrowser); // [intended] Leaks detection is off for web. -} - -/// Verifies [leaks] contains expected number of leaks for [_LeakTrackedClass]. -void _verifyLeaks(Leaks leaks, { int expectedNotDisposed = 0, int expectedNotGCed = 0 }) { - const String linkToLeakTracker = 'https://github.com/dart-lang/leak_tracker'; - - expect( - () => expect(leaks, isLeakFree), - throwsA( - predicate((Object? e) { - return e is TestFailure && e.toString().contains(linkToLeakTracker); - }), - ), - ); - - _verifyLeakList(leaks.notDisposed, expectedNotDisposed); - _verifyLeakList(leaks.notGCed, expectedNotGCed); -} - -void _verifyLeakList(List<LeakReport> list, int expectedCount){ - expect(list.length, expectedCount); - - for (final LeakReport leak in list) { - expect(leak.trackedClass, contains(_LeakTrackedClass.library)); - expect(leak.trackedClass, contains(_leakTrackedClassName)); - } -} - -/// Storage to keep disposed objects, to generate not-gced leaks. -final List<_LeakTrackedClass> _notGcedStorage = <_LeakTrackedClass>[]; - -class _StatelessLeakingWidget extends StatelessWidget { - _StatelessLeakingWidget() { - // ignore: unused_local_variable, the variable is used to create non disposed leak - final _LeakTrackedClass notDisposed = _LeakTrackedClass(); - _notGcedStorage.add(_LeakTrackedClass()..dispose()); - } - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} - -class _LeakTrackedClass { - _LeakTrackedClass() { - dispatchObjectCreated( - library: library, - className: '$_LeakTrackedClass', - object: this, - ); - } - - static const String library = 'package:my_package/lib/src/my_lib.dart'; - - void dispose() { - dispatchObjectDisposed(object: this); - } -} diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 2e524b70d15e1..7b6d36a8358dc 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -116,10 +116,19 @@ Future<Map<String, dynamic>> hasReassemble(Future<Map<String, dynamic>> pendingR } void main() { + final Set<String> testedExtensions = <String>{}; // Add the name of an extension to this set in the test where it is tested. final List<String?> console = <String?>[]; + late PipelineOwner owner; setUpAll(() async { - binding = TestServiceExtensionsBinding()..scheduleFrame(); + binding = TestServiceExtensionsBinding(); + final RenderView view = RenderView(view: binding.platformDispatcher.views.single); + owner = PipelineOwner(onSemanticsUpdate: (ui.SemanticsUpdate _) { }) + ..rootNode = view; + binding.rootPipelineOwner.adoptChild(owner); + binding.addRenderView(view); + view.prepareInitialFrame(); + binding.scheduleFrame(); expect(binding.frameScheduled, isTrue); // We need to test this service extension here because the result is true @@ -145,6 +154,9 @@ void main() { expect(binding.frameScheduled, isFalse); + testedExtensions.add(WidgetsServiceExtensions.didSendFirstFrameEvent.name); + testedExtensions.add(WidgetsServiceExtensions.didSendFirstFrameRasterizedEvent.name); + expect(debugPrint, equals(debugPrintThrottled)); debugPrint = (String? message, { int? wrapWidth }) { console.add(message); @@ -154,12 +166,13 @@ void main() { tearDownAll(() async { // See widget_inspector_test.dart for tests of the ext.flutter.inspector // service extensions included in this count. - int widgetInspectorExtensionCount = 20; + int widgetInspectorExtensionCount = 28; if (WidgetInspectorService.instance.isWidgetCreationTracked()) { // Some inspector extensions are only exposed if widget creation locations // are tracked. widgetInspectorExtensionCount += 2; } + expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount)); // The following service extensions are disabled in web: // 1. exit @@ -167,15 +180,20 @@ void main() { const int disabledExtensions = kIsWeb ? 2 : 0; // The expected number of registered service extensions in the Flutter - // framework, excluding any that are for the widget inspector - // (see widget_inspector_test.dart for tests of the ext.flutter.inspector - // service extensions). - const int serviceExtensionCount = 38; + // framework, excluding any that are for the widget inspector (see + // widget_inspector_test.dart for tests of the ext.flutter.inspector service + // extensions). Any test counted here must be tested in this file! + const int serviceExtensionCount = 29; expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions); + expect(testedExtensions, hasLength(serviceExtensionCount)); expect(console, isEmpty); debugPrint = debugPrintThrottled; + binding.rootPipelineOwner.dropChild(owner); + owner + ..rootNode = null + ..dispose(); }); // The following list is alphabetical, one test per extension. @@ -201,6 +219,8 @@ void main() { expect(result, <String, String>{'enabled': 'true'}); expect(WidgetsApp.debugAllowBannerOverride, true); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(WidgetsServiceExtensions.debugAllowBanner.name); }); test('Service extensions - debugDumpApp', () async { @@ -209,6 +229,8 @@ void main() { expect(result, <String, dynamic>{ 'data': matches('TestServiceExtensionsBinding - DEBUG MODE\n<no tree currently mounted>'), }); + + testedExtensions.add(WidgetsServiceExtensions.debugDumpApp.name); }); test('Service extensions - debugDumpFocusTree', () async { @@ -222,6 +244,8 @@ void main() { r'$', ), }); + + testedExtensions.add(WidgetsServiceExtensions.debugDumpFocusTree.name); }); test('Service extensions - debugDumpRenderTree', () async { @@ -239,6 +263,8 @@ void main() { r'$', ), }); + + testedExtensions.add(RenderingServiceExtensions.debugDumpRenderTree.name); }); test('Service extensions - debugDumpLayerTree', () async { @@ -262,34 +288,44 @@ void main() { r'$', ), }); + + testedExtensions.add(RenderingServiceExtensions.debugDumpLayerTree.name); }); test('Service extensions - debugDumpSemanticsTreeInTraversalOrder', () async { await binding.doFrame(); final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{}); - expect(result, <String, String>{ - 'data': 'Semantics not generated.\n' - 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' - 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' - 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' + expect(result, <String, Object>{ + 'data': matches( + r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n' + r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' + r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n' + r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.' + ) }); + + testedExtensions.add(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name); }); test('Service extensions - debugDumpSemanticsTreeInInverseHitTestOrder', () async { await binding.doFrame(); final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{}); - expect(result, <String, String>{ - 'data': 'Semantics not generated.\n' - 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' - 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' - 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' + expect(result, <String, Object>{ + 'data': matches( + r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n' + r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' + r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n' + r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.' + ) }); + + testedExtensions.add(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name); }); test('Service extensions - debugPaint', () async { - final Iterable<Map<String, dynamic>> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.debugPaint'); + final Iterable<Map<String, dynamic>> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.${RenderingServiceExtensions.debugPaint.name}'); Map<String, dynamic> extensionChangedEvent; Map<String, dynamic> result; Future<Map<String, dynamic>> pendingResult; @@ -341,6 +377,8 @@ void main() { expect(debugPaintSizeEnabled, false); expect(extensionChangedEvents.length, 2); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.debugPaint.name); }); test('Service extensions - debugPaintBaselinesEnabled', () async { @@ -383,6 +421,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugPaintBaselinesEnabled, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.debugPaintBaselinesEnabled.name); }); test('Service extensions - invertOversizedImages', () async { @@ -429,6 +469,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugInvertOversizedImages, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.invertOversizedImages.name); }); test('Service extensions - profileWidgetBuilds', () async { @@ -458,6 +500,8 @@ void main() { expect(debugProfileBuildsEnabled, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(WidgetsServiceExtensions.profileWidgetBuilds.name); }); test('Service extensions - profileUserWidgetBuilds', () async { @@ -487,6 +531,8 @@ void main() { expect(debugProfileBuildsEnabledUserWidgets, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(WidgetsServiceExtensions.profileUserWidgetBuilds.name); }); test('Service extensions - profileRenderObjectPaints', () async { @@ -516,6 +562,8 @@ void main() { expect(debugProfilePaintsEnabled, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.profileRenderObjectPaints.name); }); test('Service extensions - profileRenderObjectLayouts', () async { @@ -545,6 +593,8 @@ void main() { expect(debugProfileLayoutsEnabled, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.profileRenderObjectLayouts.name); }); test('Service extensions - evict', () async { @@ -580,12 +630,16 @@ void main() { expect(data, isFalse); expect(completed, isTrue); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', null); + + testedExtensions.add(ServicesServiceExtensions.evict.name); }); test('Service extensions - exit', () async { // no test for _calling_ 'exit', because that should terminate the process! // Not expecting extension to be available for web platform. expect(binding.extensions.containsKey(FoundationServiceExtensions.exit.name), !isBrowser); + + testedExtensions.add(FoundationServiceExtensions.exit.name); }); test('Service extensions - platformOverride', () async { @@ -672,6 +726,8 @@ void main() { expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'android'); binding.reassembled = 0; + + testedExtensions.add(FoundationServiceExtensions.platformOverride.name); }); test('Service extensions - repaintRainbow', () async { @@ -715,6 +771,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugRepaintRainbowEnabled, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.repaintRainbow.name); }); test('Service extensions - debugDisableClipLayers', () async { @@ -757,6 +815,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugDisableClipLayers, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.debugDisableClipLayers.name); }); test('Service extensions - debugDisablePhysicalShapeLayers', () async { @@ -799,6 +859,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugDisablePhysicalShapeLayers, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.debugDisablePhysicalShapeLayers.name); }); test('Service extensions - debugDisableOpacityLayers', () async { @@ -841,6 +903,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(debugDisableOpacityLayers, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(RenderingServiceExtensions.debugDisableOpacityLayers.name); }); test('Service extensions - reassemble', () async { @@ -864,6 +928,8 @@ void main() { expect(result, <String, String>{}); expect(binding.reassembled, 1); binding.reassembled = 0; + + testedExtensions.add(FoundationServiceExtensions.reassemble.name); }); test('Service extensions - showPerformanceOverlay', () async { @@ -872,6 +938,7 @@ void main() { // The performance overlay service extension is disabled on the web. if (kIsWeb) { expect(binding.extensions.containsKey(WidgetsServiceExtensions.showPerformanceOverlay.name), isFalse); + testedExtensions.add(WidgetsServiceExtensions.showPerformanceOverlay.name); return; } @@ -893,6 +960,8 @@ void main() { expect(result, <String, String>{'enabled': 'false'}); expect(WidgetsApp.showPerformanceOverlayOverride, false); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(WidgetsServiceExtensions.showPerformanceOverlay.name); }); test('Service extensions - timeDilation', () async { @@ -929,6 +998,8 @@ void main() { expect(timeDilation, 1.0); expect(extensionChangedEvents.length, 2); expect(binding.frameScheduled, isFalse); + + testedExtensions.add(SchedulerServiceExtensions.timeDilation.name); }); test('Service extensions - brightnessOverride', () async { @@ -937,6 +1008,8 @@ void main() { final String brightnessValue = result['value'] as String; expect(brightnessValue, 'Brightness.light'); + + testedExtensions.add(FoundationServiceExtensions.brightnessOverride.name); }); test('Service extensions - activeDevToolsServerAddress', () async { @@ -950,6 +1023,8 @@ void main() { result = await binding.testExtension(FoundationServiceExtensions.activeDevToolsServerAddress.name, <String, String>{'value': 'http://127.0.0.1:9102'}); serverAddress = result['value'] as String; expect(serverAddress, 'http://127.0.0.1:9102'); + + testedExtensions.add(FoundationServiceExtensions.activeDevToolsServerAddress.name); }); test('Service extensions - connectedVmServiceUri', () async { @@ -963,5 +1038,7 @@ void main() { result = await binding.testExtension(FoundationServiceExtensions.connectedVmServiceUri.name, <String, String>{'value': 'http://127.0.0.1:54000/kMUMseKAnog=/'}); serverAddress = result['value'] as String; expect(serverAddress, 'http://127.0.0.1:54000/kMUMseKAnog=/'); + + testedExtensions.add(FoundationServiceExtensions.connectedVmServiceUri.name); }); } diff --git a/packages/flutter/test/foundation/stack_frame_test.dart b/packages/flutter/test/foundation/stack_frame_test.dart index 63138c7352e6a..e3052188cb8b0 100644 --- a/packages/flutter/test/foundation/stack_frame_test.dart +++ b/packages/flutter/test/foundation/stack_frame_test.dart @@ -99,6 +99,10 @@ void main() { ), ); }); + + test('Parses to null for wrong format.', () { + expect(StackFrame.fromStackTraceLine('wrong stack trace format'), null); + }); } const String stackString = ''' diff --git a/packages/flutter/test/gestures/debug_test.dart b/packages/flutter/test/gestures/debug_test.dart index f3f812ccf9c08..6a17b90642978 100644 --- a/packages/flutter/test/gestures/debug_test.dart +++ b/packages/flutter/test/gestures/debug_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('debugPrintGestureArenaDiagnostics', (WidgetTester tester) async { diff --git a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart index 26c2f8ffe3bd0..76d0d82be2498 100644 --- a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart @@ -9,8 +9,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestResampleEventFlutterBinding extends AutomatedTestWidgetsFlutterBinding { @override diff --git a/packages/flutter/test/gestures/gesture_config_regression_test.dart b/packages/flutter/test/gestures/gesture_config_regression_test.dart index 7fcda721bbf09..fd7569c2e5488 100644 --- a/packages/flutter/test/gestures/gesture_config_regression_test.dart +++ b/packages/flutter/test/gestures/gesture_config_regression_test.dart @@ -6,12 +6,12 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestResult { bool dragStarted = false; bool dragUpdate = false; + bool dragEnd = false; } class NestedScrollableCase extends StatelessWidget { @@ -78,7 +78,9 @@ class NestedDraggableCase extends StatelessWidget { onDragUpdate: (DragUpdateDetails details){ testResult.dragUpdate = true; }, - onDragEnd: (_) {}, + onDragEnd: (_) { + testResult.dragEnd = true; + }, ), ); }, @@ -135,5 +137,6 @@ void main() { expect(result.dragStarted, true); expect(result.dragUpdate, true); + expect(result.dragEnd, true); }); } diff --git a/packages/flutter/test/gestures/scale_test.dart b/packages/flutter/test/gestures/scale_test.dart index a844c54cdf790..6892dcccc5295 100644 --- a/packages/flutter/test/gestures/scale_test.dart +++ b/packages/flutter/test/gestures/scale_test.dart @@ -79,6 +79,7 @@ void main() { updatedDelta = null; expect(didEndScale, isFalse); expect(didTap, isFalse); + expect(scale.pointerCount, 1); // Two-finger scaling final TestPointer pointer2 = TestPointer(2); @@ -87,6 +88,7 @@ void main() { tap.addPointer(down2); tester.closeArena(2); tester.route(down2); + expect(scale.pointerCount, 2); expect(didEndScale, isTrue); didEndScale = false; diff --git a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart b/packages/flutter/test/gestures/tap_and_drag_test.dart similarity index 99% rename from packages/flutter/test/widgets/tap_and_drag_gestures_test.dart rename to packages/flutter/test/gestures/tap_and_drag_test.dart index 33027beeee4c2..152341b03fddd 100644 --- a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart +++ b/packages/flutter/test/gestures/tap_and_drag_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../gestures/gesture_tester.dart'; diff --git a/packages/flutter/test/gestures/transformed_double_tap_test.dart b/packages/flutter/test/gestures/transformed_double_tap_test.dart index 38056a658e633..b550ad0f3f966 100644 --- a/packages/flutter/test/gestures/transformed_double_tap_test.dart +++ b/packages/flutter/test/gestures/transformed_double_tap_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async { diff --git a/packages/flutter/test/gestures/transformed_long_press_test.dart b/packages/flutter/test/gestures/transformed_long_press_test.dart index fcbd38c69a4c8..9e222ca41d56f 100644 --- a/packages/flutter/test/gestures/transformed_long_press_test.dart +++ b/packages/flutter/test/gestures/transformed_long_press_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('gets local coordinates', (WidgetTester tester) async { diff --git a/packages/flutter/test/gestures/transformed_monodrag_test.dart b/packages/flutter/test/gestures/transformed_monodrag_test.dart index c85bf24cd0c86..1a7c39b6823d0 100644 --- a/packages/flutter/test/gestures/transformed_monodrag_test.dart +++ b/packages/flutter/test/gestures/transformed_monodrag_test.dart @@ -7,12 +7,11 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('Horizontal', () { - testWidgetsWithLeakTracking('gets local coordinates', (WidgetTester tester) async { + testWidgets('gets local coordinates', (WidgetTester tester) async { int dragCancelCount = 0; final List<DragDownDetails> downDetails = <DragDownDetails>[]; final List<DragEndDetails> endDetails = <DragEndDetails>[]; diff --git a/packages/flutter/test/gestures/transformed_scale_test.dart b/packages/flutter/test/gestures/transformed_scale_test.dart index c01c6548ad3e4..f48b5e8a14d05 100644 --- a/packages/flutter/test/gestures/transformed_scale_test.dart +++ b/packages/flutter/test/gestures/transformed_scale_test.dart @@ -5,10 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../foundation/leak_tracking.dart'; - void main() { - testWidgetsWithLeakTracking('gets local coordinates', (WidgetTester tester) async { + testWidgets('gets local coordinates', (WidgetTester tester) async { final List<ScaleStartDetails> startDetails = <ScaleStartDetails>[]; final List<ScaleUpdateDetails> updateDetails = <ScaleUpdateDetails>[]; diff --git a/packages/flutter/test/gestures/transformed_tap_test.dart b/packages/flutter/test/gestures/transformed_tap_test.dart index 2d3dd3cd48bf0..54868059930c0 100644 --- a/packages/flutter/test/gestures/transformed_tap_test.dart +++ b/packages/flutter/test/gestures/transformed_tap_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('gets local coordinates', (WidgetTester tester) async { diff --git a/packages/flutter/test/gestures/velocity_tracker_test.dart b/packages/flutter/test/gestures/velocity_tracker_test.dart index 3e6b94534bbbb..61eebc13fe186 100644 --- a/packages/flutter/test/gestures/velocity_tracker_test.dart +++ b/packages/flutter/test/gestures/velocity_tracker_test.dart @@ -144,4 +144,22 @@ void main() { } } }); + + test('Assume zero velocity when there are no recent samples', () async { + final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch); + Offset position = Offset.zero; + Duration time = Duration.zero; + const Offset positionDelta = Offset(0, -1); + const Duration durationDelta = Duration(seconds: 1); + + for (int i = 0; i < 10; i+=1) { + position += positionDelta; + time += durationDelta; + tracker.addPosition(time, position); + } + + await Future<void>.delayed(const Duration(milliseconds: 50)); + + expect(tracker.getVelocity().pixelsPerSecond, Offset.zero); + }); } diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart index 29cfdf5a372b4..4af66cc40b84a 100644 --- a/packages/flutter/test/material/about_test.dart +++ b/packages/flutter/test/material/about_test.dart @@ -10,8 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { tearDown(() { @@ -58,7 +57,7 @@ void main() { expect(find.text('View licenses'), findsOneWidget); }); - testWidgetsWithLeakTracking('AboutListTile control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - AboutListTile control test', (WidgetTester tester) async { const FlutterLogo logo = FlutterLogo(); await tester.pumpWidget( @@ -141,6 +140,89 @@ void main() { expect(find.text('Pirate license'), findsOneWidget); }); + testWidgetsWithLeakTracking('Material3 - AboutListTile control test', (WidgetTester tester) async { + const FlutterLogo logo = FlutterLogo(); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + title: 'Pirate app', + home: Scaffold( + appBar: AppBar( + title: const Text('Home'), + ), + drawer: Drawer( + child: ListView( + children: const <Widget>[ + AboutListTile( + applicationVersion: '0.1.2', + applicationIcon: logo, + applicationLegalese: 'I am the very model of a modern major general.', + aboutBoxChildren: <Widget>[ + Text('About box'), + ], + ), + ], + ), + ), + ), + ), + ); + + expect(find.text('About Pirate app'), findsNothing); + expect(find.text('0.1.2'), findsNothing); + expect(find.byWidget(logo), findsNothing); + expect( + find.text('I am the very model of a modern major general.'), + findsNothing, + ); + expect(find.text('About box'), findsNothing); + + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(find.text('About Pirate app'), findsOneWidget); + expect(find.text('0.1.2'), findsNothing); + expect(find.byWidget(logo), findsNothing); + expect( + find.text('I am the very model of a modern major general.'), + findsNothing, + ); + expect(find.text('About box'), findsNothing); + + await tester.tap(find.text('About Pirate app')); + await tester.pumpAndSettle(); + + expect(find.text('About Pirate app'), findsOneWidget); + expect(find.text('0.1.2'), findsOneWidget); + expect(find.byWidget(logo), findsOneWidget); + expect( + find.text('I am the very model of a modern major general.'), + findsOneWidget, + ); + expect(find.text('About box'), findsOneWidget); + + LicenseRegistry.addLicense(() { + return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ + const LicenseEntryWithLineBreaks(<String>['Pirate package '], 'Pirate license'), + ]); + }); + + await tester.tap(find.text('View licenses')); + await tester.pumpAndSettle(); + + expect(find.text('Pirate app'), findsOneWidget); + expect(find.text('0.1.2'), findsOneWidget); + expect(find.byWidget(logo), findsOneWidget); + expect( + find.text('I am the very model of a modern major general.'), + findsOneWidget, + ); + await tester.tap(find.text('Pirate package ')); + await tester.pumpAndSettle(); + expect(find.text('Pirate license'), findsOneWidget); + }); + testWidgetsWithLeakTracking('About box logic defaults to executable name for app name', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( @@ -168,9 +250,8 @@ void main() { }); await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), - home: const Center( + const MaterialApp( + home: Center( child: LicensePage(), ), ), @@ -222,10 +303,9 @@ void main() { }); await tester.pumpWidget( - MaterialApp( - theme: ThemeData(useMaterial3: false), + const MaterialApp( title: 'Pirate app', - home: const Center( + home: Center( child: LicensePage( applicationName: 'LicensePage test app', applicationVersion: '0.1.2', @@ -280,7 +360,7 @@ void main() { expect(find.text('Another license'), findsOneWidget); }); - testWidgetsWithLeakTracking('_PackageLicensePage title style without AppBarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - _PackageLicensePage title style without AppBarTheme', (WidgetTester tester) async { LicenseRegistry.addLicense(() { return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'), @@ -328,6 +408,54 @@ void main() { expect(subtitle.style, subtitleTextStyle); }); + testWidgetsWithLeakTracking('Material3 - _PackageLicensePage title style without AppBarTheme', (WidgetTester tester) async { + LicenseRegistry.addLicense(() { + return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ + const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'), + ]); + }); + + const TextStyle titleTextStyle = TextStyle( + fontSize: 20, + color: Colors.black, + inherit: false, + ); + const TextStyle subtitleTextStyle = TextStyle( + fontSize: 15, + color: Colors.red, + inherit: false, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + textTheme: const TextTheme( + titleLarge: titleTextStyle, + titleSmall: subtitleTextStyle, + ), + ), + home: const Center( + child: LicensePage(), + ), + ), + ); + await tester.pumpAndSettle(); + + // Check for packages. + expect(find.text('AAA'), findsOneWidget); + + // Check license is displayed after entering into license page for 'AAA'. + await tester.tap(find.text('AAA')); + await tester.pumpAndSettle(); + + // Check for titles style. + final Text title = tester.widget(find.text('AAA')); + expect(title.style, titleTextStyle); + final Text subtitle = tester.widget(find.text('1 license.')); + expect(subtitle.style, subtitleTextStyle); + }); + testWidgetsWithLeakTracking('_PackageLicensePage title style with AppBarTheme', (WidgetTester tester) async { LicenseRegistry.addLicense(() { return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ @@ -378,7 +506,7 @@ void main() { expect(title.style, titleTextStyle); }); - testWidgetsWithLeakTracking('LicensePage respects the notch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - LicensePage respects the notch', (WidgetTester tester) async { const double safeareaPadding = 27.0; LicenseRegistry.addLicense(() { @@ -409,6 +537,37 @@ void main() { ); }); + testWidgetsWithLeakTracking('Material3 - LicensePage respects the notch', (WidgetTester tester) async { + const double safeareaPadding = 27.0; + + LicenseRegistry.addLicense(() { + return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ + const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'), + ]); + }); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.all(safeareaPadding), + ), + child: LicensePage(), + ), + ), + ); + + await tester.pumpAndSettle(); + + // The position of the top left of app bar title should indicate whether + // the safe area is sufficiently respected. + expect( + tester.getTopLeft(find.text('Licenses')), + const Offset(16.0 + safeareaPadding, 14.0 + safeareaPadding), + ); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + testWidgetsWithLeakTracking('LicensePage returns early if unmounted', (WidgetTester tester) async { final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>(); LicenseRegistry.addLicense(() { @@ -566,6 +725,160 @@ void main() { expect(nestedObserver.licensePageCount, 0); }); + group('Barrier dismissible', () { + late AboutDialogObserver rootObserver; + + setUp(() { + rootObserver = AboutDialogObserver(); + }); + + testWidgetsWithLeakTracking('Barrier is dismissible with default parameter', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showAboutDialog( + context: context, + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.dialogCount, 1); + + // Tap on the barrier. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.dialogCount, 0); + }); + + testWidgetsWithLeakTracking('Barrier is not dismissible with barrierDismissible is false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showAboutDialog( + context: context, + barrierDismissible: false + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.dialogCount, 1); + + // Tap on the barrier, which shouldn't do anything this time. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.dialogCount, 1); + }); + }); + + testWidgetsWithLeakTracking('Barrier color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showAboutDialog( + context: context, + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.black54); + + // Dismiss the dialog. + await tester.tapAt(const Offset(10.0, 10.0)); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showAboutDialog( + context: context, + barrierColor: Colors.pink, + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink); + }); + + testWidgetsWithLeakTracking('Barrier Label', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showAboutDialog( + context: context, + barrierLabel: 'Custom Label', + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).semanticsLabel, 'Custom Label'); + }); + testWidgetsWithLeakTracking('showAboutDialog uses root navigator by default', (WidgetTester tester) async { final AboutDialogObserver rootObserver = AboutDialogObserver(); final AboutDialogObserver nestedObserver = AboutDialogObserver(); @@ -830,7 +1143,7 @@ void main() { expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -20.0))); }); - testWidgetsWithLeakTracking("LicensePage's color must be same whether loading or done", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Material2 - LicensePage's color must be same whether loading or done", (WidgetTester tester) async { const Color scaffoldColor = Color(0xFF123456); const Color cardColor = Color(0xFF654321); @@ -877,6 +1190,53 @@ void main() { expect(materialDones[1].color, cardColor); }); + testWidgetsWithLeakTracking("Material3 - LicensePage's color must be same whether loading or done", (WidgetTester tester) async { + const Color scaffoldColor = Color(0xFF123456); + const Color cardColor = Color(0xFF654321); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData.light(useMaterial3: true).copyWith( + scaffoldBackgroundColor: scaffoldColor, + cardColor: cardColor, + ), + home: Scaffold( + body: Center( + child: Builder( + builder: (BuildContext context) => GestureDetector( + child: const Text('Show licenses'), + onTap: () { + showLicensePage( + context: context, + applicationName: 'MyApp', + applicationVersion: '1.0.0', + ); + }, + ), + ), + ), + ), + )); + + await tester.tap(find.text('Show licenses')); + await tester.pump(); + await tester.pump(); + + // Check color when loading. + final List<Material> materialLoadings = tester.widgetList<Material>(find.byType(Material)).toList(); + expect(materialLoadings.length, equals(5)); + expect(materialLoadings[1].color, scaffoldColor); + expect(materialLoadings[2].color, cardColor); + + await tester.pumpAndSettle(); + + // Check color when done. + expect(find.byKey(const ValueKey<ConnectionState>(ConnectionState.done)), findsOneWidget); + final List<Material> materialDones = tester.widgetList<Material>(find.byType(Material)).toList(); + expect(materialDones.length, equals(4)); + expect(materialDones[0].color, scaffoldColor); + expect(materialDones[1].color, cardColor); + }); + testWidgetsWithLeakTracking('Conflicting scrollbars are not applied by ScrollBehavior to _PackageLicensePage', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/83819 LicenseRegistry.addLicense(() { @@ -1054,7 +1414,7 @@ void main() { expect(appIconBottomPadding, 18.0); }); - testWidgetsWithLeakTracking('Error handling test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Error handling test', (WidgetTester tester) async { LicenseRegistry.addLicense(() => Stream<LicenseEntry>.error(Exception('Injected failure'))); await tester.pumpWidget( MaterialApp( @@ -1077,7 +1437,30 @@ void main() { expect(find.text('Exception: Injected failure'), findsOneWidget); }); - testWidgetsWithLeakTracking('LicensePage master view layout position - ltr', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Error handling test', (WidgetTester tester) async { + LicenseRegistry.addLicense(() => Stream<LicenseEntry>.error(Exception('Injected failure'))); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Material(child: AboutListTile()) + ) + ); + await tester.tap(find.byType(ListTile)); + await tester.pump(); + await tester.pump(const Duration(seconds: 2)); + await tester.tap(find.text('View licenses')); + await tester.pump(); + await tester.pump(const Duration(seconds: 2)); + final Finder finder = find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_PackagesView'); + // force the stream to complete (has to be done in a runAsync block since it's areal async process) + await tester.runAsync(() => (tester.firstState(finder) as dynamic).licenses as Future<dynamic>); // ignore: avoid_dynamic_calls + expect(tester.takeException().toString(), 'Exception: Injected failure'); + await tester.pumpAndSettle(); + expect(tester.takeException().toString(), 'Exception: Injected failure'); + expect(find.text('Exception: Injected failure'), findsOneWidget); + }); + + testWidgetsWithLeakTracking('Material2 - LicensePage master view layout position - ltr', (WidgetTester tester) async { const TextDirection textDirection = TextDirection.ltr; const Size defaultSize = Size(800.0, 600.0); const Size wideSize = Size(1200.0, 600.0); @@ -1088,6 +1471,10 @@ void main() { ]); }); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + // Configure to show the default layout. await tester.binding.setSurfaceSize(defaultSize); @@ -1136,12 +1523,76 @@ void main() { expect(titleOffset, const Offset(292.0, 136.0)); expect(titleOffset.dx, lessThan(wideSize.width - 320)); // Default master view width is 320.0. expect(tester.getCenter(find.byType(ListView)), const Offset(160, 356)); + }); + + testWidgetsWithLeakTracking('Material3 - LicensePage master view layout position - ltr', (WidgetTester tester) async { + const TextDirection textDirection = TextDirection.ltr; + const Size defaultSize = Size(800.0, 600.0); + const Size wideSize = Size(1200.0, 600.0); + const String title = 'License ABC'; + LicenseRegistry.addLicense(() { + return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ + const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'), + ]); + }); + + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); // Configure to show the default layout. await tester.binding.setSurfaceSize(defaultSize); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + title: title, + home: const Scaffold( + body: Directionality( + textDirection: textDirection, + child: LicensePage(), + ), + ), + ), + ); + + await tester.pumpAndSettle(); // Finish rendering the page. + + // If the layout width is less than 840.0 pixels, nested layout is + // used which positions license page title at the top center. + Offset titleOffset = tester.getCenter(find.text(title)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(titleOffset, Offset(defaultSize.width / 2, 96.0)); + } + expect(tester.getCenter(find.byType(ListView)), Offset(defaultSize.width / 2, 328.0)); + + // Configure a wide window to show the lateral UI. + await tester.binding.setSurfaceSize(wideSize); + + await tester.pumpWidget( + const MaterialApp( + title: title, + home: Scaffold( + body: Directionality( + textDirection: textDirection, + child: LicensePage(), + ), + ), + ), + ); + + await tester.pumpAndSettle(); // Finish rendering the page. + + // If the layout width is greater than 840.0 pixels, lateral UI layout + // is used which positions license page title and packageList + // at the top left. + titleOffset = tester.getTopRight(find.text(title)); + expect(titleOffset, const Offset(292.0, 136.0)); + expect(titleOffset.dx, lessThan(wideSize.width - 320)); // Default master view width is 320.0. + expect(tester.getCenter(find.byType(ListView)), const Offset(160, 356)); }); - testWidgetsWithLeakTracking('LicensePage master view layout position - rtl', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - LicensePage master view layout position - rtl', (WidgetTester tester) async { const TextDirection textDirection = TextDirection.rtl; const Size defaultSize = Size(800.0, 600.0); const Size wideSize = Size(1200.0, 600.0); @@ -1152,6 +1603,10 @@ void main() { ]); }); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + // Configure to show the default layout. await tester.binding.setSurfaceSize(defaultSize); @@ -1200,9 +1655,73 @@ void main() { expect(titleOffset, const Offset(908.0, 136.0)); expect(titleOffset.dx, greaterThan(wideSize.width - 320)); // Default master view width is 320.0. expect(tester.getCenter(find.byType(ListView)), const Offset(1040.0, 356.0)); + }); + + testWidgetsWithLeakTracking('Material3 - LicensePage master view layout position - rtl', (WidgetTester tester) async { + const TextDirection textDirection = TextDirection.rtl; + const Size defaultSize = Size(800.0, 600.0); + const Size wideSize = Size(1200.0, 600.0); + const String title = 'License ABC'; + LicenseRegistry.addLicense(() { + return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[ + const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'), + ]); + }); + + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); // Configure to show the default layout. await tester.binding.setSurfaceSize(defaultSize); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + title: title, + home: const Scaffold( + body: Directionality( + textDirection: textDirection, + child: LicensePage(), + ), + ), + ), + ); + + await tester.pumpAndSettle(); // Finish rendering the page. + + // If the layout width is less than 840.0 pixels, nested layout is + // used which positions license page title at the top center. + Offset titleOffset = tester.getCenter(find.text(title)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(titleOffset, Offset(defaultSize.width / 2, 96.0)); + } + expect(tester.getCenter(find.byType(ListView)), Offset(defaultSize.width / 2, 328.0)); + + // Configure a wide window to show the lateral UI. + await tester.binding.setSurfaceSize(wideSize); + + await tester.pumpWidget( + const MaterialApp( + title: title, + home: Scaffold( + body: Directionality( + textDirection: textDirection, + child: LicensePage(), + ), + ), + ), + ); + + await tester.pumpAndSettle(); // Finish rendering the page. + + // If the layout width is greater than 840.0 pixels, lateral UI layout + // is used which positions license page title and packageList + // at the top right. + titleOffset = tester.getTopLeft(find.text(title)); + expect(titleOffset, const Offset(908.0, 136.0)); + expect(titleOffset.dx, greaterThan(wideSize.width - 320)); // Default master view width is 320.0. + expect(tester.getCenter(find.byType(ListView)), const Offset(1040.0, 356.0)); }); testWidgetsWithLeakTracking('License page title in lateral UI does not use AppBarTheme.foregroundColor', (WidgetTester tester) async { @@ -1218,6 +1737,10 @@ void main() { ]); }); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + // Configure a wide window to show the lateral UI. await tester.binding.setSurfaceSize(const Size(1200.0, 600.0)); @@ -1240,9 +1763,6 @@ void main() { // License page title in the lateral UI uses default text style color. expect(renderParagraph.text.style!.color, theme.textTheme.titleLarge!.color); - - // Configure to show the default layout. - await tester.binding.setSurfaceSize(const Size(800.0, 600.0)); }); testWidgetsWithLeakTracking('License page default title text color in the nested UI', (WidgetTester tester) async { @@ -1362,4 +1882,12 @@ class AboutDialogObserver extends NavigatorObserver { } super.didPush(route, previousRoute); } + + @override + void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { + if (route is DialogRoute) { + dialogCount--; + } + super.didPop(route, previousRoute); + } } diff --git a/packages/flutter/test/material/action_chip_test.dart b/packages/flutter/test/material/action_chip_test.dart index f3f4d67fc1d23..310db84c5c4ed 100644 --- a/packages/flutter/test/material/action_chip_test.dart +++ b/packages/flutter/test/material/action_chip_test.dart @@ -4,9 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; /// Adds the basic requirements for a Chip. Widget wrapForChip({ @@ -65,7 +63,7 @@ void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { } void main() { - testWidgets('ActionChip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionChip defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'action chip'; @@ -85,7 +83,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(ActionChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(ActionChip)), + within<Size>(distance: 0.01, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, label).style.color!.value, @@ -136,7 +137,7 @@ void main() { expect(decoration.color, null); }); - testWidgets('ActionChip.elevated defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionChip.elevated defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'action chip'; @@ -156,7 +157,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(ActionChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(ActionChip)), + within<Size>(distance: 0.01, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, label).style.color!.value, @@ -207,7 +211,7 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); - testWidgets('ActionChip.color resolves material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionChip.color resolves material states', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); final MaterialStateProperty<Color?> color = MaterialStateProperty.resolveWith((Set<MaterialState> states) { @@ -266,7 +270,7 @@ void main() { ); }); - testWidgets('ActionChip uses provided state color properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionChip uses provided state color properties', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); Widget buildApp({ required bool enabled, required bool selected }) { diff --git a/packages/flutter/test/material/action_icons_theme_test.dart b/packages/flutter/test/material/action_icons_theme_test.dart index c5e1ddd6da59b..37ad357c9dd22 100644 --- a/packages/flutter/test/material/action_icons_theme_test.dart +++ b/packages/flutter/test/material/action_icons_theme_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('ActionIconThemeData copyWith, ==, hashCode basics', () { @@ -15,7 +14,7 @@ void main() { const ActionIconThemeData().copyWith().hashCode); }); - testWidgets('ActionIconThemeData copyWith overrides all properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionIconThemeData copyWith overrides all properties', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/126762. Widget originalButtonBuilder(BuildContext context) { return const SizedBox(); diff --git a/packages/flutter/test/material/adaptive_text_selection_toolbar_test.dart b/packages/flutter/test/material/adaptive_text_selection_toolbar_test.dart index 3d85f3ea76c09..4a3c51ee5b652 100644 --- a/packages/flutter/test/material/adaptive_text_selection_toolbar_test.dart +++ b/packages/flutter/test/material/adaptive_text_selection_toolbar_test.dart @@ -7,8 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart'; import '../widgets/live_text_utils.dart'; @@ -27,6 +26,13 @@ void main() { await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); }); + Finder findOverflowNextButton() { + return find.byWidgetPredicate((Widget widget) => + widget is CustomPaint && + '${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter', + ); + } + testWidgetsWithLeakTracking('Builds the right toolbar on each platform, including web, and shows buttonItems', (WidgetTester tester) async { const String buttonText = 'Click me'; @@ -107,6 +113,8 @@ void main() { testWidgetsWithLeakTracking('Can build from EditableTextState', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + final TextEditingController controller = TextEditingController(); + final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -114,9 +122,9 @@ void main() { child: SizedBox( width: 400, child: EditableText( - controller: TextEditingController(), + controller: controller, backgroundCursorColor: const Color(0xff00ffff), - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(), cursorColor: const Color(0xff00ffff), selectionControls: materialTextSelectionHandleControls, @@ -164,6 +172,8 @@ void main() { case TargetPlatform.macOS: expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsOneWidget); } + controller.dispose(); + focusNode.dispose(); }, skip: kIsWeb, // [intended] on web the browser handles the context menu. variant: TargetPlatformVariant.all(), @@ -186,6 +196,9 @@ void main() { onPaste: () {}, onSelectAll: () {}, onLiveTextInput: () {}, + onLookUp: () {}, + onSearchWeb: () {}, + onShare: () {}, ), ), ), @@ -199,20 +212,24 @@ void main() { switch (defaultTargetPlatform) { case TargetPlatform.android: + expect(find.text('Select all'), findsOneWidget); + expect(find.byType(TextSelectionToolbarTextButton), findsNWidgets(6)); case TargetPlatform.fuchsia: expect(find.text('Select all'), findsOneWidget); - expect(find.byType(TextSelectionToolbarTextButton), findsNWidgets(5)); + expect(find.byType(TextSelectionToolbarTextButton), findsNWidgets(8)); case TargetPlatform.iOS: expect(find.text('Select All'), findsOneWidget); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(6)); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); + await tester.pumpAndSettle(); expect(findLiveTextButton(), findsOneWidget); - expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(5)); case TargetPlatform.linux: case TargetPlatform.windows: expect(find.text('Select all'), findsOneWidget); - expect(find.byType(DesktopTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(DesktopTextSelectionToolbarButton), findsNWidgets(8)); case TargetPlatform.macOS: expect(find.text('Select All'), findsOneWidget); - expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(5)); + expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(8)); } }, skip: kIsWeb, // [intended] on web the browser handles the context menu. @@ -227,6 +244,7 @@ void main() { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; final TextEditingController controller = TextEditingController(); + final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MaterialApp( @@ -235,7 +253,7 @@ void main() { child: EditableText( controller: controller, backgroundCursorColor: Colors.grey, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.red, selectionControls: materialTextSelectionHandleControls, @@ -310,6 +328,9 @@ void main() { case TargetPlatform.macOS: expect(buttonTypes, isNot(contains(ContextMenuButtonType.selectAll))); } + + focusNode.dispose(); + controller.dispose(); }, variant: TargetPlatformVariant.all(), skip: kIsWeb, // [intended] diff --git a/packages/flutter/test/material/animated_icons_test.dart b/packages/flutter/test/material/animated_icons_test.dart index 720dfa63172f8..b81df83618104 100644 --- a/packages/flutter/test/material/animated_icons_test.dart +++ b/packages/flutter/test/material/animated_icons_test.dart @@ -9,7 +9,7 @@ import 'dart:math' as math show pi; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; class MockCanvas extends Fake implements Canvas { @@ -95,7 +95,7 @@ class RecordedScale extends RecordedCanvasCall { } void main() { - testWidgets('IconTheme color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme color', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -116,7 +116,7 @@ void main() { expect(canvas.capturedPaint, hasColor(0xFF666666)); }); - testWidgets('IconTheme opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme opacity', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -138,7 +138,7 @@ void main() { expect(canvas.capturedPaint, hasColor(0x80666666)); }); - testWidgets('color overrides IconTheme color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('color overrides IconTheme color', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -160,7 +160,7 @@ void main() { expect(canvas.capturedPaint, hasColor(0xFF0000FF)); }); - testWidgets('IconTheme size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -184,7 +184,7 @@ void main() { expect(canvas.capturedSy, 0.25); }); - testWidgets('size overridesIconTheme size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('size overridesIconTheme size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -209,7 +209,7 @@ void main() { expect(canvas.capturedSy, 2); }); - testWidgets('Semantic label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantic label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -229,7 +229,7 @@ void main() { semantics.dispose(); }); - testWidgets('Inherited text direction rtl', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited text direction rtl', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.rtl, @@ -258,7 +258,7 @@ void main() { matchesGoldenFile('animated_icons_test.icon.rtl.png')); }); - testWidgets('Inherited text direction ltr', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited text direction ltr', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -285,7 +285,7 @@ void main() { matchesGoldenFile('animated_icons_test.icon.ltr.png')); }); - testWidgets('Inherited text direction overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited text direction overridden', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -311,7 +311,7 @@ void main() { ]); }); - testWidgets('Direction has no effect on position of widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Direction has no effect on position of widget', (WidgetTester tester) async { const AnimatedIcon icon = AnimatedIcon( progress: AlwaysStoppedAnimation<double>(0.0), icon: AnimatedIcons.arrow_menu, diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index b9234fbe602dd..c37613731f1f3 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; Widget buildSliverAppBarApp({ @@ -55,7 +55,7 @@ ScrollController primaryScrollController(WidgetTester tester) { return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView))); } -TextStyle? iconStyle(WidgetTester tester, IconData icon) { +TextStyle? _iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget<RichText>( find.descendant(of: find.byIcon(icon).first, matching: find.byType(RichText)), ); @@ -82,7 +82,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('AppBar centers title on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centers title on iOS', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), @@ -163,7 +163,7 @@ void main() { } }); - testWidgets('AppBar centerTitle:true centers on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:true centers on Android', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), @@ -183,7 +183,7 @@ void main() { expect(center.dx, lessThan(400 + size.width / 2.0)); }); - testWidgets('AppBar centerTitle:false title start edge is 16.0 (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:false title start edge is 16.0 (LTR)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -200,7 +200,7 @@ void main() { expect(tester.getTopRight(titleWidget).dx, 800 - 16.0); }); - testWidgets('AppBar centerTitle:false title start edge is 16.0 (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:false title start edge is 16.0 (RTL)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -220,7 +220,7 @@ void main() { expect(tester.getTopLeft(titleWidget).dx, 16.0); }); - testWidgets('AppBar titleSpacing:32 title start edge is 32.0 (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar titleSpacing:32 title start edge is 32.0 (LTR)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -238,7 +238,7 @@ void main() { expect(tester.getTopRight(titleWidget).dx, 800 - 32.0); }); - testWidgets('AppBar titleSpacing:32 title start edge is 32.0 (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar titleSpacing:32 title start edge is 32.0 (RTL)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -259,7 +259,7 @@ void main() { expect(tester.getTopLeft(titleWidget).dx, 32.0); }); - testWidgets( + testWidgetsWithLeakTracking( 'AppBar centerTitle:false leading button title left edge is 72.0 (LTR)', (WidgetTester tester) async { await tester.pumpWidget( @@ -279,7 +279,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'AppBar centerTitle:false leading button title left edge is 72.0 (RTL)', (WidgetTester tester) async { await tester.pumpWidget( @@ -302,7 +302,7 @@ void main() { }, ); - testWidgets('AppBar centerTitle:false title overflow OK', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:false title overflow OK', (WidgetTester tester) async { // The app bar's title should be constrained to fit within the available space // between the leading and actions widgets. @@ -363,7 +363,7 @@ void main() { expect(tester.getSize(title).width, equals(800.0 - 56.0 - 16.0 - 16.0 - 200.0)); }); - testWidgets('AppBar centerTitle:true title overflow OK (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:true title overflow OK (LTR)', (WidgetTester tester) async { // The app bar's title should be constrained to fit within the available space // between the leading and actions widgets. When it's also centered it may // also be start or end justified if it doesn't fit in the overall center. @@ -415,7 +415,7 @@ void main() { expect(tester.getSize(title).width, equals(620.0)); }); - testWidgets('AppBar centerTitle:true title overflow OK (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar centerTitle:true title overflow OK (RTL)', (WidgetTester tester) async { // The app bar's title should be constrained to fit within the available space // between the leading and actions widgets. When it's also centered it may // also be start or end justified if it doesn't fit in the overall center. @@ -470,7 +470,7 @@ void main() { expect(tester.getSize(title).width, equals(620.0)); }); - testWidgets('AppBar with no Scaffold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar with no Scaffold', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SizedBox( @@ -490,7 +490,7 @@ void main() { expect(find.text('A2'), findsOneWidget); }); - testWidgets('AppBar render at zero size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar render at zero size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( @@ -509,7 +509,7 @@ void main() { expect(tester.getSize(title).isEmpty, isTrue); }); - testWidgets('AppBar actions are vertically centered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar actions are vertically centered', (WidgetTester tester) async { final UniqueKey appBarKey = UniqueKey(); final UniqueKey leadingKey = UniqueKey(); final UniqueKey titleKey = UniqueKey(); @@ -541,7 +541,7 @@ void main() { expect(yCenter(appBarKey), equals(yCenter(action1Key))); }); - testWidgets('AppBar drawer icon has default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar drawer icon has default size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -559,10 +559,31 @@ void main() { ); }); - testWidgets('AppBar drawer icon has default color', (WidgetTester tester) async { - final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light()); - final bool useMaterial3 = themeData.useMaterial3; + testWidgetsWithLeakTracking('Material2 - AppBar drawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + title: const Text('Howdy!'), + ), + drawer: const Drawer(), + ), + ), + ); + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); + }); + + testWidgetsWithLeakTracking('Material3 - AppBar drawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: true, + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -575,13 +596,10 @@ void main() { ), ); - Color? iconColor() => iconStyle(tester, Icons.menu)?.color; - final Color iconColorM2 = themeData.colorScheme.onPrimary; - final Color iconColorM3 = themeData.colorScheme.onSurfaceVariant; - expect(iconColor(), useMaterial3 ? iconColorM3 : iconColorM2); + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onSurfaceVariant); }); - testWidgets('AppBar drawer icon is sized by iconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar drawer icon is sized by iconTheme', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -599,7 +617,7 @@ void main() { ); }); - testWidgets('AppBar drawer icon is colored by iconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar drawer icon is colored by iconTheme', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light()); const Color color = Color(0xFF2196F3); @@ -616,12 +634,10 @@ void main() { ), ); - Color? iconColor() => iconStyle(tester, Icons.menu)?.color; - - expect(iconColor(), color); + expect(_iconStyle(tester, Icons.menu)?.color, color); }); - testWidgets('AppBar endDrawer icon has default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar endDrawer icon has default size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -632,6 +648,7 @@ void main() { ), ), ); + final double iconSize = const IconThemeData.fallback().size!; expect( tester.getSize(find.byIcon(Icons.menu)), @@ -639,10 +656,31 @@ void main() { ); }); - testWidgets('AppBar endDrawer icon has default color', (WidgetTester tester) async { - final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light()); - final bool useMaterial3 = themeData.useMaterial3; + testWidgetsWithLeakTracking('Material2 - AppBar endDrawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + title: const Text('Howdy!'), + ), + endDrawer: const Drawer(), + ), + ), + ); + + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); + }); + testWidgetsWithLeakTracking('Material3 - AppBar endDrawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: true, + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -655,13 +693,10 @@ void main() { ), ); - Color? iconColor() => iconStyle(tester, Icons.menu)?.color; - final Color iconColorM2 = themeData.colorScheme.onPrimary; - final Color iconColorM3 = themeData.colorScheme.onSurfaceVariant; - expect(iconColor(), useMaterial3 ? iconColorM3 : iconColorM2); + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onSurfaceVariant); }); - testWidgets('AppBar endDrawer icon is sized by iconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar endDrawer icon is sized by iconTheme', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -679,7 +714,7 @@ void main() { ); }); - testWidgets('AppBar endDrawer icon is colored by iconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar endDrawer icon is colored by iconTheme', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from(colorScheme: const ColorScheme.light()); const Color color = Color(0xFF2196F3); @@ -696,14 +731,72 @@ void main() { ), ); - Color? iconColor() => iconStyle(tester, Icons.menu)?.color; + expect(_iconStyle(tester, Icons.menu)?.color, color); + }); + + testWidgetsWithLeakTracking('Material2 - leading widget extends to edge and is square', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + platform: TargetPlatform.android, + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + title: const Text('X'), + ), + drawer: const Column(), // Doesn't really matter. Triggers a hamburger regardless. + ), + ), + ); + + // Default IconButton has a size of (56x56). + final Finder hamburger = find.byType(IconButton); + expect(tester.getTopLeft(hamburger), Offset.zero); + expect(tester.getSize(hamburger), const Size(56.0, 56.0)); + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: Container(), + title: const Text('X'), + ), + ), + ), + ); + + // Default leading widget has a size of (56x56). + final Finder leadingBox = find.byType(Container); + expect(tester.getTopLeft(leadingBox), Offset.zero); + expect(tester.getSize(leadingBox), const Size(56.0, 56.0)); + + // The custom leading widget should still be 56x56 even if its size is smaller. + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: const SizedBox(height: 36, width: 36,), + title: const Text('X'), + ), // Doesn't really matter. Triggers a hamburger regardless. + ), + ), + ); - expect(iconColor(), color); + final Finder leading = find.byType(SizedBox); + expect(tester.getTopLeft(leading), Offset.zero); + expect(tester.getSize(leading), const Size(56.0, 56.0)); }); - testWidgets('leading widget extends to edge and is square', (WidgetTester tester) async { - final ThemeData themeData = ThemeData(platform: TargetPlatform.android); - final bool material3 = themeData.useMaterial3; + testWidgetsWithLeakTracking('Material3 - leading widget extends to edge and is square', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + platform: TargetPlatform.android, + useMaterial3: true, + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -717,10 +810,10 @@ void main() { ), ); - // Default IconButton has a size of (48x48) in M3, (56x56) in M2 + // Default IconButton has a size of (48x48). final Finder hamburger = find.byType(IconButton); - expect(tester.getTopLeft(hamburger), material3 ? const Offset(4.0, 4.0) : Offset.zero); - expect(tester.getSize(hamburger), material3 ? const Size(48.0, 48.0) : const Size(56.0, 56.0)); + expect(tester.getTopLeft(hamburger), const Offset(4.0, 4.0)); + expect(tester.getSize(hamburger), const Size(48.0, 48.0)); await tester.pumpWidget( MaterialApp( @@ -734,7 +827,7 @@ void main() { ), ); - // Default leading widget has a size of (56x56) for both M2 and M3 + // Default leading widget has a size of (56x56). final Finder leadingBox = find.byType(Container); expect(tester.getTopLeft(leadingBox), Offset.zero); expect(tester.getSize(leadingBox), const Size(56.0, 56.0)); @@ -757,9 +850,11 @@ void main() { expect(tester.getSize(leading), const Size(56.0, 56.0)); }); - testWidgets('test action is 4dp from edge and 48dp min', (WidgetTester tester) async { - final ThemeData theme = ThemeData(platform: TargetPlatform.android); - final bool material3 = theme.useMaterial3; + testWidgetsWithLeakTracking('Material2 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + platform: TargetPlatform.android, + useMaterial3: false, + ); await tester.pumpWidget( MaterialApp( theme: theme, @@ -792,10 +887,50 @@ void main() { final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share); // The 20dp icon is expanded to fill the IconButton's touch target to 48dp. - expect(tester.getSize(shareButton), material3 ? const Size(48.0, 48.0) : const Size(48.0, 56.0)); + expect(tester.getSize(shareButton), const Size(48.0, 56.0)); }); - testWidgets('SliverAppBar default configuration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + platform: TargetPlatform.android, + useMaterial3: true, + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar( + title: const Text('X'), + actions: const <Widget> [ + IconButton( + icon: Icon(Icons.share), + onPressed: null, + tooltip: 'Share', + iconSize: 20.0, + ), + IconButton( + icon: Icon(Icons.add), + onPressed: null, + tooltip: 'Add', + iconSize: 60.0, + ), + ], + ), + ), + ), + ); + + final Finder addButton = find.widgetWithIcon(IconButton, Icons.add); + expect(tester.getTopRight(addButton), const Offset(800.0, 0.0)); + // It's still the size it was plus the 2 * 8dp padding from IconButton. + expect(tester.getSize(addButton), const Size(60.0 + 2 * 8.0, 56.0)); + + final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share); + // The 20dp icon is expanded to fill the IconButton's touch target to 48dp. + expect(tester.getSize(shareButton), const Size(48.0, 48.0)); + }); + + testWidgetsWithLeakTracking('SliverAppBar default configuration', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp()); final ScrollController controller = primaryScrollController(tester); @@ -827,8 +962,7 @@ void main() { expect(tabBarHeight(tester), initialTabBarHeight); }); - testWidgets('SliverAppBar expandedHeight, pinned', (WidgetTester tester) async { - + testWidgetsWithLeakTracking('SliverAppBar expandedHeight, pinned', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp( pinned: true, expandedHeight: 128.0, @@ -859,8 +993,7 @@ void main() { expect(tabBarHeight(tester), initialTabBarHeight); }); - testWidgets('SliverAppBar expandedHeight, pinned and floating', (WidgetTester tester) async { - + testWidgetsWithLeakTracking('SliverAppBar expandedHeight, pinned and floating', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp( floating: true, pinned: true, @@ -892,7 +1025,7 @@ void main() { expect(tabBarHeight(tester), initialTabBarHeight); }); - testWidgets('SliverAppBar expandedHeight, floating with snap:true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar expandedHeight, floating with snap:true', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp( floating: true, snap: true, @@ -972,7 +1105,7 @@ void main() { expect(appBarBottom(tester), lessThanOrEqualTo(0.0)); }); - testWidgets('SliverAppBar expandedHeight, floating and pinned with snap:true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar expandedHeight, floating and pinned with snap:true', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp( floating: true, pinned: true, @@ -1058,7 +1191,7 @@ void main() { expect(appBarBottom(tester), kTextTabBarHeight); }); - testWidgets('SliverAppBar expandedHeight, collapsedHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar expandedHeight, collapsedHeight', (WidgetTester tester) async { const double expandedAppBarHeight = 400.0; const double collapsedAppBarHeight = 200.0; @@ -1096,7 +1229,7 @@ void main() { expect(tabBarHeight(tester), initialTabBarHeight); }); - testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SliverAppBar.medium defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 112; @@ -1148,7 +1281,9 @@ void main() { // Test the expanded title is positioned correctly. final Offset titleOffset = tester.getBottomLeft(expandedTitle); expect(titleOffset.dx, 16.0); - expect(titleOffset.dy, 96.0); + if (!kIsWeb || isCanvasKit) { + expect(titleOffset.dy, 96.0); + } _verifyTextNotClipped(expandedTitle, tester); @@ -1183,7 +1318,7 @@ void main() { expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); }); - testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SliverAppBar.large defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 152; @@ -1276,11 +1411,34 @@ void main() { expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); }); - testWidgets('AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async { - final bool useMaterial3 = ThemeData().useMaterial3; + testWidgetsWithLeakTracking('Material2 - AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async { + Widget buildAppBar([double? elevation]) { + return MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + appBar: AppBar(title: const Text('Title'), elevation: elevation), + ), + ); + } + + Material getMaterial() => tester.widget<Material>(find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + )); + + // Default elevation should be used for the material. + await tester.pumpWidget(buildAppBar()); + expect(getMaterial().elevation, 4); + + // AppBar should use the specified elevation. + await tester.pumpWidget(buildAppBar(8.0)); + expect(getMaterial().elevation, 8.0); + }); + testWidgetsWithLeakTracking('Material3 - AppBar uses the specified elevation or defaults to 0', (WidgetTester tester) async { Widget buildAppBar([double? elevation]) { return MaterialApp( + theme: ThemeData(useMaterial3: true), home: Scaffold( appBar: AppBar(title: const Text('Title'), elevation: elevation), ), @@ -1294,14 +1452,14 @@ void main() { // Default elevation should be used for the material. await tester.pumpWidget(buildAppBar()); - expect(getMaterial().elevation, useMaterial3 ? 0 : 4); + expect(getMaterial().elevation, 0); // AppBar should use the specified elevation. await tester.pumpWidget(buildAppBar(8.0)); expect(getMaterial().elevation, 8.0); }); - testWidgets('scrolledUnderElevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolledUnderElevation', (WidgetTester tester) async { Widget buildAppBar({double? elevation, double? scrolledUnderElevation}) { return MaterialApp( home: Scaffold( @@ -1334,7 +1492,7 @@ void main() { expect(getMaterial().elevation, 10); }); - testWidgets('scrolledUnderElevation with nested scroll view', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - scrolledUnderElevation with nested scroll view', (WidgetTester tester) async { Widget buildAppBar({double? scrolledUnderElevation}) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1403,7 +1561,7 @@ void main() { ); } - testWidgets('Respects forceElevated parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Respects forceElevated parameter', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/59158. AppBar getAppBar() => tester.widget<AppBar>(find.byType(AppBar)); Material getMaterial() => tester.widget<Material>(find.byType(Material)); @@ -1426,7 +1584,7 @@ void main() { expect(getMaterial().elevation, 8.0); }); - testWidgets('Uses elevation of AppBarTheme by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses elevation of AppBarTheme by default', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/73525. Material getMaterial() => tester.widget<Material>(find.byType(Material)); @@ -1451,48 +1609,53 @@ void main() { // Generates a MaterialApp with a SliverAppBar in a CustomScrollView. // The first cell of the scroll view contains a button at its top, and is // initially scrolled so that it is beneath the SliverAppBar. - Widget buildWidget({ + (ScrollController, Widget) buildWidget({ required bool forceMaterialTransparency, required VoidCallback onPressed }) { const double appBarHeight = 120; - return MaterialApp( - home: Scaffold( - body: CustomScrollView( - controller: ScrollController(initialScrollOffset:appBarHeight), - slivers: <Widget>[ - SliverAppBar( - collapsedHeight: appBarHeight, - expandedHeight: appBarHeight, - pinned: true, - elevation: 0, - backgroundColor: Colors.transparent, - forceMaterialTransparency: forceMaterialTransparency, - title: const Text('AppBar'), - ), - SliverList( - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return SizedBox( - height: appBarHeight, - child: index == 0 - ? Align( - alignment: Alignment.topCenter, - child: TextButton(onPressed: onPressed, child: const Text('press'))) - : const SizedBox(), - ); - }, - childCount: 20, + final ScrollController controller = ScrollController(initialScrollOffset: appBarHeight); + + return ( + controller, + MaterialApp( + home: Scaffold( + body: CustomScrollView( + controller: controller, + slivers: <Widget>[ + SliverAppBar( + collapsedHeight: appBarHeight, + expandedHeight: appBarHeight, + pinned: true, + elevation: 0, + backgroundColor: Colors.transparent, + forceMaterialTransparency: forceMaterialTransparency, + title: const Text('AppBar'), + ), + SliverList( + delegate: SliverChildBuilderDelegate((BuildContext context, int index) { + return SizedBox( + height: appBarHeight, + child: index == 0 + ? Align( + alignment: Alignment.topCenter, + child: TextButton(onPressed: onPressed, child: const Text('press'))) + : const SizedBox(), + ); + }, + childCount: 20, + ), ), - ), - ]), + ]), + ), ), ); } - testWidgets( + testWidgetsWithLeakTracking( 'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async { bool buttonWasPressed = false; - final Widget widget = buildWidget( + final (ScrollController controller, Widget widget) = buildWidget( forceMaterialTransparency:true, onPressed:() { buttonWasPressed = true; }, ); @@ -1505,16 +1668,18 @@ void main() { await tester.tap(buttonFinder); await tester.pump(); expect(buttonWasPressed, isTrue); + + controller.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async { // Set this, and tester.tap(warnIfMissed:false), to suppress // errors/warning that the button is not hittable (which is expected). WidgetController.hitTestWarningShouldBeFatal = false; bool buttonWasPressed = false; - final Widget widget = buildWidget( + final (ScrollController controller, Widget widget) = buildWidget( forceMaterialTransparency:false, onPressed:() { buttonWasPressed = true; }, ); @@ -1527,10 +1692,12 @@ void main() { await tester.tap(buttonFinder, warnIfMissed:false); await tester.pump(); expect(buttonWasPressed, isFalse); + + controller.dispose(); }); }); - testWidgets('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async { const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0)); await tester.pumpWidget( @@ -1655,7 +1822,7 @@ void main() { expect(tester.getTopLeft(find.text('title')).dy, lessThan(100.0)); }); - testWidgets('AppBar in body excludes bottom SafeArea padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar in body excludes bottom SafeArea padding', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/26163 await tester.pumpWidget( Localizations( @@ -1685,7 +1852,37 @@ void main() { expect(appBarHeight(tester), kToolbarHeight + 100.0); }); - testWidgets('AppBar updates when you add a drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.title sees the correct padding from MediaQuery', (WidgetTester tester) async { + bool titleBuilt = false; + await tester.pumpWidget( + Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(padding: EdgeInsets.fromLTRB(12, 34, 56, 78)), + child: Scaffold( + appBar: AppBar( + title: Builder(builder: (BuildContext context) { + titleBuilt = true; + final EdgeInsets padding = MediaQuery.paddingOf(context); + expect(padding, EdgeInsets.zero); + return const Text('heh'); + }), + ), + ), + ), + ), + ), + ); + expect(titleBuilt, isTrue); + }); + + testWidgetsWithLeakTracking('AppBar updates when you add a drawer', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1705,7 +1902,7 @@ void main() { expect(find.byIcon(Icons.menu), findsOneWidget); }); - testWidgets('AppBar does not draw menu for drawer if automaticallyImplyLeading is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar does not draw menu for drawer if automaticallyImplyLeading is false', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1719,7 +1916,7 @@ void main() { expect(find.byIcon(Icons.menu), findsNothing); }); - testWidgets('AppBar does not update the leading if a route is popped case 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar does not update the leading if a route is popped case 1', (WidgetTester tester) async { final Page<void> page1 = MaterialPage<void>( key: const ValueKey<String>('1'), child: Scaffold( @@ -1757,7 +1954,7 @@ void main() { expect(find.byType(BackButton), findsNothing); }); - testWidgets('AppBar does not update the leading if a route is popped case 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar does not update the leading if a route is popped case 2', (WidgetTester tester) async { final Page<void> page1 = MaterialPage<void>( key: const ValueKey<String>('1'), child: Scaffold( @@ -1810,7 +2007,7 @@ void main() { ); }); - testWidgets('AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/58665 final Key key = UniqueKey(); await tester.pumpWidget( @@ -1857,7 +2054,54 @@ void main() { expect(painter, paints..save()..translate()..save()..translate()..circle(x: 24.0, y: 28.0)); }); - testWidgets('AppBar handles loose children 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/58665 + final Key key = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + // Test was designed against InkSplash so need to make sure that is used. + theme: ThemeData( + useMaterial3: true, + splashFactory: InkSplash.splashFactory + ), + home: Center( + child: AppBar( + title: const Text('Abc'), + actions: <Widget>[ + IconButton( + key: key, + icon: const Icon(Icons.add_circle), + tooltip: 'First button', + onPressed: () {}, + ), + ], + flexibleSpace: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: const Alignment(-0.04, 1.0), + colors: <Color>[Colors.blue.shade500, Colors.blue.shade800], + ), + ), + ), + ), + ), + ), + ); + final RenderObject painter = tester.renderObject( + find.descendant( + of: find.descendant( + of: find.byType(AppBar), + matching: find.byType(Stack), + ), + matching: find.byType(Material).last, + ), + ); + await tester.tap(find.byKey(key)); + expect(painter, paints..save()..translate()..save()..translate()..circle(x: 20.0, y: 20.0)); + }); + + testWidgetsWithLeakTracking('AppBar handles loose children 0', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1878,7 +2122,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0)); }); - testWidgets('AppBar handles loose children 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar handles loose children 1', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1908,7 +2152,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0)); }); - testWidgets('AppBar handles loose children 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar handles loose children 2', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1948,7 +2192,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0)); }); - testWidgets('AppBar handles loose children 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar handles loose children 3', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1979,9 +2223,8 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0)); }); - testWidgets('AppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async { const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100)); - final Key leadingKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key trailingKey = UniqueKey(); @@ -2023,9 +2266,8 @@ void main() { expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(10 + NavigationToolbar.kMiddleSpacing, 72)); }); - testWidgets('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async { const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0)); - final Key leadingKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key trailingKey = UniqueKey(); @@ -2061,9 +2303,8 @@ void main() { expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(0.0, 100.0)); }); - testWidgets('SliverAppBar positioning of leading and trailing widgets with bottom padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar positioning of leading and trailing widgets with bottom padding', (WidgetTester tester) async { const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0, bottom: 50.0)); - final Key leadingKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key trailingKey = UniqueKey(); @@ -2098,7 +2339,7 @@ void main() { expect(tester.getRect(find.byKey(trailingKey)), const Rect.fromLTRB(0.0, 100.0, 400.0, 100.0 + 56.0)); }); - testWidgets('SliverAppBar provides correct semantics in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar provides correct semantics in LTR', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2179,7 +2420,7 @@ void main() { semantics.dispose(); }); - testWidgets('SliverAppBar provides correct semantics in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar provides correct semantics in RTL', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2271,7 +2512,7 @@ void main() { semantics.dispose(); }); - testWidgets('AppBar excludes header semantics correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar excludes header semantics correctly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2327,7 +2568,7 @@ void main() { semantics.dispose(); }); - testWidgets('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2396,7 +2637,7 @@ void main() { semantics.dispose(); }); - testWidgets('SliverAppBar with flexible space has correct semantics order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with flexible space has correct semantics order', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/64922. final SemanticsTester semantics = SemanticsTester(tester); @@ -2473,7 +2714,7 @@ void main() { semantics.dispose(); }); - testWidgets('AppBar draws a light system bar for a dark background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - AppBar draws a light system bar for a dark background', (WidgetTester tester) async { final ThemeData darkTheme = ThemeData.dark(useMaterial3: false); await tester.pumpWidget(MaterialApp( theme: darkTheme, @@ -2491,7 +2732,26 @@ void main() { )); }); - testWidgets('AppBar draws a dark system bar for a light background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar draws a light system bar for a dark background', (WidgetTester tester) async { + final ThemeData darkTheme = ThemeData.dark(useMaterial3: true); + await tester.pumpWidget(MaterialApp( + theme: darkTheme, + home: Scaffold( + appBar: AppBar( + title: const Text('test'), + ), + ), + )); + + expect(darkTheme.colorScheme.brightness, Brightness.dark); + expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + )); + }); + + testWidgetsWithLeakTracking('Material2 - AppBar draws a dark system bar for a light background', (WidgetTester tester) async { final ThemeData lightTheme = ThemeData(primarySwatch: Colors.lightBlue, useMaterial3: false); await tester.pumpWidget( MaterialApp( @@ -2511,7 +2771,28 @@ void main() { )); }); - testWidgets('Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar draws a dark system bar for a light background', (WidgetTester tester) async { + final ThemeData lightTheme = ThemeData(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: lightTheme, + home: Scaffold( + appBar: AppBar( + title: const Text('test'), + ), + ), + ), + ); + + expect(lightTheme.colorScheme.brightness, Brightness.light); + expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.light, + statusBarIconBrightness: Brightness.dark, + )); + }); + + testWidgetsWithLeakTracking('Material2 - Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { Widget buildAppBar(ThemeData theme) { return MaterialApp( theme: theme, @@ -2562,31 +2843,98 @@ void main() { } }); - testWidgets('Default status bar color', (WidgetTester tester) async { - Future<void> pumpBoilerplate({required bool useMaterial3}) async { - await tester.pumpWidget( - MaterialApp( - key: GlobalKey(), - theme: ThemeData.light().copyWith( - useMaterial3: useMaterial3, - appBarTheme: const AppBarTheme(), - ), - home: Scaffold( - appBar: AppBar( - title: const Text('title'), - ), - ), + testWidgetsWithLeakTracking('Material3 - Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { + Widget buildAppBar(ThemeData theme) { + return MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar(title: const Text('Title')), + ), + ); + } + + // Using a light theme. + { + await tester.pumpWidget(buildAppBar(ThemeData(useMaterial3: true))); + final Material appBarMaterial = tester.widget<Material>( + find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + ), + ); + final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); + final Brightness onAppBarBrightness = appBarBrightness == Brightness.light + ? Brightness.dark + : Brightness.light; + + expect(SystemChrome.latestStyle, SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: appBarBrightness, + statusBarIconBrightness: onAppBarBrightness, + )); + } + + // Using a dark theme. + { + await tester.pumpWidget(buildAppBar(ThemeData.dark(useMaterial3: true))); + final Material appBarMaterial = tester.widget<Material>( + find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), ), ); + final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); + final Brightness onAppBarBrightness = appBarBrightness == Brightness.light + ? Brightness.dark + : Brightness.light; + + expect(SystemChrome.latestStyle, SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: appBarBrightness, + statusBarIconBrightness: onAppBarBrightness, + )); } + }); + + testWidgetsWithLeakTracking('Material2 - Default status bar color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + key: GlobalKey(), + theme: ThemeData.light().copyWith( + useMaterial3: false, + appBarTheme: const AppBarTheme(), + ), + home: Scaffold( + appBar: AppBar( + title: const Text('title'), + ), + ), + ), + ); - await pumpBoilerplate(useMaterial3: false); expect(SystemChrome.latestStyle!.statusBarColor, null); - await pumpBoilerplate(useMaterial3: true); + }); + + testWidgetsWithLeakTracking('Material3 - Default status bar color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + key: GlobalKey(), + theme: ThemeData.light().copyWith( + useMaterial3: true, + appBarTheme: const AppBarTheme(), + ), + home: Scaffold( + appBar: AppBar( + title: const Text('title'), + ), + ), + ), + ); + expect(SystemChrome.latestStyle!.statusBarColor, Colors.transparent); }); - testWidgets('AppBar systemOverlayStyle is use to style status bar and navigation bar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar systemOverlayStyle is use to style status bar and navigation bar', (WidgetTester tester) async { final SystemUiOverlayStyle systemOverlayStyle = SystemUiOverlayStyle.light.copyWith( statusBarColor: Colors.red, systemNavigationBarColor: Colors.green, @@ -2606,7 +2954,7 @@ void main() { expect(SystemChrome.latestStyle!.systemNavigationBarColor, Colors.green); }); - testWidgets('Changing SliverAppBar snap from true to false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing SliverAppBar snap from true to false', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17598 const double appBarHeight = 256.0; bool snap = true; @@ -2667,7 +3015,7 @@ void main() { await tester.pump(); }); - testWidgets('AppBar shape default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar shape default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: AppBar( @@ -2687,7 +3035,7 @@ void main() { expect(getMaterialWidget(materialFinder).shape, null); }); - testWidgets('AppBar with shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar with shape', (WidgetTester tester) async { const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), ); @@ -2711,7 +3059,7 @@ void main() { expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder); }); - testWidgets('SliverAppBar shape default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar shape default', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: CustomScrollView( @@ -2735,7 +3083,7 @@ void main() { expect(getMaterialWidget(materialFinder).shape, null); }); - testWidgets('SliverAppBar with shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with shape', (WidgetTester tester) async { const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), ); @@ -2763,7 +3111,7 @@ void main() { expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder); }); - testWidgets('AppBars title has upper limit on text scaling, textScaleFactor = 1, 1.34, 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBars title has upper limit on text scaling, textScaleFactor = 1, 1.34, 2', (WidgetTester tester) async { late double textScaleFactor; Widget buildFrame() { @@ -2801,7 +3149,7 @@ void main() { expect(tester.getRect(appBarTitle).height, 24); }); - testWidgets('AppBars with jumbo titles, textScaleFactor = 3, 3.5, 4', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBars with jumbo titles, textScaleFactor = 3, 3.5, 4', (WidgetTester tester) async { double textScaleFactor = 1.0; TextDirection textDirection = TextDirection.ltr; bool centerTitle = false; @@ -2871,7 +3219,7 @@ void main() { expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy); }); - testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar configures the delegate properly', (WidgetTester tester) async { Future<void> buildAndVerifyDelegate({ required bool pinned, required bool floating, required bool snap }) async { await tester.pumpWidget( MaterialApp( @@ -2907,7 +3255,7 @@ void main() { await buildAndVerifyDelegate(pinned: true, floating: true, snap: true); }); - testWidgets('AppBar respects toolbarHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar respects toolbarHeight', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2923,7 +3271,7 @@ void main() { expect(appBarHeight(tester), 48); }); - testWidgets('SliverAppBar default collapsedHeight with respect to toolbarHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar default collapsedHeight with respect to toolbarHeight', (WidgetTester tester) async { const double toolbarHeight = 100.0; await tester.pumpWidget(buildSliverAppBarApp( @@ -2942,7 +3290,7 @@ void main() { expect(appBarHeight(tester), toolbarHeight + initialTabBarHeight); }); - testWidgets('SliverAppBar collapsedHeight with toolbarHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar collapsedHeight with toolbarHeight', (WidgetTester tester) async { const double toolbarHeight = 100.0; const double collapsedHeight = 150.0; @@ -2961,7 +3309,7 @@ void main() { expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight); }); - testWidgets('SliverAppBar collapsedHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar collapsedHeight', (WidgetTester tester) async { const double collapsedHeight = 56.0; await tester.pumpWidget(buildSliverAppBarApp( @@ -2978,7 +3326,7 @@ void main() { expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight); }); - testWidgets('AppBar respects leadingWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar respects leadingWidth', (WidgetTester tester) async { const Key key = Key('leading'); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -2994,7 +3342,7 @@ void main() { expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56)); }); - testWidgets('SliverAppBar respects leadingWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar respects leadingWidth', (WidgetTester tester) async { const Key key = Key('leading'); await tester.pumpWidget(const MaterialApp( home: CustomScrollView( @@ -3012,7 +3360,7 @@ void main() { expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56)); }); - testWidgets("AppBar with EndDrawer doesn't have leading", (WidgetTester tester) async { + testWidgetsWithLeakTracking("AppBar with EndDrawer doesn't have leading", (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( appBar: AppBar(), @@ -3029,7 +3377,7 @@ void main() { expect(getAppBarWidget(appBarFinder).leading, null); }); - testWidgets('AppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( appBar: AppBar( @@ -3042,14 +3390,14 @@ void main() { expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing); }); - testWidgets('SliverAppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async { await tester.pumpWidget(buildSliverAppBarApp()); final NavigationToolbar navToolBar = tester.widget(find.byType(NavigationToolbar)); expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing); }); - testWidgets('AppBar foregroundColor and backgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar foregroundColor and backgroundColor', (WidgetTester tester) async { const Color foregroundColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff00ffff); final Key leadingIconKey = UniqueKey(); @@ -3093,14 +3441,14 @@ void main() { expect(actionIconTheme.color, foregroundColor); // Test icon color - Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color; - Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color; + Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color; + Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color; expect(leadingIconColor(), foregroundColor); expect(actionIconColor(), foregroundColor); }); - testWidgets('Leading, title, and actions show correct default colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leading, title, and actions show correct default colors', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from( colorScheme: const ColorScheme.light( onPrimary: Colors.blue, @@ -3126,8 +3474,8 @@ void main() { Color textColor() { return tester.renderObject<RenderParagraph>(find.text('title')).text.style!.color!; } - Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color; - Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color; + Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color; + Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color; // M2 default color are onPrimary, and M3 has onSurface for leading and title, // onSurfaceVariant for actions. @@ -3137,8 +3485,8 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/107305 - group('Icons are colored correctly by IconTheme and ActionIconTheme in M3', () { - testWidgets('Icons and IconButtons are colored by IconTheme in M3', (WidgetTester tester) async { + group('Material3 - Icons are colored correctly by IconTheme and ActionIconTheme', () { + testWidgetsWithLeakTracking('Material3 - Icons and IconButtons are colored by IconTheme', (WidgetTester tester) async { const Color iconColor = Color(0xff00ff00); final Key leadingIconKey = UniqueKey(); final Key actionIconKey = UniqueKey(); @@ -3161,16 +3509,16 @@ void main() { ), ); - Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color; - Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; + Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color; + Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; expect(leadingIconColor(), iconColor); expect(actionIconColor(), iconColor); expect(actionIconButtonColor(), iconColor); }); - testWidgets('Action icons and IconButtons are colored by ActionIconTheme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Action icons and IconButtons are colored by ActionIconTheme', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from( colorScheme: const ColorScheme.light(), useMaterial3: true, @@ -3197,16 +3545,16 @@ void main() { ), ); - Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color; - Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; + Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color; + Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; expect(leadingIconColor(), themeData.colorScheme.onSurface); expect(actionIconColor(), actionsIconColor); expect(actionIconButtonColor(), actionsIconColor); }); - testWidgets('The actionIconTheme property overrides iconTheme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - The actionIconTheme property overrides iconTheme', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from( colorScheme: const ColorScheme.light(), useMaterial3: true, @@ -3235,16 +3583,16 @@ void main() { ), ); - Color? leadingIconColor() => iconStyle(tester, Icons.add_circle)?.color; - Color? actionIconColor() => iconStyle(tester, Icons.ac_unit)?.color; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; + Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color; + Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; expect(leadingIconColor(), overallIconColor); expect(actionIconColor(), actionsIconColor); expect(actionIconButtonColor(), actionsIconColor); }); - testWidgets('AppBar.iconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar.iconTheme should override any IconButtonTheme present in the theme', (WidgetTester tester) async { final ThemeData themeData = ThemeData( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( @@ -3272,10 +3620,10 @@ void main() { ), ); - Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color; - double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; - double? actionIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize; + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + double? leadingIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + double? actionIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize; expect(leadingIconButtonColor(), Colors.yellow); expect(leadingIconButtonSize(), 30.0); @@ -3283,7 +3631,7 @@ void main() { expect(actionIconButtonSize(), 30.0); }); - testWidgets('AppBar.iconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar.iconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton', (WidgetTester tester) async { final ThemeData themeData = ThemeData( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( @@ -3308,15 +3656,15 @@ void main() { ), ); - Color? leadingIconButtonColor() => iconStyle(tester, Icons.arrow_back)?.color; - double? leadingIconButtonSize() => iconStyle(tester, Icons.arrow_back)?.fontSize; + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.arrow_back)?.color; + double? leadingIconButtonSize() => _iconStyle(tester, Icons.arrow_back)?.fontSize; expect(leadingIconButtonColor(), Colors.yellow); expect(leadingIconButtonSize(), 30.0); }); - testWidgets('AppBar.actionsIconTheme should override any IconButtonTheme present in the theme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar.actionsIconTheme should override any IconButtonTheme present in the theme', (WidgetTester tester) async { final ThemeData themeData = ThemeData( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( @@ -3344,10 +3692,10 @@ void main() { ), ); - Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color; - double? leadingIconButtonSize() => iconStyle(tester, Icons.menu)?.fontSize; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; - double? actionIconButtonSize() => iconStyle(tester, Icons.add)?.fontSize; + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + double? leadingIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + double? actionIconButtonSize() => _iconStyle(tester, Icons.add)?.fontSize; // The leading icon button uses the style in the IconButtonTheme because only actionsIconTheme is provided. expect(leadingIconButtonColor(), Colors.red); @@ -3356,7 +3704,7 @@ void main() { expect(actionIconButtonSize(), 30.0); }); - testWidgets('AppBar.actionsIconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBar.actionsIconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton', (WidgetTester tester) async { final ThemeData themeData = ThemeData( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( @@ -3383,14 +3731,14 @@ void main() { ), ); - Color? actionIconButtonColor() => iconStyle(tester, Icons.arrow_back)?.color; - double? actionIconButtonSize() => iconStyle(tester, Icons.arrow_back)?.fontSize; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.arrow_back)?.color; + double? actionIconButtonSize() => _iconStyle(tester, Icons.arrow_back)?.fontSize; expect(actionIconButtonColor(), Colors.yellow); expect(actionIconButtonSize(), 30.0); }); - testWidgets('The foregroundColor property of the AppBar overrides any IconButtonTheme present in the theme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - The foregroundColor property of the AppBar overrides any IconButtonTheme present in the theme', (WidgetTester tester) async { final ThemeData themeData = ThemeData( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( @@ -3416,12 +3764,124 @@ void main() { ), ); - Color? leadingIconButtonColor() => iconStyle(tester, Icons.menu)?.color; - Color? actionIconButtonColor() => iconStyle(tester, Icons.add)?.color; + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; expect(leadingIconButtonColor(), Colors.purple); expect(actionIconButtonColor(), Colors.purple); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/130485. + testWidgetsWithLeakTracking('Material3 - AppBar.iconTheme is correctly applied in dark mode', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + colorScheme: const ColorScheme.dark().copyWith(onSurfaceVariant: Colors.red), + useMaterial3: true, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + iconTheme: const IconThemeData(color: Colors.white), + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + actions: <Widget>[ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + ], + ), + ), + ), + ); + + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + + expect(leadingIconButtonColor(), Colors.white); + expect(actionIconButtonColor(), Colors.white); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/130485. + testWidgetsWithLeakTracking('Material3 - AppBar.foregroundColor is correctly applied in dark mode', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + colorScheme: const ColorScheme.dark().copyWith(onSurfaceVariant: Colors.red), + useMaterial3: true, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + foregroundColor: Colors.white, + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + actions: <Widget>[ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + ], + ), + ), + ), + ); + + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + + expect(leadingIconButtonColor(), Colors.white); + expect(actionIconButtonColor(), Colors.white); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/130485. + testWidgetsWithLeakTracking('Material3 - AppBar.iconTheme is correctly applied in light mode', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + colorScheme: const ColorScheme.light().copyWith(onSurfaceVariant: Colors.red), + useMaterial3: true, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + iconTheme: const IconThemeData(color: Colors.black87), + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + actions: <Widget>[ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + ], + ), + ), + ), + ); + + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + + expect(leadingIconButtonColor(), Colors.black87); + expect(actionIconButtonColor(), Colors.black87); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/130485. + testWidgetsWithLeakTracking('Material3 - AppBar.foregroundColor is correctly applied in light mode', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + colorScheme: const ColorScheme.light().copyWith(onSurfaceVariant: Colors.red), + useMaterial3: true, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + foregroundColor: Colors.black87, + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + actions: <Widget>[ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + ], + ), + ), + ), + ); + + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + + expect(leadingIconButtonColor(), Colors.black87); + expect(actionIconButtonColor(), Colors.black87); + }); }); group('MaterialStateColor scrolledUnder', () { @@ -3475,7 +3935,7 @@ void main() { ); } - testWidgets('backgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp(contentHeight: 1200.0) ); @@ -3500,7 +3960,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, expandedHeight); }); - testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp(contentHeight: 1200.0, includeFlexibleSpace: true) ); @@ -3525,7 +3985,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, expandedHeight); }); - testWidgets('backgroundColor - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor - reverse', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp(contentHeight: 1200.0, reverse: true) ); @@ -3550,7 +4010,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, expandedHeight); }); - testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp( contentHeight: 1200.0, @@ -3579,7 +4039,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, expandedHeight); }); - testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp(contentHeight: 200, reverse: true) ); @@ -3598,7 +4058,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, expandedHeight); }); - testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { await tester.pumpWidget( buildSliverApp( contentHeight: 200, @@ -3652,7 +4112,7 @@ void main() { ); } - testWidgets('backgroundColor for horizontal scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor for horizontal scrolling', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -3710,7 +4170,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('backgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar(contentHeight: 1200.0) ); @@ -3735,7 +4195,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true) ); @@ -3760,7 +4220,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('backgroundColor - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor - reverse', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar(contentHeight: 1200.0, reverse: true) ); @@ -3788,7 +4248,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar( contentHeight: 1200.0, @@ -3820,7 +4280,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('_handleScrollNotification safely calls setState()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_handleScrollNotification safely calls setState()', (WidgetTester tester) async { // Regression test for failures found in Google internal issue b/185192049. final ScrollController controller = ScrollController(initialScrollOffset: 400); await tester.pumpWidget( @@ -3844,9 +4304,11 @@ void main() { ); expect(tester.takeException(), isNull); + + controller.dispose(); }); - testWidgets('does not trigger on horizontal scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not trigger on horizontal scroll', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -3886,7 +4348,7 @@ void main() { expect(getAppBarBackgroundColor(tester), defaultColor); }); - testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar( contentHeight: 200.0, @@ -3909,7 +4371,7 @@ void main() { expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); - testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { await tester.pumpWidget( buildAppBar( contentHeight: 200.0, @@ -3936,7 +4398,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/80256 - testWidgets('The second page should have a back button even it has a end drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The second page should have a back button even it has a end drawer', (WidgetTester tester) async { final Page<void> page1 = MaterialPage<void>( key: const ValueKey<String>('1'), child: Scaffold( @@ -3973,7 +4435,7 @@ void main() { ); }); - testWidgets('Only local entries that imply app bar dismissal will introduce an back button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Only local entries that imply app bar dismissal will introduce an back button', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -4000,7 +4462,7 @@ void main() { expect(find.byType(BackButton), findsOneWidget); }); - testWidgets('AppBar.preferredHeightFor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.preferredHeightFor', (WidgetTester tester) async { late double preferredHeight; late Size preferredSize; @@ -4053,7 +4515,7 @@ void main() { expect(preferredSize.height, 64); }); - testWidgets('AppBar title with actions should have the same position regardless of centerTitle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar title with actions should have the same position regardless of centerTitle', (WidgetTester tester) async { final Key titleKey = UniqueKey(); bool centerTitle = false; @@ -4083,7 +4545,7 @@ void main() { expect(tester.getTopLeft(title).dx, 16.0); }); - testWidgets('AppBar leading widget can take up arbitrary space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar leading widget can take up arbitrary space', (WidgetTester tester) async { final Key leadingKey = UniqueKey(); final Key titleKey = UniqueKey(); late double leadingWidth; @@ -4113,7 +4575,7 @@ void main() { expect(tester.getSize(find.byKey(leadingKey)).width, leadingWidth); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar.medium collapsed title does not overlap with leading/actions widgets', (WidgetTester tester) async { const String title = 'Medium SliverAppBar Very Long Title'; @@ -4165,7 +4627,7 @@ void main() { expect(titleOffset.dx, lessThan(searchOffset.dx)); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar.large collapsed title does not overlap with leading/actions widgets', (WidgetTester tester) async { const String title = 'Large SliverAppBar Very Long Title'; @@ -4217,7 +4679,7 @@ void main() { expect(titleOffset.dx, lessThan(searchOffset.dx)); }); - testWidgets('SliverAppBar.medium respects title spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium respects title spacing', (WidgetTester tester) async { const String title = 'Medium SliverAppBar Very Long Title'; const double titleSpacing = 16.0; @@ -4310,7 +4772,7 @@ void main() { expect(titleOffset.dx, iconButtonOffset.dx); }); - testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large respects title spacing', (WidgetTester tester) async { const String title = 'Large SliverAppBar Very Long Title'; const double titleSpacing = 16.0; @@ -4402,7 +4864,7 @@ void main() { expect(titleOffset.dx, iconButtonOffset.dx); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar.medium without the leading widget updates collapsed title padding', (WidgetTester tester) async { const String title = 'Medium SliverAppBar Title'; @@ -4464,7 +4926,7 @@ void main() { expect(titleOffset.dx, titleSpacing); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar.large without the leading widget updates collapsed title padding', (WidgetTester tester) async { const String title = 'Large SliverAppBar Title'; @@ -4526,7 +4988,7 @@ void main() { expect(titleOffset.dx, titleSpacing); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar large & medium title respects automaticallyImplyLeading', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/121511 @@ -4584,7 +5046,7 @@ void main() { expect(titleOffset.dx, backButtonOffset.dx + titleSpacing); }); - testWidgets('SliverAppBar.medium with bottom widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium with bottom widget', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/115091 const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 112; @@ -4644,7 +5106,7 @@ void main() { expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight); }); - testWidgets('SliverAppBar.large with bottom widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large with bottom widget', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/115091 const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 152; @@ -4704,7 +5166,7 @@ void main() { expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight); }); - testWidgets('SliverAppBar.medium expanded title has upper limit on text scaling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium expanded title has upper limit on text scaling', (WidgetTester tester) async { const String title = 'Medium AppBar'; Widget buildAppBar({double textScaleFactor = 1.0}) { return MaterialApp( @@ -4743,9 +5205,9 @@ void main() { await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0)); expect(tester.getRect(expandedTitle).height, 43.0); _verifyTextNotClipped(expandedTitle, tester); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('SliverAppBar.large expanded title has upper limit on text scaling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large expanded title has upper limit on text scaling', (WidgetTester tester) async { const String title = 'Large AppBar'; Widget buildAppBar({double textScaleFactor = 1.0}) { return MaterialApp( @@ -4773,24 +5235,17 @@ void main() { await tester.pumpWidget(buildAppBar()); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); final Finder expandedTitle = find.text(title).first; - expect( - tester.getRect(expandedTitle).height, - closeTo( hasIssue99933 ? 37.0 : 36.0, 0.1), - ); + expect(tester.getRect(expandedTitle).height, 36.0); await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0)); expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1)); await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0)); expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1)); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('SliverAppBar.medium expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async { const String title = 'Medium AppBar'; Widget buildAppBar({double textScaleFactor = 1.0}) { return MaterialApp( @@ -4829,9 +5284,9 @@ void main() { await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0)); expect(tester.getBottomLeft(expandedTitle).dy, 107.0); _verifyTextNotClipped(expandedTitle, tester); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('SliverAppBar.large expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async { const String title = 'Large AppBar'; Widget buildAppBar({double textScaleFactor = 1.0}) { return MaterialApp( @@ -4915,7 +5370,7 @@ void main() { ); } - testWidgets( + testWidgetsWithLeakTracking( 'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async { bool buttonWasPressed = false; final Widget widget = buildWidget( @@ -4933,7 +5388,7 @@ void main() { expect(buttonWasPressed, isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async { // Set this, and tester.tap(warnIfMissed:false), to suppress @@ -4962,7 +5417,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SliverAppBar.medium defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 112; @@ -5046,13 +5501,13 @@ void main() { expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); }); - testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SliverAppBar.large defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 152; await tester.pumpWidget(MaterialApp( - theme: ThemeData(useMaterial3: false), + theme: theme, home: Scaffold( body: CustomScrollView( primary: true, diff --git a/packages/flutter/test/material/app_bar_theme_test.dart b/packages/flutter/test/material/app_bar_theme_test.dart index e520391d1a3cb..f73bd9f12dc0d 100644 --- a/packages/flutter/test/material/app_bar_theme_test.dart +++ b/packages/flutter/test/material/app_bar_theme_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { const AppBarTheme appBarTheme = AppBarTheme( @@ -42,9 +43,8 @@ void main() { expect(identical(AppBarTheme.lerp(data, data, 0.5), data), true); }); - testWidgets('Passing no AppBarTheme returns defaults', (WidgetTester tester) async { - final ThemeData theme = ThemeData(); - final bool material3 = theme.useMaterial3; + testWidgetsWithLeakTracking('Material2 - Passing no AppBarTheme returns defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: theme, @@ -64,38 +64,61 @@ void main() { final RichText actionIconText = _getAppBarIconRichText(tester); final DefaultTextStyle text = _getAppBarText(tester); - if (theme.useMaterial3) { - expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); - expect(widget.color, theme.colorScheme.surface); - expect(widget.elevation, 0); - expect(widget.shadowColor, material3 ? Colors.transparent : null); - expect(widget.surfaceTintColor, theme.colorScheme.surfaceTint); - expect(widget.shape, null); - expect(iconTheme.data, IconThemeData(color: theme.colorScheme.onSurface, size: 24)); - expect(actionsIconTheme.data, IconThemeData(color: theme.colorScheme.onSurfaceVariant, size: 24)); - expect(actionIconText.text.style!.color, material3 ? theme.colorScheme.onSurfaceVariant : Colors.black); - expect(text.style, material3 - ? Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: theme.colorScheme.onSurface, decorationColor: theme.colorScheme.onSurface) - : Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: theme.colorScheme.onSurface)); - expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight); - expect(tester.getSize(find.byType(AppBar)).width, 800); - } else { - expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); - expect(widget.color, Colors.blue); - expect(widget.elevation, 4.0); - expect(widget.shadowColor, Colors.black); - expect(widget.surfaceTintColor, null); - expect(widget.shape, null); - expect(iconTheme.data, const IconThemeData(color: Colors.white)); - expect(actionsIconTheme.data, const IconThemeData(color: Colors.white)); - expect(actionIconText.text.style!.color, Colors.white); - expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().white.bodyMedium)); - expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight); - expect(tester.getSize(find.byType(AppBar)).width, 800); - } + expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); + expect(widget.color, Colors.blue); + expect(widget.elevation, 4.0); + expect(widget.shadowColor, Colors.black); + expect(widget.surfaceTintColor, null); + expect(widget.shape, null); + expect(iconTheme.data, const IconThemeData(color: Colors.white)); + expect(actionsIconTheme.data, const IconThemeData(color: Colors.white)); + expect(actionIconText.text.style!.color, Colors.white); + expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().white.bodyMedium)); + expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight); + expect(tester.getSize(find.byType(AppBar)).width, 800); }); - testWidgets('AppBar uses values from AppBarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Passing no AppBarTheme returns defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar( + actions: <Widget>[ + IconButton(icon: const Icon(Icons.share), onPressed: () { }), + ], + ), + ), + ), + ); + + final Material widget = _getAppBarMaterial(tester); + final IconTheme iconTheme = _getAppBarIconTheme(tester); + final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); + final RichText actionIconText = _getAppBarIconRichText(tester); + final DefaultTextStyle text = _getAppBarText(tester); + + expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); + expect(widget.color, theme.colorScheme.surface); + expect(widget.elevation, 0); + expect(widget.shadowColor, Colors.transparent); + expect(widget.surfaceTintColor, theme.colorScheme.surfaceTint); + expect(widget.shape, null); + expect(iconTheme.data, IconThemeData(color: theme.colorScheme.onSurface, size: 24)); + expect(actionsIconTheme.data, IconThemeData(color: theme.colorScheme.onSurfaceVariant, size: 24)); + expect(actionIconText.text.style!.color, theme.colorScheme.onSurfaceVariant); + expect( + text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .copyWith(color: theme.colorScheme.onSurface, decorationColor: theme.colorScheme.onSurface), + ); + expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight); + expect(tester.getSize(find.byType(AppBar)).width, 800); + }); + + testWidgetsWithLeakTracking('AppBar uses values from AppBarTheme', (WidgetTester tester) async { final AppBarTheme appBarTheme = _appBarTheme(); await tester.pumpWidget( @@ -132,7 +155,7 @@ void main() { expect(tester.getSize(find.byType(AppBar)).width, 800); }); - testWidgets('AppBar widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar widget properties take priority over theme', (WidgetTester tester) async { const Brightness brightness = Brightness.dark; const SystemUiOverlayStyle systemOverlayStyle = SystemUiOverlayStyle.light; const Color color = Colors.orange; @@ -188,7 +211,7 @@ void main() { expect(text.style, toolbarTextStyle); }); - testWidgets('AppBar icon color takes priority over everything', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar icon color takes priority over everything', (WidgetTester tester) async { const Color color = Colors.lime; const IconThemeData iconThemeData = IconThemeData(color: Colors.green); const IconThemeData actionsIconThemeData = IconThemeData(color: Colors.lightBlue); @@ -208,7 +231,7 @@ void main() { expect(actionIconText.text.style!.color, color); }); - testWidgets('AppBarTheme properties take priority over ThemeData properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBarTheme properties take priority over ThemeData properties', (WidgetTester tester) async { final AppBarTheme appBarTheme = _appBarTheme(); await tester.pumpWidget( @@ -242,9 +265,9 @@ void main() { expect(text.style, appBarTheme.toolbarTextStyle); }); - testWidgets('ThemeData colorScheme is used when no AppBarTheme is set', (WidgetTester tester) async { - final ThemeData lightTheme = ThemeData.from(colorScheme: const ColorScheme.light()); - final ThemeData darkTheme = ThemeData.from(colorScheme: const ColorScheme.dark()); + testWidgetsWithLeakTracking('Material2 - ThemeData colorScheme is used when no AppBarTheme is set', (WidgetTester tester) async { + final ThemeData lightTheme = ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: false); + final ThemeData darkTheme = ThemeData.from(colorScheme: const ColorScheme.dark(), useMaterial3: false); Widget buildFrame(ThemeData appTheme) { return MaterialApp( theme: appTheme, @@ -262,129 +285,140 @@ void main() { ); } - if (lightTheme.useMaterial3) { - // M3 AppBar defaults for light themes: - // - elevation: 0 - // - shadow color: Colors.transparent - // - surface tint color: ColorScheme.surfaceTint - // - background color: ColorScheme.surface - // - foreground color: ColorScheme.onSurface - // - actions text: style bodyMedium, foreground color - // - status bar brightness: light (based on color scheme brightness) - { - await tester.pumpWidget(buildFrame(lightTheme)); - - final Material widget = _getAppBarMaterial(tester); - final IconTheme iconTheme = _getAppBarIconTheme(tester); - final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); - final RichText actionIconText = _getAppBarIconRichText(tester); - final DefaultTextStyle text = _getAppBarText(tester); - - expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); - expect(widget.color, lightTheme.colorScheme.surface); - expect(widget.elevation, 0); - expect(widget.shadowColor, Colors.transparent); - expect(widget.surfaceTintColor, lightTheme.colorScheme.surfaceTint); - expect(iconTheme.data.color, lightTheme.colorScheme.onSurface); - expect(actionsIconTheme.data.color, lightTheme.colorScheme.onSurface); - expect(actionIconText.text.style!.color, lightTheme.colorScheme.onSurface); - expect(text.style, Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: lightTheme.colorScheme.onSurface)); - } - - // M3 AppBar defaults for dark themes: - // - elevation: 0 - // - shadow color: Colors.transparent - // - surface tint color: ColorScheme.surfaceTint - // - background color: ColorScheme.surface - // - foreground color: ColorScheme.onSurface - // - actions text: style bodyMedium, foreground color - // - status bar brightness: dark (based on background color) - { - await tester.pumpWidget(buildFrame(darkTheme)); - await tester.pumpAndSettle(); // Theme change animation - - final Material widget = _getAppBarMaterial(tester); - final IconTheme iconTheme = _getAppBarIconTheme(tester); - final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); - final RichText actionIconText = _getAppBarIconRichText(tester); - final DefaultTextStyle text = _getAppBarText(tester); - - expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.dark); - expect(widget.color, darkTheme.colorScheme.surface); - expect(widget.elevation, 0); - expect(widget.shadowColor, Colors.transparent); - expect(widget.surfaceTintColor, darkTheme.colorScheme.surfaceTint); - expect(iconTheme.data.color, darkTheme.colorScheme.onSurface); - expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface); - expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface); - expect(text.style, Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: darkTheme.colorScheme.onSurface, decorationColor: darkTheme.colorScheme.onSurface)); - } - } else { - // AppBar M2 defaults for light themes: - // - elevation: 4 - // - shadow color: black - // - surface tint color: null - // - background color: ColorScheme.primary - // - foreground color: ColorScheme.onPrimary - // - actions text: style bodyMedium, foreground color - // - status bar brightness: light (based on color scheme brightness) - { - await tester.pumpWidget(buildFrame(lightTheme)); - - final Material widget = _getAppBarMaterial(tester); - final IconTheme iconTheme = _getAppBarIconTheme(tester); - final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); - final RichText actionIconText = _getAppBarIconRichText(tester); - final DefaultTextStyle text = _getAppBarText(tester); - - expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); - expect(widget.color, lightTheme.colorScheme.primary); - expect(widget.elevation, 4.0); - expect(widget.shadowColor, Colors.black); - expect(widget.surfaceTintColor, null); - expect(iconTheme.data.color, lightTheme.colorScheme.onPrimary); - expect(actionsIconTheme.data.color, lightTheme.colorScheme.onPrimary); - expect(actionIconText.text.style!.color, lightTheme.colorScheme.onPrimary); - expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium).copyWith(color: lightTheme.colorScheme.onPrimary)); - } - - // AppBar M2 defaults for dark themes: - // - elevation: 4 - // - shadow color: black - // - surface tint color: null - // - background color: ColorScheme.surface - // - foreground color: ColorScheme.onSurface - // - actions text: style bodyMedium, foreground color - // - status bar brightness: dark (based on background color) - { - await tester.pumpWidget(buildFrame(darkTheme)); - await tester.pumpAndSettle(); // Theme change animation - - final Material widget = _getAppBarMaterial(tester); - final IconTheme iconTheme = _getAppBarIconTheme(tester); - final IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); - final RichText actionIconText = _getAppBarIconRichText(tester); - final DefaultTextStyle text = _getAppBarText(tester); - - expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); - expect(widget.color, darkTheme.colorScheme.surface); - expect(widget.elevation, 4.0); - expect(widget.shadowColor, Colors.black); - expect(widget.surfaceTintColor, null); - expect(iconTheme.data.color, darkTheme.colorScheme.onSurface); - expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface); - expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface); - expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium).copyWith(color: darkTheme.colorScheme.onSurface)); - } + // AppBar M2 defaults for light themes: + // - elevation: 4 + // - shadow color: black + // - surface tint color: null + // - background color: ColorScheme.primary + // - foreground color: ColorScheme.onPrimary + // - actions text: style bodyMedium, foreground color + // - status bar brightness: light (based on color scheme brightness) + await tester.pumpWidget(buildFrame(lightTheme)); + + Material widget = _getAppBarMaterial(tester); + IconTheme iconTheme = _getAppBarIconTheme(tester); + IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); + RichText actionIconText = _getAppBarIconRichText(tester); + DefaultTextStyle text = _getAppBarText(tester); + + expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); + expect(widget.color, lightTheme.colorScheme.primary); + expect(widget.elevation, 4.0); + expect(widget.shadowColor, Colors.black); + expect(widget.surfaceTintColor, null); + expect(iconTheme.data.color, lightTheme.colorScheme.onPrimary); + expect(actionsIconTheme.data.color, lightTheme.colorScheme.onPrimary); + expect(actionIconText.text.style!.color, lightTheme.colorScheme.onPrimary); + expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium).copyWith(color: lightTheme.colorScheme.onPrimary)); + + // AppBar M2 defaults for dark themes: + // - elevation: 4 + // - shadow color: black + // - surface tint color: null + // - background color: ColorScheme.surface + // - foreground color: ColorScheme.onSurface + // - actions text: style bodyMedium, foreground color + // - status bar brightness: dark (based on background color) + await tester.pumpWidget(buildFrame(darkTheme)); + await tester.pumpAndSettle(); // Theme change animation + + widget = _getAppBarMaterial(tester); + iconTheme = _getAppBarIconTheme(tester); + actionsIconTheme = _getAppBarActionsIconTheme(tester); + actionIconText = _getAppBarIconRichText(tester); + text = _getAppBarText(tester); + + expect(SystemChrome.latestStyle!.statusBarBrightness, SystemUiOverlayStyle.light.statusBarBrightness); + expect(widget.color, darkTheme.colorScheme.surface); + expect(widget.elevation, 4.0); + expect(widget.shadowColor, Colors.black); + expect(widget.surfaceTintColor, null); + expect(iconTheme.data.color, darkTheme.colorScheme.onSurface); + expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface); + expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface); + expect(text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium).copyWith(color: darkTheme.colorScheme.onSurface)); + }); + + testWidgetsWithLeakTracking('Material3 - ThemeData colorScheme is used when no AppBarTheme is set', (WidgetTester tester) async { + final ThemeData lightTheme = ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true); + final ThemeData darkTheme = ThemeData.from(colorScheme: const ColorScheme.dark(), useMaterial3: true); + Widget buildFrame(ThemeData appTheme) { + return MaterialApp( + theme: appTheme, + home: Builder( + builder: (BuildContext context) { + return Scaffold( + appBar: AppBar( + actions: <Widget>[ + IconButton(icon: const Icon(Icons.share), onPressed: () { }), + ], + ), + ); + }, + ), + ); } + + // M3 AppBar defaults for light themes: + // - elevation: 0 + // - shadow color: Colors.transparent + // - surface tint color: ColorScheme.surfaceTint + // - background color: ColorScheme.surface + // - foreground color: ColorScheme.onSurface + // - actions text: style bodyMedium, foreground color + // - status bar brightness: light (based on color scheme brightness) + await tester.pumpWidget(buildFrame(lightTheme)); + + Material widget = _getAppBarMaterial(tester); + IconTheme iconTheme = _getAppBarIconTheme(tester); + IconTheme actionsIconTheme = _getAppBarActionsIconTheme(tester); + RichText actionIconText = _getAppBarIconRichText(tester); + DefaultTextStyle text = _getAppBarText(tester); + + expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); + expect(widget.color, lightTheme.colorScheme.surface); + expect(widget.elevation, 0); + expect(widget.shadowColor, Colors.transparent); + expect(widget.surfaceTintColor, lightTheme.colorScheme.surfaceTint); + expect(iconTheme.data.color, lightTheme.colorScheme.onSurface); + expect(actionsIconTheme.data.color, lightTheme.colorScheme.onSurface); + expect(actionIconText.text.style!.color, lightTheme.colorScheme.onSurface); + expect(text.style, Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: lightTheme.colorScheme.onSurface)); + + // M3 AppBar defaults for dark themes: + // - elevation: 0 + // - shadow color: Colors.transparent + // - surface tint color: ColorScheme.surfaceTint + // - background color: ColorScheme.surface + // - foreground color: ColorScheme.onSurface + // - actions text: style bodyMedium, foreground color + // - status bar brightness: dark (based on background color) + await tester.pumpWidget(buildFrame(darkTheme)); + await tester.pumpAndSettle(); // Theme change animation + + widget = _getAppBarMaterial(tester); + iconTheme = _getAppBarIconTheme(tester); + actionsIconTheme = _getAppBarActionsIconTheme(tester); + actionIconText = _getAppBarIconRichText(tester); + text = _getAppBarText(tester); + + expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.dark); + expect(widget.color, darkTheme.colorScheme.surface); + expect(widget.elevation, 0); + expect(widget.shadowColor, Colors.transparent); + expect(widget.surfaceTintColor, darkTheme.colorScheme.surfaceTint); + expect(iconTheme.data.color, darkTheme.colorScheme.onSurface); + expect(actionsIconTheme.data.color, darkTheme.colorScheme.onSurface); + expect(actionIconText.text.style!.color, darkTheme.colorScheme.onSurface); + expect(text.style, Typography.material2021().englishLike.bodyMedium!.merge(Typography.material2021().black.bodyMedium).copyWith(color: darkTheme.colorScheme.onSurface, decorationColor: darkTheme.colorScheme.onSurface)); }); - testWidgets('AppBar iconTheme with color=null defers to outer IconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar iconTheme with color=null defers to outer IconTheme', (WidgetTester tester) async { // Verify claim made in https://github.com/flutter/flutter/pull/71184#issuecomment-737419215 Widget buildFrame({ Color? appIconColor, Color? appBarIconColor }) { return MaterialApp( - theme: ThemeData.from(useMaterial3: false, colorScheme: const ColorScheme.light()), + theme: ThemeData.from(colorScheme: const ColorScheme.light()), home: IconTheme( data: IconThemeData(color: appIconColor), child: Builder( @@ -419,7 +453,7 @@ void main() { expect(getIconText().text.style!.color, Colors.purple); }); - testWidgets('AppBar uses AppBarTheme.centerTitle when centerTitle is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar uses AppBarTheme.centerTitle when centerTitle is null', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(centerTitle: true)), home: Scaffold(appBar: AppBar( @@ -431,7 +465,7 @@ void main() { expect(navToolBar.centerMiddle, true); }); - testWidgets('AppBar.centerTitle takes priority over AppBarTheme.centerTitle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.centerTitle takes priority over AppBarTheme.centerTitle', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(centerTitle: true)), home: Scaffold( @@ -447,7 +481,7 @@ void main() { expect(navToolBar.centerMiddle, false); }); - testWidgets('AppBar.centerTitle adapts to TargetPlatform when AppBarTheme.centerTitle is null', (WidgetTester tester) async{ + testWidgetsWithLeakTracking('AppBar.centerTitle adapts to TargetPlatform when AppBarTheme.centerTitle is null', (WidgetTester tester) async{ await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Scaffold(appBar: AppBar( @@ -461,7 +495,7 @@ void main() { expect(navToolBar.centerMiddle, true); }); - testWidgets('AppBar.shadowColor takes priority over AppBarTheme.shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.shadowColor takes priority over AppBarTheme.shadowColor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(shadowColor: Colors.red)), home: Scaffold( @@ -477,7 +511,7 @@ void main() { expect(appBar.shadowColor, Colors.yellow); }); - testWidgets('AppBar.surfaceTintColor takes priority over AppBarTheme.surfaceTintColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.surfaceTintColor takes priority over AppBarTheme.surfaceTintColor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(surfaceTintColor: Colors.red)), home: Scaffold( @@ -493,7 +527,7 @@ void main() { expect(appBar.surfaceTintColor, Colors.yellow); }); - testWidgets('AppBarTheme.iconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBarTheme.iconTheme.color takes priority over IconButtonTheme.foregroundColor', (WidgetTester tester) async { const IconThemeData overallIconTheme = IconThemeData(color: Colors.yellow); await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -519,7 +553,7 @@ void main() { expect(actionIconButtonColor, overallIconTheme.color); }); - testWidgets('AppBarTheme.iconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBarTheme.iconTheme.size takes priority over IconButtonTheme.iconSize', (WidgetTester tester) async { const IconThemeData overallIconTheme = IconThemeData(size: 30.0); await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -546,7 +580,7 @@ void main() { }); - testWidgets('AppBarTheme.actionsIconTheme.color takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBarTheme.actionsIconTheme.color takes priority over IconButtonTheme.foregroundColor', (WidgetTester tester) async { const IconThemeData actionsIconTheme = IconThemeData(color: Colors.yellow); final IconButtonThemeData iconButtonTheme = IconButtonThemeData( style: IconButton.styleFrom(foregroundColor: Colors.red), @@ -574,7 +608,7 @@ void main() { expect(actionIconButtonColor, actionsIconTheme.color); }); - testWidgets('AppBarTheme.actionsIconTheme.size takes priority over IconButtonTheme.iconSize - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBarTheme.actionsIconTheme.size takes priority over IconButtonTheme.iconSize', (WidgetTester tester) async { const IconThemeData actionsIconTheme = IconThemeData(size: 30.0); final IconButtonThemeData iconButtonTheme = IconButtonThemeData( style: IconButton.styleFrom(iconSize: 32.0), @@ -601,7 +635,7 @@ void main() { expect(actionIconButtonSize, actionsIconTheme.size); }); - testWidgets('AppBarTheme.foregroundColor takes priority over IconButtonTheme.foregroundColor - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - AppBarTheme.foregroundColor takes priority over IconButtonTheme.foregroundColor', (WidgetTester tester) async { final IconButtonThemeData iconButtonTheme = IconButtonThemeData( style: IconButton.styleFrom(foregroundColor: Colors.red), ); @@ -636,7 +670,7 @@ void main() { expect(actionIconButtonColor, appBarTheme.foregroundColor); }); - testWidgets('AppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async { const double kTitleSpacing = 10; await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(titleSpacing: kTitleSpacing)), @@ -651,7 +685,7 @@ void main() { expect(navToolBar.middleSpacing, kTitleSpacing); }); - testWidgets('AppBar.titleSpacing takes priority over AppBarTheme.titleSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBar.titleSpacing takes priority over AppBarTheme.titleSpacing', (WidgetTester tester) async { const double kTitleSpacing = 10; await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(titleSpacing: kTitleSpacing)), @@ -667,7 +701,7 @@ void main() { expect(navToolBar.middleSpacing, 40); }); - testWidgets('SliverAppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar uses AppBarTheme.titleSpacing', (WidgetTester tester) async { const double kTitleSpacing = 10; await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(titleSpacing: kTitleSpacing)), @@ -684,7 +718,7 @@ void main() { expect(navToolBar.middleSpacing, kTitleSpacing); }); - testWidgets('SliverAppBar.titleSpacing takes priority over AppBarTheme.titleSpacing ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.titleSpacing takes priority over AppBarTheme.titleSpacing ', (WidgetTester tester) async { const double kTitleSpacing = 10; await tester.pumpWidget(MaterialApp( theme: ThemeData(appBarTheme: const AppBarTheme(titleSpacing: kTitleSpacing)), @@ -702,7 +736,7 @@ void main() { expect(navToolbar.middleSpacing, 40); }); - testWidgets('SliverAppBar.medium uses AppBarTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium uses AppBarTheme properties', (WidgetTester tester) async { const String title = 'Medium App Bar'; await tester.pumpWidget(MaterialApp( @@ -758,7 +792,7 @@ void main() { expect(titleOffset.dx, iconOffset.dx + appBarTheme.titleSpacing!); }); - testWidgets('SliverAppBar.medium properties take priority over AppBarTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium properties take priority over AppBarTheme properties', (WidgetTester tester) async { const String title = 'Medium App Bar'; const Color backgroundColor = Color(0xff000099); const Color foregroundColor = Color(0xff00ff98); @@ -835,7 +869,7 @@ void main() { expect(titleOffset.dx, iconOffset.dx + titleSpacing); }); - testWidgets('SliverAppBar.large uses AppBarTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large uses AppBarTheme properties', (WidgetTester tester) async { const String title = 'Large App Bar'; await tester.pumpWidget(MaterialApp( @@ -891,7 +925,7 @@ void main() { expect(titleOffset.dx, iconOffset.dx + appBarTheme.titleSpacing!); }); - testWidgets('SliverAppBar.large properties take priority over AppBarTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large properties take priority over AppBarTheme properties', (WidgetTester tester) async { const String title = 'Large App Bar'; const Color backgroundColor = Color(0xff000099); const Color foregroundColor = Color(0xff00ff98); @@ -968,7 +1002,7 @@ void main() { expect(titleOffset.dx, iconOffset.dx + titleSpacing); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverAppBar medium & large supports foregroundColor', (WidgetTester tester) async { const String title = 'AppBar title'; const AppBarTheme appBarTheme = AppBarTheme(foregroundColor: Color(0xff00ff20)); @@ -1012,7 +1046,7 @@ void main() { expect(largeTitle.text.style!.color, foregroundColor); }); - testWidgets('Default AppBarTheme debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default AppBarTheme debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const AppBarTheme().debugFillProperties(builder); @@ -1024,15 +1058,25 @@ void main() { expect(description, <String>[]); }); - testWidgets('AppBarTheme implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AppBarTheme implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const AppBarTheme( - backgroundColor: Color(0xff000001), + backgroundColor: Color(0xff000000), + foregroundColor: Color(0xff000001), elevation: 8.0, + scrolledUnderElevation: 3, shadowColor: Color(0xff000002), surfaceTintColor: Color(0xff000003), + shape: StadiumBorder(), + iconTheme: IconThemeData(color: Color(0xff000004)), centerTitle: true, titleSpacing: 40.0, + toolbarHeight: 96, + toolbarTextStyle: TextStyle(color: Color(0xff000005)), + titleTextStyle: TextStyle(color: Color(0xff000006)), + systemOverlayStyle: SystemUiOverlayStyle( + systemNavigationBarColor: Color(0xff000007), + ), ).debugFillProperties(builder); final List<String> description = builder.properties @@ -1040,14 +1084,26 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description, <String>[ - 'backgroundColor: Color(0xff000001)', - 'elevation: 8.0', - 'shadowColor: Color(0xff000002)', - 'surfaceTintColor: Color(0xff000003)', - 'centerTitle: true', - 'titleSpacing: 40.0', - ]); + expect( + description, + equalsIgnoringHashCodes( + <String>[ + 'backgroundColor: Color(0xff000000)', + 'foregroundColor: Color(0xff000001)', + 'elevation: 8.0', + 'scrolledUnderElevation: 3.0', + 'shadowColor: Color(0xff000002)', + 'surfaceTintColor: Color(0xff000003)', + 'shape: StadiumBorder(BorderSide(width: 0.0, style: none))', + 'iconTheme: IconThemeData#00000(color: Color(0xff000004))', + 'centerTitle: true', + 'titleSpacing: 40.0', + 'toolbarHeight: 96.0', + 'toolbarTextStyle: TextStyle(inherit: true, color: Color(0xff000005))', + 'titleTextStyle: TextStyle(inherit: true, color: Color(0xff000006))' + ], + ), + ); // On the web, Dart doubles and ints are backed by the same kind of object because // JavaScript does not support integers. So, the Dart double "4.0" is identical @@ -1055,6 +1111,34 @@ void main() { // one is used. This results in a difference for doubles in debugFillProperties between // the web and the rest of Flutter's target platforms. }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87364 + + // This is a regression test for https://github.com/flutter/flutter/issues/130485. + testWidgetsWithLeakTracking('Material3 - AppBarTheme.iconTheme correctly applies custom white color in dark mode', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + appBarTheme: const AppBarTheme(iconTheme: IconThemeData(color: Colors.white)), + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + actions: <Widget>[ + IconButton(icon: const Icon(Icons.add), onPressed: () {}), + ], + ), + ), + ), + ); + + Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color; + Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color; + + expect(leadingIconButtonColor(), Colors.white); + expect(actionIconButtonColor(), Colors.white); + }); } AppBarTheme _appBarTheme() { diff --git a/packages/flutter/test/material/app_builder_test.dart b/packages/flutter/test/material/app_builder_test.dart index 851a88edcf82d..f5b6500ac569a 100644 --- a/packages/flutter/test/material/app_builder_test.dart +++ b/packages/flutter/test/material/app_builder_test.dart @@ -4,19 +4,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets("builder doesn't get called if app doesn't change", (WidgetTester tester) async { + testWidgetsWithLeakTracking("builder doesn't get called if app doesn't change", (WidgetTester tester) async { final List<String> log = <String>[]; final Widget app = MaterialApp( - theme: ThemeData( - useMaterial3: false, - primarySwatch: Colors.green, - ), home: const Placeholder(), builder: (BuildContext context, Widget? child) { log.add('build'); - expect(Theme.of(context).primaryColor, Colors.green); expect(Directionality.of(context), TextDirection.ltr); expect(child, isA<FocusScope>()); return const Placeholder(); @@ -38,18 +34,13 @@ void main() { expect(log, <String>['build']); }); - testWidgets("builder doesn't get called if app doesn't change", (WidgetTester tester) async { + testWidgetsWithLeakTracking("builder doesn't get called if app doesn't change", (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( MaterialApp( - theme: ThemeData( - useMaterial3: false, - primarySwatch: Colors.yellow, - ), home: Builder( builder: (BuildContext context) { log.add('build'); - expect(Theme.of(context).primaryColor, Colors.yellow); expect(Directionality.of(context), TextDirection.rtl); return const Placeholder(); }, diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 734aa878be7a8..2a4110f1d0fff 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -8,8 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class StateMarker extends StatefulWidget { const StateMarker({ super.key, this.child }); @@ -33,7 +32,7 @@ class StateMarkerState extends State<StateMarker> { } void main() { - testWidgets('Can nest apps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can nest apps', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: MaterialApp( @@ -45,7 +44,7 @@ void main() { expect(find.text('Home sweet home'), findsOneWidget); }); - testWidgets('Focus handling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus handling', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget(MaterialApp( home: Material( @@ -58,7 +57,7 @@ void main() { expect(focusNode.hasFocus, isTrue); }); - testWidgets('Can place app inside FocusScope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can place app inside FocusScope', (WidgetTester tester) async { final FocusScopeNode focusScopeNode = FocusScopeNode(); await tester.pumpWidget(FocusScope( @@ -70,9 +69,10 @@ void main() { )); expect(find.text('Home'), findsOneWidget); + focusScopeNode.dispose(); }); - testWidgets('Can show grid without losing sync', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can show grid without losing sync', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: StateMarker(), @@ -94,7 +94,7 @@ void main() { expect(state2.marker, equals('original')); }); - testWidgets('Do not rebuild page during a route transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not rebuild page during a route transition', (WidgetTester tester) async { int buildCounter = 0; await tester.pumpWidget( MaterialApp( @@ -139,7 +139,7 @@ void main() { expect(find.text('Y'), findsOneWidget); }); - testWidgets('Do rebuild the home page if it changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do rebuild the home page if it changes', (WidgetTester tester) async { int buildCounter = 0; await tester.pumpWidget( MaterialApp( @@ -167,7 +167,7 @@ void main() { expect(find.text('B'), findsOneWidget); }); - testWidgets('Do not rebuild the home page if it does not actually change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not rebuild the home page if it does not actually change', (WidgetTester tester) async { int buildCounter = 0; final Widget home = Builder( builder: (BuildContext context) { @@ -189,7 +189,7 @@ void main() { expect(buildCounter, 1); }); - testWidgets('Do rebuild pages that come from the routes table if the MaterialApp changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do rebuild pages that come from the routes table if the MaterialApp changes', (WidgetTester tester) async { int buildCounter = 0; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) { @@ -211,7 +211,7 @@ void main() { expect(buildCounter, 2); }); - testWidgets('Cannot pop the initial route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot pop the initial route', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Text('Home'))); expect(find.text('Home'), findsOneWidget); @@ -224,7 +224,7 @@ void main() { expect(find.text('Home'), findsOneWidget); }); - testWidgets('Default initialRoute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default initialRoute', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: <String, WidgetBuilder>{ '/': (BuildContext context) => const Text('route "/"'), })); @@ -232,7 +232,7 @@ void main() { expect(find.text('route "/"'), findsOneWidget); }); - testWidgets('One-step initial route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('One-step initial route', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( initialRoute: '/a', @@ -251,7 +251,7 @@ void main() { expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); - testWidgets('Return value from pop is correct', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Return value from pop is correct', (WidgetTester tester) async { late Future<Object?> result; await tester.pumpWidget( MaterialApp( @@ -291,7 +291,7 @@ void main() { expect(await result, equals('all done')); }); - testWidgets('Two-step initial route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Two-step initial route', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), @@ -311,7 +311,7 @@ void main() { expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); - testWidgets('Initial route with missing step', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial route with missing step', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), @@ -336,7 +336,7 @@ void main() { } }); - testWidgets('Make sure initialRoute is only used the first time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Make sure initialRoute is only used the first time', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), @@ -371,7 +371,7 @@ void main() { expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); - testWidgets('onGenerateRoute / onUnknownRoute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onGenerateRoute / onUnknownRoute', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( MaterialApp( @@ -393,7 +393,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('MaterialApp with builder and no route information works.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp with builder and no route information works.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/18904 await tester.pumpWidget( MaterialApp( @@ -404,7 +404,7 @@ void main() { ); }); - testWidgets("WidgetsApp doesn't rebuild routes when MediaQuery updates", (WidgetTester tester) async { + testWidgetsWithLeakTracking("WidgetsApp doesn't rebuild routes when MediaQuery updates", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/37878 addTearDown(tester.platformDispatcher.clearAllTestValues); addTearDown(tester.view.reset); @@ -464,19 +464,18 @@ void main() { expect(dependentBuildCount, equals(5)); }); - testWidgets('Can get text scale from media query', (WidgetTester tester) async { - double? textScaleFactor; + testWidgetsWithLeakTracking('Can get text scale from media query', (WidgetTester tester) async { + TextScaler? textScaler; await tester.pumpWidget(MaterialApp( home: Builder(builder:(BuildContext context) { - textScaleFactor = MediaQuery.textScaleFactorOf(context); + textScaler = MediaQuery.textScalerOf(context); return Container(); }), )); - expect(textScaleFactor, isNotNull); - expect(textScaleFactor, equals(1.0)); + expect(textScaler, TextScaler.noScaling); }); - testWidgets('MaterialApp.navigatorKey', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.navigatorKey', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: key, @@ -497,7 +496,7 @@ void main() { expect(key.currentState, isA<NavigatorState>()); }); - testWidgets('Has default material and cupertino localizations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Has default material and cupertino localizations', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Builder( @@ -519,7 +518,7 @@ void main() { expect(find.text('Select All'), findsOneWidget); }); - testWidgets('MaterialApp uses regular theme when themeMode is light', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses regular theme when themeMode is light', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. @@ -567,7 +566,7 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); - testWidgets('MaterialApp uses darkTheme when themeMode is dark', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses darkTheme when themeMode is dark', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. @@ -615,7 +614,7 @@ void main() { expect(appliedTheme.brightness, Brightness.dark); }); - testWidgets('MaterialApp uses regular theme when themeMode is system and platformBrightness is light', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses regular theme when themeMode is system and platformBrightness is light', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. @@ -643,7 +642,7 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); - testWidgets('MaterialApp uses darkTheme when themeMode is system and platformBrightness is dark', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses darkTheme when themeMode is system and platformBrightness is dark', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. @@ -669,7 +668,7 @@ void main() { expect(appliedTheme.brightness, Brightness.dark); }); - testWidgets('MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. @@ -694,7 +693,7 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); - testWidgets('MaterialApp uses fallback light theme when platformBrightness is dark but no theme is provided at all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses fallback light theme when platformBrightness is dark but no theme is provided at all', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. @@ -716,7 +715,7 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); - testWidgets('MaterialApp uses fallback light theme when platformBrightness is light and a dark theme is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses fallback light theme when platformBrightness is light and a dark theme is provided', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. @@ -741,7 +740,7 @@ void main() { expect(appliedTheme.brightness, Brightness.light); }); - testWidgets('MaterialApp uses dark theme when platformBrightness is dark', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses dark theme when platformBrightness is dark', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. @@ -769,7 +768,7 @@ void main() { expect(appliedTheme.brightness, Brightness.dark); }); - testWidgets('MaterialApp uses high contrast theme when appropriate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses high contrast theme when appropriate', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; @@ -797,7 +796,7 @@ void main() { expect(appliedTheme.primaryColor, Colors.blue); }); - testWidgets('MaterialApp uses high contrast dark theme when appropriate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses high contrast dark theme when appropriate', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; @@ -831,7 +830,7 @@ void main() { expect(appliedTheme.primaryColor, Colors.green); }); - testWidgets('MaterialApp uses dark theme when no high contrast dark theme is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp uses dark theme when no high contrast dark theme is provided', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; @@ -859,7 +858,7 @@ void main() { expect(appliedTheme.primaryColor, Colors.lightGreen); }); - testWidgets('MaterialApp animates theme changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp animates theme changes', (WidgetTester tester) async { final ThemeData lightTheme = ThemeData.light(); final ThemeData darkTheme = ThemeData.dark(); await tester.pumpWidget( @@ -899,7 +898,7 @@ void main() { expect(tester.widget<Material>(find.byType(Material)).color, halfBGColor); }); - testWidgets('MaterialApp theme animation can be turned off', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp theme animation can be turned off', (WidgetTester tester) async { final ThemeData lightTheme = ThemeData.light(); final ThemeData darkTheme = ThemeData.dark(); int scaffoldRebuilds = 0; @@ -941,7 +940,7 @@ void main() { expect(scaffoldRebuilds, 2); }); - testWidgets('MaterialApp switches themes when the platformBrightness changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp switches themes when the platformBrightness changes.', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. @@ -980,7 +979,7 @@ void main() { expect(themeAfterBrightnessChange!.brightness, Brightness.dark); }); - testWidgets('MaterialApp provides default overscroll color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - MaterialApp provides default overscroll color', (WidgetTester tester) async { Future<void> slowDrag(WidgetTester tester, Offset start, Offset offset) async { final TestGesture gesture = await tester.startGesture(start); for (int index = 0; index < 10; index += 1) { @@ -1012,7 +1011,7 @@ void main() { expect(painter, paints..circle(color: glowSecondaryColor)); }); - testWidgets('MaterialApp can customize initial routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp can customize initial routes', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -1059,7 +1058,7 @@ void main() { expect(find.text('regular page two'), findsNothing); }); - testWidgets('MaterialApp does create HeroController with the MaterialRectArcTween', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp does create HeroController with the MaterialRectArcTween', (WidgetTester tester) async { final HeroController controller = MaterialApp.createMaterialHeroController(); final Tween<Rect?> tween = controller.createRectTween!( const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), @@ -1068,7 +1067,7 @@ void main() { expect(tween, isA<MaterialRectArcTween>()); }); - testWidgets('MaterialApp.navigatorKey can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.navigatorKey can be updated', (WidgetTester tester) async { final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: key1, @@ -1084,12 +1083,13 @@ void main() { expect(key1.currentState, isNull); }); - testWidgets('MaterialApp.router works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.router works', (WidgetTester tester) async { final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -1101,6 +1101,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); await tester.pumpWidget(MaterialApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), @@ -1113,9 +1114,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('MaterialApp.router route information parser is optional', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('MaterialApp.router route information parser is optional', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -1127,6 +1133,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget(MaterialApp.router( routerDelegate: delegate, @@ -1138,9 +1145,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('MaterialApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('MaterialApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -1152,6 +1164,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( @@ -1163,9 +1176,10 @@ void main() { routerDelegate: delegate, )); expect(tester.takeException(), isAssertionError); + provider.dispose(); }); - testWidgets('MaterialApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -1177,6 +1191,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>(routerDelegate: delegate); await tester.pumpWidget(MaterialApp.router( @@ -1186,15 +1201,19 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('MaterialApp.router router config works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.router router config works', (WidgetTester tester) async { + late SimpleNavigatorRouterDelegate routerDelegate; + addTearDown(() => routerDelegate.dispose()); + late PlatformRouteInformationProvider provider; + addTearDown(() => provider.dispose()); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( - routeInformationProvider: PlatformRouteInformationProvider( + routeInformationProvider: provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ), routeInformationParser: SimpleRouteInformationParser(), - routerDelegate: SimpleNavigatorRouterDelegate( + routerDelegate: routerDelegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, @@ -1217,9 +1236,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('MaterialApp.builder can build app without a Navigator', (WidgetTester tester) async { + }, + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + leakTrackingTestConfig: const LeakTrackingTestConfig( + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('MaterialApp.builder can build app without a Navigator', (WidgetTester tester) async { Widget? builderChild; await tester.pumpWidget(MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1230,7 +1254,7 @@ void main() { expect(builderChild, isNull); }); - testWidgets('MaterialApp has correct default ScrollBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp has correct default ScrollBehavior', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( @@ -1245,7 +1269,7 @@ void main() { expect(ScrollConfiguration.of(capturedContext).runtimeType, MaterialScrollBehavior); }); - testWidgets('A ScrollBehavior can be set for MaterialApp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A ScrollBehavior can be set for MaterialApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( @@ -1263,7 +1287,7 @@ void main() { expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); }); - testWidgets('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), scrollBehavior: const MaterialScrollBehavior(), @@ -1282,27 +1306,10 @@ void main() { expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('ScrollBehavior stretch android overscroll indicator', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - scrollBehavior: const MaterialScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch), - home: ListView( - children: const <Widget>[ - SizedBox( - height: 1000.0, - width: 1000.0, - child: Text('Test'), - ), - ], - ), - )); - - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - - testWidgets('ScrollBehavior stretch android overscroll indicator via useMaterial3 flag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true), + scrollBehavior: const MaterialScrollBehavior(), home: ListView( children: const <Widget>[ SizedBox( @@ -1318,10 +1325,8 @@ void main() { expect(find.byType(GlowingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('Overscroll indicator can be set by theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialScrollBehavior default stretch android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( - // The current default is glowing, setting via the theme should override. - theme: ThemeData().copyWith(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch), home: ListView( children: const <Widget>[ SizedBox( @@ -1337,11 +1342,10 @@ void main() { expect(find.byType(GlowingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('Overscroll indicator in MaterialScrollBehavior takes precedence over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll indicator can be set by theme', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( - // MaterialScrollBehavior.androidOverscrollIndicator takes precedence over theme. - scrollBehavior: const MaterialScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch), - theme: ThemeData().copyWith(androidOverscrollIndicator: AndroidOverscrollIndicator.glow), + // The current default is M3 and stretch overscroll, setting via the theme should override. + theme: ThemeData().copyWith(useMaterial3: false), home: ListView( children: const <Widget>[ SizedBox( @@ -1353,88 +1357,87 @@ void main() { ), )); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); + expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); + expect(find.byType(StretchingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets( - 'ListView clip behavior updates overscroll indicator clip behavior', (WidgetTester tester) async { - Widget buildFrame(Clip clipBehavior) { - return MaterialApp( - theme: ThemeData(useMaterial3: true), - home: Column( - children: <Widget>[ - SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - clipBehavior: clipBehavior, - itemBuilder: (BuildContext context, int index){ - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, - ), + testWidgetsWithLeakTracking('Material3 - ListView clip behavior updates overscroll indicator clip behavior', (WidgetTester tester) async { + Widget buildFrame(Clip clipBehavior) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Column( + children: <Widget>[ + SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + clipBehavior: clipBehavior, + itemBuilder: (BuildContext context, int index){ + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, ), - Opacity( - opacity: 0.5, - child: Container( - color: const Color(0xD0FF0000), - height: 100, - ), + ), + Opacity( + opacity: 0.5, + child: Container( + color: const Color(0xD0FF0000), + height: 100, ), - ], - ), - ); - } + ), + ], + ), + ); + } - // Test default clip behavior. - await tester.pumpWidget(buildFrame(Clip.hardEdge)); + // Test default clip behavior. + await tester.pumpWidget(buildFrame(Clip.hardEdge)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - expect(find.text('Index 1'), findsOneWidget); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + expect(find.text('Index 1'), findsOneWidget); - RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; - // Currently not clipping - expect(renderClip.clipBehavior, equals(Clip.none)); + RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; + // Currently not clipping + expect(renderClip.clipBehavior, equals(Clip.none)); - TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll the start. - await gesture.moveBy(const Offset(0.0, 200.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); - renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; - // Now clipping - expect(renderClip.clipBehavior, equals(Clip.hardEdge)); + TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll the start. + await gesture.moveBy(const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); + renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; + // Now clipping + expect(renderClip.clipBehavior, equals(Clip.hardEdge)); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Test custom clip behavior. - await tester.pumpWidget(buildFrame(Clip.none)); + // Test custom clip behavior. + await tester.pumpWidget(buildFrame(Clip.none)); - renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; - // Currently not clipping - expect(renderClip.clipBehavior, equals(Clip.none)); + renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; + // Currently not clipping + expect(renderClip.clipBehavior, equals(Clip.none)); - gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll the start. - await gesture.moveBy(const Offset(0.0, 200.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); - renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; - // Now clipping - expect(renderClip.clipBehavior, equals(Clip.none)); + gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll the start. + await gesture.moveBy(const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); + renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; + // Now clipping + expect(renderClip.clipBehavior, equals(Clip.none)); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { late BuildContext capturedContext; final UniqueKey uniqueKey = UniqueKey(); await tester.pumpWidget( @@ -1454,7 +1457,7 @@ void main() { expect(capturedContext.dependOnInheritedWidgetOfExactType<MediaQuery>()?.key, uniqueKey); }); - testWidgets('Assert in buildScrollbar that controller != null when using it (vertical)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assert in buildScrollbar that controller != null when using it (vertical)', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = MaterialScrollBehavior(); late BuildContext capturedContext; @@ -1499,7 +1502,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('Assert in buildScrollbar that controller != null when using it (horizontal)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assert in buildScrollbar that controller != null when using it (horizontal)', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = MaterialScrollBehavior(); late BuildContext capturedContext; @@ -1566,7 +1569,9 @@ class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> wit SimpleNavigatorRouterDelegate({ required this.builder, required this.onPopPage, - }); + }) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } @override GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); diff --git a/packages/flutter/test/material/autocomplete_test.dart b/packages/flutter/test/material/autocomplete_test.dart index ad33efd68c667..47c02c4e90842 100644 --- a/packages/flutter/test/material/autocomplete_test.dart +++ b/packages/flutter/test/material/autocomplete_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class User { const User({ @@ -47,7 +46,7 @@ void main() { User(name: 'Charlie', email: 'charlie123@gmail.com'), ]; - testWidgets('can filter and select a list of string options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can filter and select a list of string options', (WidgetTester tester) async { late String lastSelection; await tester.pumpWidget( MaterialApp( @@ -107,7 +106,7 @@ void main() { expect(list.semanticChildCount, 6); }); - testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can filter and select a list of custom User options', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -161,7 +160,7 @@ void main() { expect(list.semanticChildCount, 1); }); - testWidgets('displayStringForOption is displayed in the options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('displayStringForOption is displayed in the options', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -204,7 +203,7 @@ void main() { expect(field.controller!.text, kOptionsUsers.first.name); }); - testWidgets('can build a custom field', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can build a custom field', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -228,7 +227,7 @@ void main() { expect(find.byType(TextFormField), findsNothing); }); - testWidgets('can build custom options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can build custom options', (WidgetTester tester) async { final GlobalKey optionsKey = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -257,7 +256,7 @@ void main() { expect(find.byKey(optionsKey), findsOneWidget); }); - testWidgets('the default Autocomplete options widget has a maximum height of 200', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the default Autocomplete options widget has a maximum height of 200', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: Scaffold( body: Autocomplete<String>( optionsBuilder: (TextEditingValue textEditingValue) { @@ -278,7 +277,7 @@ void main() { expect(resultingHeight, equals(200)); }); - testWidgets('the options height restricts to max desired height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the options height restricts to max desired height', (WidgetTester tester) async { const double desiredHeight = 150.0; await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -307,7 +306,7 @@ void main() { expect(resultingHeight, equals(desiredHeight)); }); - testWidgets('The height of options shrinks to height of resulting items, if less than maxHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The height of options shrinks to height of resulting items, if less than maxHeight', (WidgetTester tester) async { // Returns a Future with the height of the default [Autocomplete] options widget // after the provided text had been entered into the [Autocomplete] field. Future<double> getDefaultOptionsHeight( @@ -355,7 +354,7 @@ void main() { expect(oneItemsHeight, lessThan(twoItemsHeight)); }); - testWidgets('initialValue sets initial text field value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialValue sets initial text field value', (WidgetTester tester) async { late String lastSelection; await tester.pumpWidget( MaterialApp( @@ -416,7 +415,7 @@ void main() { } } - testWidgets('keyboard navigation of the options properly highlights the option', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard navigation of the options properly highlights the option', (WidgetTester tester) async { const Color highlightColor = Color(0xFF112233); await tester.pumpWidget( MaterialApp( @@ -455,13 +454,11 @@ void main() { checkOptionHighlight(tester, 'elephant', highlightColor); }); - testWidgets('keyboard navigation keeps the highlighted option scrolled into view', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard navigation keeps the highlighted option scrolled into view', (WidgetTester tester) async { const Color highlightColor = Color(0xFF112233); await tester.pumpWidget( MaterialApp( - theme: ThemeData.light(useMaterial3: false).copyWith( - focusColor: highlightColor, - ), + theme: ThemeData.light().copyWith(focusColor: highlightColor), home: Scaffold( body: Autocomplete<String>( optionsBuilder: (TextEditingValue textEditingValue) { @@ -481,30 +478,91 @@ void main() { final ListView list = find.byType(ListView).evaluate().first.widget as ListView; expect(list.semanticChildCount, 6); - // Highlighted item should be at the top - expect(tester.getTopLeft(find.text('chameleon')).dy, equals(64.0)); + final Rect optionsGroupRect = tester.getRect(find.byType(ListView)); + const double optionsGroupPadding = 16.0; + + // Highlighted item should be at the top. checkOptionHighlight(tester, 'chameleon', highlightColor); + expect( + tester.getTopLeft(find.text('chameleon')).dy, + equals(optionsGroupRect.top + optionsGroupPadding), + ); - // Move down the list of options - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); + // Move down the list of options. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'elephant'. + await tester.pumpAndSettle(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'goose'. + await tester.pumpAndSettle(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'lemur'. + await tester.pumpAndSettle(); - // First item should have scrolled off the top, and not be selected. - expect(find.text('chameleon'), findsNothing); + // Highlighted item 'lemur' should be centered in the options popup. + checkOptionHighlight(tester, 'lemur', highlightColor); + expect( + tester.getCenter(find.text('lemur')).dy, + equals(optionsGroupRect.center.dy), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // Select 'mouse'. + await tester.pumpAndSettle(); - // Highlighted item 'lemur' should be centered in the options popup - expect(tester.getTopLeft(find.text('mouse')).dy, equals(187.0)); checkOptionHighlight(tester, 'mouse', highlightColor); + // First item should have scrolled off the top, and not be selected. + expect(find.text('chameleon'), findsNothing); + // The other items on screen should not be selected. checkOptionHighlight(tester, 'goose', null); checkOptionHighlight(tester, 'lemur', null); checkOptionHighlight(tester, 'northern white rhinoceros', null); }); + + group('optionsViewOpenDirection', () { + testWidgetsWithLeakTracking('default (down)', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Autocomplete<String>( + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + ), + ), + ), + ); + final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>)) + .optionsViewOpenDirection; + expect(actual, equals(OptionsViewOpenDirection.down)); + }); + + testWidgetsWithLeakTracking('down', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Autocomplete<String>( + optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + ), + ), + ), + ); + final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>)) + .optionsViewOpenDirection; + expect(actual, equals(OptionsViewOpenDirection.down)); + }); + + testWidgetsWithLeakTracking('up', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Autocomplete<String>( + optionsViewOpenDirection: OptionsViewOpenDirection.up, + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + ), + ), + ), + ); + final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>)) + .optionsViewOpenDirection; + expect(actual, equals(OptionsViewOpenDirection.up)); + }); + }); } diff --git a/packages/flutter/test/material/back_button_test.dart b/packages/flutter/test/material/back_button_test.dart index afdba078a123e..164eaafee8b17 100644 --- a/packages/flutter/test/material/back_button_test.dart +++ b/packages/flutter/test/material/back_button_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('BackButton control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton control test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: const Material(child: Text('Home')), @@ -34,7 +35,7 @@ void main() { expect(find.text('Home'), findsOneWidget); }); - testWidgets('BackButton onPressed overrides default pop behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton onPressed overrides default pop behavior', (WidgetTester tester) async { bool customCallbackWasCalled = false; await tester.pumpWidget( MaterialApp( @@ -67,7 +68,7 @@ void main() { expect(customCallbackWasCalled, true); }); - testWidgets('BackButton icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton icon', (WidgetTester tester) async { final Key androidKey = UniqueKey(); final Key iOSKey = UniqueKey(); final Key linuxKey = UniqueKey(); @@ -115,7 +116,7 @@ void main() { expect(windowsIcon.icon == androidIcon.icon, isTrue); }); - testWidgets('BackButton color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton color', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -133,7 +134,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('BackButton color with ButtonStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton color with ButtonStyle', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -154,7 +155,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('BackButton.style.iconColor parameter overrides BackButton.color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton.style.iconColor parameter overrides BackButton.color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -177,7 +178,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('BackButton semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BackButton semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( MaterialApp( @@ -220,7 +221,7 @@ void main() { handle.dispose(); }, variant: TargetPlatformVariant.all()); - testWidgets('CloseButton semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CloseButton semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( MaterialApp( @@ -263,7 +264,7 @@ void main() { handle.dispose(); }, variant: TargetPlatformVariant.all()); - testWidgets('CloseButton color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CloseButton color', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -281,7 +282,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('CloseButton color with ButtonStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CloseButton color with ButtonStyle', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -302,7 +303,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('CloseButton.style.iconColor parameter overrides CloseButton.color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CloseButton.style.iconColor parameter overrides CloseButton.color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -325,7 +326,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('CloseButton onPressed overrides default pop behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CloseButton onPressed overrides default pop behavior', (WidgetTester tester) async { bool customCallbackWasCalled = false; await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/badge_test.dart b/packages/flutter/test/material/badge_test.dart index 4b97861418da9..b5b5b3c658102 100644 --- a/packages/flutter/test/material/badge_test.dart +++ b/packages/flutter/test/material/badge_test.dart @@ -2,15 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Large Badge defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Large Badge defaults', (WidgetTester tester) async { late final ThemeData theme; await tester.pumpWidget( @@ -47,16 +47,16 @@ void main() { expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size expect(tester.getTopLeft(find.byType(Badge)), Offset.zero); - expect(tester.getTopLeft(find.text('0')), const Offset(16, -4)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getTopLeft(find.text('0')), const Offset(16, -4)); + } final RenderBox box = tester.renderObject(find.byType(Badge)); - final RRect rrect = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? RRect.fromLTRBR(12, -4, 31.5, 12, const Radius.circular(8)) - : RRect.fromLTRBR(12, -4, 32, 12, const Radius.circular(8)); + final RRect rrect = RRect.fromLTRBR(12, -4, 31.5, 12, const Radius.circular(8)); expect(box, paints..rrect(rrect: rrect, color: theme.colorScheme.error)); }); - testWidgets('Large Badge defaults with RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Large Badge defaults with RTL', (WidgetTester tester) async { late final ThemeData theme; await tester.pumpWidget( @@ -89,17 +89,17 @@ void main() { expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size expect(tester.getTopLeft(find.byType(Badge)), Offset.zero); - expect(tester.getTopLeft(find.text('0')), const Offset(0, -4)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getTopLeft(find.text('0')), const Offset(0, -4)); + } final RenderBox box = tester.renderObject(find.byType(Badge)); - final RRect rrect = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? RRect.fromLTRBR(-4, -4, 15.5, 12, const Radius.circular(8)) - : RRect.fromLTRBR(-4, -4, 16, 12, const Radius.circular(8)); + final RRect rrect = RRect.fromLTRBR(-4, -4, 15.5, 12, const Radius.circular(8)); expect(box, paints..rrect(rrect: rrect, color: theme.colorScheme.error)); }); // Essentially the same as 'Large Badge defaults' - testWidgets('Badge.count', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Badge.count', (WidgetTester tester) async { late final ThemeData theme; Widget buildFrame(int count) { @@ -141,7 +141,9 @@ void main() { // x = alignment.start + padding.left // y = alignment.top - expect(tester.getTopLeft(find.text('0')), const Offset(16, -4)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getTopLeft(find.text('0')), const Offset(16, -4)); + } final RenderBox box = tester.renderObject(find.byType(Badge)); // '0'.width = 12 @@ -149,16 +151,14 @@ void main() { // T = alignment.top // R = L + '0'.width + padding.width // B = T + largeSize, R = largeSize/2 - final RRect rrect = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? RRect.fromLTRBR(12, -4, 31.5, 12, const Radius.circular(8)) - : RRect.fromLTRBR(12, -4, 32, 12, const Radius.circular(8)); + final RRect rrect = RRect.fromLTRBR(12, -4, 31.5, 12, const Radius.circular(8)); expect(box, paints..rrect(rrect: rrect, color: theme.colorScheme.error)); await tester.pumpWidget(buildFrame(1000)); expect(find.text('999+'), findsOneWidget); }); - testWidgets('Small Badge defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small Badge defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData.light(useMaterial3: true); await tester.pumpWidget( @@ -189,7 +189,7 @@ void main() { expect(box, paints..rrect(rrect: RRect.fromLTRBR(18, 0, 24, 6, const Radius.circular(3)), color: theme.colorScheme.error)); }); - testWidgets('Small Badge RTL defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small Badge RTL defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData.light(useMaterial3: true); await tester.pumpWidget( @@ -222,7 +222,7 @@ void main() { expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 0, 6, 6, const Radius.circular(3)), color: theme.colorScheme.error)); }); - testWidgets('Large Badge textStyle and colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Large Badge textStyle and colors', (WidgetTester tester) async { final ThemeData theme = ThemeData.light(useMaterial3: true); const Color green = Color(0xff00ff00); const Color black = Color(0xff000000); @@ -249,7 +249,7 @@ void main() { expect(tester.renderObject(find.byType(Badge)), paints..rrect(color: black)); }); - testWidgets('isLabelVisible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isLabelVisible', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true), @@ -273,7 +273,7 @@ void main() { expect(box, isNot(paints..rrect())); }); - testWidgets('Large Badge alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Large Badge alignment', (WidgetTester tester) async { const Radius badgeRadius = Radius.circular(8); Widget buildFrame(Alignment alignment, [Offset offset = Offset.zero]) { @@ -348,7 +348,7 @@ void main() { expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 200 - 16, 200, 200, badgeRadius).shift(offset))); }); - testWidgets('Small Badge alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small Badge alignment', (WidgetTester tester) async { const Radius badgeRadius = Radius.circular(3); Widget buildFrame(Alignment alignment, [Offset offset = Offset.zero]) { diff --git a/packages/flutter/test/material/badge_theme_test.dart b/packages/flutter/test/material/badge_theme_test.dart index ca820eef310c7..9ab250eaab492 100644 --- a/packages/flutter/test/material/badge_theme_test.dart +++ b/packages/flutter/test/material/badge_theme_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('BadgeThemeData copyWith, ==, hashCode basics', () { @@ -32,7 +31,7 @@ void main() { expect(themeData.offset, null); }); - testWidgets('Default BadgeThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default BadgeThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BadgeThemeData().debugFillProperties(builder); @@ -44,7 +43,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('BadgeThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BadgeThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BadgeThemeData( backgroundColor: Color(0xfffffff0), @@ -74,7 +73,7 @@ void main() { ]); }); - testWidgets('Badge uses ThemeData badge theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Badge uses ThemeData badge theme', (WidgetTester tester) async { const Color green = Color(0xff00ff00); const Color black = Color(0xff000000); const BadgeThemeData badgeTheme = BadgeThemeData( @@ -123,7 +122,7 @@ void main() { // This test is essentially the same as 'Badge uses ThemeData badge theme'. In // this case the theme is introduced with the BadgeTheme widget instead of // ThemeData.badgeTheme. - testWidgets('Badge uses BadgeTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Badge uses BadgeTheme', (WidgetTester tester) async { const Color green = Color(0xff00ff00); const Color black = Color(0xff000000); const BadgeThemeData badgeTheme = BadgeThemeData( diff --git a/packages/flutter/test/material/banner_test.dart b/packages/flutter/test/material/banner_test.dart index e1a8843c8be78..4d766fbfc188b 100644 --- a/packages/flutter/test/material/banner_test.dart +++ b/packages/flutter/test/material/banner_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('MaterialBanner properties are respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner properties are respected', (WidgetTester tester) async { const String contentText = 'Content'; const Color backgroundColor = Colors.pink; const Color surfaceTintColor = Colors.green; @@ -47,7 +48,7 @@ void main() { expect(divider.color, dividerColor); }); - testWidgets('MaterialBanner properties are respected when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner properties are respected when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); const Color backgroundColor = Colors.pink; @@ -104,7 +105,7 @@ void main() { expect(divider.color, dividerColor); }); - testWidgets('Actions laid out below content if more than one action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out below content if more than one action', (WidgetTester tester) async { const String contentText = 'Content'; await tester.pumpWidget( @@ -131,7 +132,7 @@ void main() { expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); }); - testWidgets('Actions laid out below content if more than one action when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out below content if more than one action when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -174,7 +175,7 @@ void main() { expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); }); - testWidgets('Actions laid out beside content if only one action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out beside content if only one action', (WidgetTester tester) async { const String contentText = 'Content'; await tester.pumpWidget( @@ -197,7 +198,7 @@ void main() { expect(contentBottomLeft.dx, lessThan(actionsTopRight.dx)); }); - testWidgets('Actions laid out beside content if only one action when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out beside content if only one action when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -269,7 +270,7 @@ void main() { ); } - testWidgets('Elevation defaults to 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Elevation defaults to 0', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(buildBanner(tapTarget)); @@ -294,7 +295,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Uses elevation of MaterialBannerTheme by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses elevation of MaterialBannerTheme by default', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(buildBanner(tapTarget, themeElevation: 6.0)); @@ -312,7 +313,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scaffold body is pushed down if elevation is 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold body is pushed down if elevation is 0', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(buildBanner(tapTarget, elevation: 0.0)); @@ -327,7 +328,7 @@ void main() { }); }); - testWidgets('MaterialBanner control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner control test', (WidgetTester tester) async { const String helloMaterialBanner = 'Hello MaterialBanner'; const Key tapTarget = Key('tap-target'); const Key dismissTarget = Key('dismiss-target'); @@ -379,7 +380,7 @@ void main() { expect(find.text(helloMaterialBanner), findsNothing); }); - testWidgets('MaterialBanner twice test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner twice test', (WidgetTester tester) async { int materialBannerCount = 0; const Key tapTarget = Key('tap-target'); const Key dismissTarget = Key('dismiss-target'); @@ -461,7 +462,7 @@ void main() { expect(find.text('banner2'), findsNothing); }); - testWidgets('ScaffoldMessenger does not duplicate a MaterialBanner when presenting a SnackBar.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger does not duplicate a MaterialBanner when presenting a SnackBar.', (WidgetTester tester) async { const Key materialBannerTapTarget = Key('materialbanner-tap-target'); const Key snackBarTapTarget = Key('snackbar-tap-target'); const String snackBarText = 'SnackBar'; @@ -519,7 +520,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/39574 - testWidgets('Single action laid out beside content but aligned to the trailing edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Single action laid out beside content but aligned to the trailing edge', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: MaterialBanner( @@ -540,7 +541,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/39574 - testWidgets('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -578,7 +579,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/39574 - testWidgets('Single action laid out beside content but aligned to the trailing edge - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Single action laid out beside content but aligned to the trailing edge - RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -598,10 +599,10 @@ void main() { final Offset actionsTopLeft = tester.getTopLeft(find.byType(OverflowBar)); final Offset bannerTopLeft = tester.getTopLeft(find.byType(MaterialBanner)); - expect(actionsTopLeft.dx - 8, bannerTopLeft.dx); // actions OverflowBar is padded by 8 + expect(actionsTopLeft.dx - 8, moreOrLessEquals(bannerTopLeft.dx)); // actions OverflowBar is padded by 8 }); - testWidgets('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Single action laid out beside content but aligned to the trailing edge when presented by ScaffoldMessenger - RTL', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( home: Directionality( @@ -638,10 +639,10 @@ void main() { final Offset actionsTopLeft = tester.getTopLeft(find.byType(OverflowBar)); final Offset bannerTopLeft = tester.getTopLeft(find.byType(MaterialBanner)); - expect(actionsTopLeft.dx - 8, bannerTopLeft.dx); // actions OverflowBar is padded by 8 + expect(actionsTopLeft.dx - 8, moreOrLessEquals(bannerTopLeft.dx)); // actions OverflowBar is padded by 8 }); - testWidgets('Actions laid out below content if forced override', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out below content if forced override', (WidgetTester tester) async { const String contentText = 'Content'; await tester.pumpWidget( @@ -665,7 +666,7 @@ void main() { expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); }); - testWidgets('Actions laid out below content if forced override when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions laid out below content if forced override when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -705,7 +706,7 @@ void main() { expect(contentBottomLeft.dx, lessThan(actionsTopLeft.dx)); }); - testWidgets('Action widgets layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action widgets layout', (WidgetTester tester) async { // This regression test ensures that the action widgets layout matches what // it was, before ButtonBar was replaced by OverflowBar. Widget buildFrame(int actionCount, TextDirection textDirection) { @@ -749,7 +750,7 @@ void main() { expect(tester.getTopLeft(action2), const Offset(8, 130)); }); - testWidgets('Action widgets layout when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action widgets layout when presented by ScaffoldMessenger', (WidgetTester tester) async { // This regression test ensures that the action widgets layout matches what // it was, before ButtonBar was replaced by OverflowBar. @@ -836,7 +837,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Action widgets layout with overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action widgets layout with overflow', (WidgetTester tester) async { // This regression test ensures that the action widgets layout matches what // it was, before ButtonBar was replaced by OverflowBar. const int actionCount = 4; @@ -871,7 +872,7 @@ void main() { } }); - testWidgets('Action widgets layout with overflow when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action widgets layout with overflow when presented by ScaffoldMessenger', (WidgetTester tester) async { // This regression test ensures that the action widgets layout matches what // it was, before ButtonBar was replaced by OverflowBar. @@ -942,7 +943,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('[overflowAlignment] test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('[overflowAlignment] test', (WidgetTester tester) async { const int actionCount = 4; Widget buildFrame(TextDirection textDirection, OverflowBarAlignment overflowAlignment) { return MaterialApp( @@ -979,7 +980,7 @@ void main() { } }); - testWidgets('[overflowAlignment] test when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('[overflowAlignment] test when presented by ScaffoldMessenger', (WidgetTester tester) async { const int actionCount = 4; Widget buildFrame(TextDirection textDirection, OverflowBarAlignment overflowAlignment) { return MaterialApp( @@ -1054,7 +1055,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('ScaffoldMessenger will alert for MaterialBanners that cannot be presented', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger will alert for MaterialBanners that cannot be presented', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103004 await tester.pumpWidget(const MaterialApp( home: Center(), @@ -1083,7 +1084,7 @@ void main() { ); }); - testWidgets('Custom Margin respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Margin respected', (WidgetTester tester) async { const EdgeInsets margin = EdgeInsets.all(30); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/banner_theme_test.dart b/packages/flutter/test/material/banner_theme_test.dart index 4fd5f3a05c319..1287699d39cd2 100644 --- a/packages/flutter/test/material/banner_theme_test.dart +++ b/packages/flutter/test/material/banner_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('MaterialBannerThemeData copyWith, ==, hashCode basics', () { @@ -24,7 +25,7 @@ void main() { expect(bannerTheme.leadingPadding, null); }); - testWidgets('Default MaterialBannerThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default MaterialBannerThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const MaterialBannerThemeData().debugFillProperties(builder); @@ -36,7 +37,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('MaterialBannerThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBannerThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const MaterialBannerThemeData( backgroundColor: Color(0xfffffff0), @@ -66,7 +67,7 @@ void main() { ]); }); - testWidgets('Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { const String contentText = 'Content'; final ThemeData theme = ThemeData(useMaterial3: true); late final ThemeData localizedTheme; @@ -115,7 +116,7 @@ void main() { expect(divider.color, theme.colorScheme.outlineVariant); }); - testWidgets('Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); final ThemeData theme = ThemeData(useMaterial3: true); @@ -178,7 +179,7 @@ void main() { expect(divider.color, theme.colorScheme.outlineVariant); }); - testWidgets('MaterialBanner uses values from MaterialBannerThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner uses values from MaterialBannerThemeData', (WidgetTester tester) async { final MaterialBannerThemeData bannerTheme = _bannerTheme(); const String contentText = 'Content'; await tester.pumpWidget(MaterialApp( @@ -217,7 +218,7 @@ void main() { expect(find.byType(Divider), findsNothing); }); - testWidgets('MaterialBanner uses values from MaterialBannerThemeData when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner uses values from MaterialBannerThemeData when presented by ScaffoldMessenger', (WidgetTester tester) async { final MaterialBannerThemeData bannerTheme = _bannerTheme(); const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); @@ -273,7 +274,7 @@ void main() { expect(find.byType(Divider), findsNothing); }); - testWidgets('MaterialBanner widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner widget properties take priority over theme', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; const Color surfaceTintColor = Colors.red; const Color shadowColor = Colors.orange; @@ -324,7 +325,7 @@ void main() { expect(find.byType(Divider), findsNothing); }); - testWidgets('MaterialBanner widget properties take priority over theme when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner widget properties take priority over theme when presented by ScaffoldMessenger', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; const double elevation = 6.0; const TextStyle textStyle = TextStyle(color: Colors.green); @@ -387,7 +388,7 @@ void main() { expect(find.byType(Divider), findsNothing); }); - testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async { final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple); const String contentText = 'Content'; await tester.pumpWidget(MaterialApp( @@ -409,7 +410,7 @@ void main() { expect(material.color, colorScheme.surface); }); - testWidgets('MaterialBanner uses color scheme when necessary when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialBanner uses color scheme when necessary when presented by ScaffoldMessenger', (WidgetTester tester) async { final ColorScheme colorScheme = const ColorScheme.light().copyWith(surface: Colors.purple); const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); @@ -453,7 +454,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { const String contentText = 'Content'; await tester.pumpWidget(MaterialApp( @@ -499,7 +500,7 @@ void main() { expect(divider.color, null); }); - testWidgets('Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 5e2c9c82a6f36..05f7da0b696d8 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart @@ -10,11 +10,9 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('shadow effect is not doubled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Shadow effect is not doubled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/123064 debugDisableShadows = false; @@ -40,7 +38,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('only one layer with `color` is painted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Only one layer with `color` is painted', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/122667 const Color bottomAppBarColor = Colors.black45; @@ -50,7 +48,6 @@ void main() { home: const Scaffold( bottomNavigationBar: BottomAppBar( color: bottomAppBarColor, - // Avoid getting a surface tint color, to keep the color check below simple elevation: 0, ), @@ -79,7 +76,7 @@ void main() { } }); - testWidgets('no overlap with floating action button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No overlap with floating action button', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -110,7 +107,7 @@ void main() { ); }); - testWidgets('custom shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Custom shape', (WidgetTester tester) async { final Key key = UniqueKey(); Future<void> pump(FloatingActionButtonLocation location) async { await tester.pumpWidget( @@ -144,53 +141,65 @@ void main() { await pump(FloatingActionButtonLocation.endDocked); await expectLater( find.byKey(key), - matchesGoldenFile('bottom_app_bar.custom_shape.1.png'), + matchesGoldenFile('m2_bottom_app_bar.custom_shape.1.png'), ); await pump(FloatingActionButtonLocation.centerDocked); await tester.pumpAndSettle(); await expectLater( find.byKey(key), - matchesGoldenFile('bottom_app_bar.custom_shape.2.png'), + matchesGoldenFile('m2_bottom_app_bar.custom_shape.2.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572 - testWidgets('Custom Padding', (WidgetTester tester) async { - const EdgeInsets customPadding = EdgeInsets.all(10); - await tester.pumpWidget( - MaterialApp( - theme: ThemeData.from(colorScheme: const ColorScheme.light()), - home: Builder( - builder: (BuildContext context) { - return const Scaffold( - body: Align( - alignment: Alignment.bottomCenter, - child: BottomAppBar( - padding: customPadding, - child: ColoredBox( - color: Colors.green, - child: SizedBox(width: 300, height: 60), + testWidgetsWithLeakTracking('Material3 - Custom shape', (WidgetTester tester) async { + final Key key = UniqueKey(); + Future<void> pump(FloatingActionButtonLocation location) async { + await tester.pumpWidget( + SizedBox( + width: 200, + height: 200, + child: RepaintBoundary( + key: key, + child: MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + ), + floatingActionButtonLocation: location, + bottomNavigationBar: const BottomAppBar( + shape: AutomaticNotchedShape( + BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), + ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))), ), + notchMargin: 10.0, + color: Colors.green, + child: SizedBox(height: 100.0), ), ), - ); - }, + ), + ), ), - ), + ); + } + await pump(FloatingActionButtonLocation.endDocked); + await expectLater( + find.byKey(key), + matchesGoldenFile('m3_bottom_app_bar.custom_shape.1.png'), ); + await pump(FloatingActionButtonLocation.centerDocked); + await tester.pumpAndSettle(); + await expectLater( + find.byKey(key), + matchesGoldenFile('m3_bottom_app_bar.custom_shape.2.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572 - final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar)); - expect(bottomAppBar.padding, customPadding); - final Rect babRect = tester.getRect(find.byType(BottomAppBar)); - final Rect childRect = tester.getRect(find.byType(ColoredBox)); - expect(childRect, const Rect.fromLTRB(250, 530, 550, 590)); - expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); - }); - - testWidgets('Custom Padding in Material 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Padding', (WidgetTester tester) async { const EdgeInsets customPadding = EdgeInsets.all(10); await tester.pumpWidget( MaterialApp( - theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true), + theme: ThemeData.from(colorScheme: const ColorScheme.light()), home: Builder( builder: (BuildContext context) { return const Scaffold( @@ -218,7 +227,7 @@ void main() { expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); }); - testWidgets('color defaults to Theme.bottomAppBarColor in M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -244,7 +253,7 @@ void main() { expect(physicalShape.color, const Color(0xffffff00)); }); - testWidgets('color overrides theme color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Color overrides theme color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -275,11 +284,12 @@ void main() { }); - testWidgets('color overrides theme color with Material 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Color overrides theme color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true).copyWith( - bottomAppBarColor: const Color(0xffffff00)), + bottomAppBarColor: const Color(0xffffff00), + ), home: Builder( builder: (BuildContext context) { return const Scaffold( @@ -302,7 +312,7 @@ void main() { expect(physicalShape.color, const Color(0xff0000ff)); }); - testWidgets('Shadow color is transparent in Material 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Shadow color is transparent', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true, @@ -324,7 +334,7 @@ void main() { expect(physicalShape.shadowColor, Colors.transparent); }); - testWidgets('dark theme applies an elevation overlay color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Dark theme applies an elevation overlay color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(useMaterial3: false, colorScheme: const ColorScheme.dark()), @@ -342,10 +352,30 @@ void main() { expect(physicalShape.color, const Color(0xFF2D2D2D)); }); + testWidgetsWithLeakTracking('Material3 - Dark theme applies an elevation overlay color', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.dark(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.from(useMaterial3: true, colorScheme: colorScheme), + home: Scaffold( + bottomNavigationBar: BottomAppBar( + color: colorScheme.surface, + ), + ), + ), + ); + + final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); + + const double elevation = 3.0; // Default for M3. + final Color overlayColor = ElevationOverlay.applySurfaceTint(colorScheme.surface, colorScheme.surfaceTint, elevation); + expect(physicalShape.color, overlayColor); + }); + // This is a regression test for a bug we had where toggling the notch on/off // would crash, as the shouldReclip method of ShapeBorderClipper or // _BottomAppBarClipper would try an illegal downcast. - testWidgets('toggle shape to null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toggle shape to null', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -375,7 +405,7 @@ void main() { ); }); - testWidgets('no notch when notch param is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no notch when notch param is null', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -405,7 +435,7 @@ void main() { ); }); - testWidgets('notch no margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('notch no margin', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -457,7 +487,7 @@ void main() { ); }); - testWidgets('notch with margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('notch with margin', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -509,7 +539,7 @@ void main() { ); }); - testWidgets('observes safe area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Observes safe area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -534,7 +564,38 @@ void main() { ); }); - testWidgets('clipBehavior is propagated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Observes safe area', (WidgetTester tester) async { + const double safeAreaPadding = 50.0; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const MediaQuery( + data: MediaQueryData( + padding: EdgeInsets.all(safeAreaPadding), + ), + child: Scaffold( + bottomNavigationBar: BottomAppBar( + child: Center( + child: Text('safe'), + ), + ), + ), + ), + ), + ); + + const double appBarVerticalPadding = 12.0; + const double appBarHorizontalPadding = 16.0; + expect( + tester.getBottomLeft(find.widgetWithText(Center, 'safe')), + const Offset( + safeAreaPadding + appBarHorizontalPadding, + 600 - safeAreaPadding - appBarVerticalPadding, + ), + ); + }); + + testWidgetsWithLeakTracking('clipBehavior is propagated', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -568,9 +629,45 @@ void main() { expect(physicalShape.clipBehavior, Clip.antiAliasWithSaveLayer); }); - testWidgets('BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/80878 + final ThemeData theme = ThemeData(useMaterial3: false); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.green, + child: const Icon(Icons.home), + onPressed: () {}, + ), + body: Stack( + children: <Widget>[ + Container( + color: Colors.amber, + ), + Container( + alignment: Alignment.bottomCenter, + child: BottomAppBar( + color: Colors.green, + shape: const CircularNotchedRectangle(), + child: Container(height: 50), + ), + ), + ], + ), + ), + ), + ); + + expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584)); + expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 50)); + }); + + testWidgetsWithLeakTracking('Material3 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80878 - final ThemeData theme = ThemeData(); + final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, @@ -601,10 +698,10 @@ void main() { ); expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584)); - expect(tester.getSize(find.byType(BottomAppBar)), theme.useMaterial3 ? const Size(800, 80) : const Size(800, 50)); + expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 80)); }); - testWidgets('notch with margin and top padding, home safe area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('notch with margin and top padding, home safe area', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/90024 await tester.pumpWidget( const MediaQuery( @@ -665,7 +762,7 @@ void main() { ); }); - testWidgets('BottomAppBar does not apply custom clipper without FAB', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomAppBar does not apply custom clipper without FAB', (WidgetTester tester) async { Widget buildWidget({Widget? fab}) { return MaterialApp( home: Scaffold( @@ -690,7 +787,7 @@ void main() { expect(physicalShape.clipper.toString(), 'ShapeBorderClipper'); }); - testWidgets('BottomAppBar adds bottom padding to height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomAppBar adds bottom padding to height', (WidgetTester tester) async { const double bottomPadding = 35.0; await tester.pumpWidget( diff --git a/packages/flutter/test/material/bottom_app_bar_theme_test.dart b/packages/flutter/test/material/bottom_app_bar_theme_test.dart index bd08943f4250d..e3358d1af875f 100644 --- a/packages/flutter/test/material/bottom_app_bar_theme_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_theme_test.dart @@ -9,6 +9,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('BottomAppBarTheme lerp special cases', () { @@ -18,17 +19,17 @@ void main() { }); group('Material 2 tests', () { - testWidgets('BAB theme overrides color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB theme overrides color', (WidgetTester tester) async { const Color themedColor = Colors.black87; const BottomAppBarTheme theme = BottomAppBarTheme(color: themedColor); - await tester.pumpWidget(_withTheme(theme)); + await tester.pumpWidget(_withTheme(theme, useMaterial3: false)); final PhysicalShape widget = _getBabRenderObject(tester); expect(widget.color, themedColor); }); - testWidgets('BAB color - Widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB color - Widget', (WidgetTester tester) async { const Color themeColor = Colors.white10; const Color babThemeColor = Colors.black87; const Color babColor = Colors.pink; @@ -47,7 +48,7 @@ void main() { expect(widget.color, babColor); }); - testWidgets('BAB color - BabTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB color - BabTheme', (WidgetTester tester) async { const Color themeColor = Colors.white10; const Color babThemeColor = Colors.black87; const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); @@ -65,7 +66,7 @@ void main() { expect(widget.color, babThemeColor); }); - testWidgets('BAB color - Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB color - Theme', (WidgetTester tester) async { const Color themeColor = Colors.white10; await tester.pumpWidget(MaterialApp( @@ -77,7 +78,7 @@ void main() { expect(widget.color, themeColor); }); - testWidgets('BAB color - Default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB color - Default', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold(body: BottomAppBar()), @@ -88,14 +89,14 @@ void main() { expect(widget.color, Colors.white); }); - testWidgets('BAB theme customizes shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB theme customizes shape', (WidgetTester tester) async { const BottomAppBarTheme theme = BottomAppBarTheme( color: Colors.white30, shape: CircularNotchedRectangle(), elevation: 1.0, ); - await tester.pumpWidget(_withTheme(theme)); + await tester.pumpWidget(_withTheme(theme, useMaterial3: false)); await expectLater( find.byKey(_painterKey), @@ -103,7 +104,7 @@ void main() { ); }); - testWidgets('BAB theme does not affect defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BAB theme does not affect defaults', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold(body: BottomAppBar()), @@ -117,19 +118,19 @@ void main() { }); group('Material 3 tests', () { - testWidgets('BAB theme overrides color - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB theme overrides color', (WidgetTester tester) async { const Color themedColor = Colors.black87; const BottomAppBarTheme theme = BottomAppBarTheme( color: themedColor, elevation: 0 ); - await tester.pumpWidget(_withTheme(theme, true)); + await tester.pumpWidget(_withTheme(theme, useMaterial3: true)); final PhysicalShape widget = _getBabRenderObject(tester); expect(widget.color, themedColor); }); - testWidgets('BAB color - Widget - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB color - Widget', (WidgetTester tester) async { const Color themeColor = Colors.white10; const Color babThemeColor = Colors.black87; const Color babColor = Colors.pink; @@ -148,7 +149,7 @@ void main() { expect(widget.color, babColor); }); - testWidgets('BAB color - BabTheme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB color - BabTheme', (WidgetTester tester) async { const Color themeColor = Colors.white10; const Color babThemeColor = Colors.black87; const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); @@ -166,7 +167,7 @@ void main() { expect(widget.color, babThemeColor); }); - testWidgets('BAB theme does not affect defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB theme does not affect defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, @@ -179,30 +180,30 @@ void main() { expect(widget.elevation, equals(3.0)); }); - testWidgets('BAB theme overrides surfaceTintColor - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB theme overrides surfaceTintColor', (WidgetTester tester) async { const Color color = Colors.blue; // base color that the surface tint will be applied to const Color babThemeSurfaceTintColor = Colors.black87; const BottomAppBarTheme theme = BottomAppBarTheme( color: color, surfaceTintColor: babThemeSurfaceTintColor, elevation: 0, ); - await tester.pumpWidget(_withTheme(theme, true)); + await tester.pumpWidget(_withTheme(theme, useMaterial3: true)); final PhysicalShape widget = _getBabRenderObject(tester); expect(widget.color, ElevationOverlay.applySurfaceTint(color, babThemeSurfaceTintColor, 0)); }); - testWidgets('BAB theme overrides shadowColor - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB theme overrides shadowColor', (WidgetTester tester) async { const Color babThemeShadowColor = Colors.yellow; const BottomAppBarTheme theme = BottomAppBarTheme( shadowColor: babThemeShadowColor, elevation: 0 ); - await tester.pumpWidget(_withTheme(theme, true)); + await tester.pumpWidget(_withTheme(theme, useMaterial3: true)); final PhysicalShape widget = _getBabRenderObject(tester); expect(widget.shadowColor, babThemeShadowColor); }); - testWidgets('BAB surfaceTintColor - Widget - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB surfaceTintColor - Widget', (WidgetTester tester) async { const Color color = Colors.white10; // base color that the surface tint will be applied to const Color themeSurfaceTintColor = Colors.white10; const Color babThemeSurfaceTintColor = Colors.black87; @@ -225,7 +226,7 @@ void main() { expect(widget.color, ElevationOverlay.applySurfaceTint(color, babSurfaceTintColor, 3.0)); }); - testWidgets('BAB surfaceTintColor - BabTheme - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BAB surfaceTintColor - BabTheme', (WidgetTester tester) async { const Color color = Colors.blue; // base color that the surface tint will be applied to const Color themeColor = Colors.white10; const Color babThemeColor = Colors.black87; @@ -259,7 +260,7 @@ PhysicalShape _getBabRenderObject(WidgetTester tester) { final Key _painterKey = UniqueKey(); -Widget _withTheme(BottomAppBarTheme theme, [bool useMaterial3 = false]) { +Widget _withTheme(BottomAppBarTheme theme, {required bool useMaterial3}) { return MaterialApp( theme: ThemeData(useMaterial3: useMaterial3, bottomAppBarTheme: theme), home: Scaffold( diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart index a73239ad9c4c1..88f772474a7e4 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart @@ -12,14 +12,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:vector_math/vector_math_64.dart' show Vector3; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; void main() { - testWidgets('BottomNavigationBar callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar callback test', (WidgetTester tester) async { late int mutatedIndex; await tester.pumpWidget( @@ -49,7 +49,7 @@ void main() { expect(mutatedIndex, 1); }); - testWidgets('BottomNavigationBar content test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar content test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -76,7 +76,7 @@ void main() { expect(find.text('Alarm'), findsOneWidget); }); - testWidgets('Fixed BottomNavigationBar defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed BottomNavigationBar defaults', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000001); const Color unselectedWidgetColor = Color(0xFF000002); @@ -141,7 +141,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(8.0)); }); - testWidgets('Custom selected and unselected font styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom selected and unselected font styles', (WidgetTester tester) async { const TextStyle selectedTextStyle = TextStyle(fontWeight: FontWeight.w200, fontSize: 18.0); const TextStyle unselectedTextStyle = TextStyle(fontWeight: FontWeight.w600, fontSize: 12.0); @@ -178,7 +178,7 @@ void main() { expect(unselectedFontStyle.fontWeight, equals(unselectedTextStyle.fontWeight)); }); - testWidgets('font size on text styles overrides font size params', (WidgetTester tester) async { + testWidgetsWithLeakTracking('font size on text styles overrides font size params', (WidgetTester tester) async { const TextStyle selectedTextStyle = TextStyle(fontSize: 18.0); const TextStyle unselectedTextStyle = TextStyle(fontSize: 12.0); const double selectedFontSize = 17.0; @@ -216,7 +216,7 @@ void main() { ); }); - testWidgets('Custom selected and unselected icon themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom selected and unselected icon themes', (WidgetTester tester) async { const IconThemeData selectedIconTheme = IconThemeData(size: 36, color: Color(0x00000001)); const IconThemeData unselectedIconTheme = IconThemeData(size: 18, color: Color(0x00000002)); @@ -250,7 +250,7 @@ void main() { expect(unselectedIcon.fontSize, equals(unselectedIconTheme.size)); }); - testWidgets('color on icon theme overrides selected and unselected item colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('color on icon theme overrides selected and unselected item colors', (WidgetTester tester) async { const IconThemeData selectedIconTheme = IconThemeData(size: 36, color: Color(0x00000001)); const IconThemeData unselectedIconTheme = IconThemeData(size: 18, color: Color(0x00000002)); const Color selectedItemColor = Color(0x00000003); @@ -290,7 +290,7 @@ void main() { expect(unselectedFontStyle.color, equals(unselectedItemColor)); }); - testWidgets('Padding is calculated properly on items - all labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding is calculated properly on items - all labels', (WidgetTester tester) async { const double selectedFontSize = 16.0; const double selectedIconSize = 36.0; const double unselectedIconSize = 20.0; @@ -331,7 +331,7 @@ void main() { expect(unselectedItemPadding.bottom, equals(expectedUnselectedPadding)); }); - testWidgets('Padding is calculated properly on items - selected labels only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding is calculated properly on items - selected labels only', (WidgetTester tester) async { const double selectedFontSize = 16.0; const double selectedIconSize = 36.0; const double unselectedIconSize = 20.0; @@ -371,7 +371,7 @@ void main() { expect(unselectedItemPadding.bottom, equals((selectedIconSize - unselectedIconSize) / 2.0)); }); - testWidgets('Padding is calculated properly on items - no labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding is calculated properly on items - no labels', (WidgetTester tester) async { const double selectedFontSize = 16.0; const double selectedIconSize = 36.0; const double unselectedIconSize = 20.0; @@ -411,7 +411,7 @@ void main() { expect(unselectedItemPadding.bottom, equals((selectedIconSize - unselectedIconSize) / 2.0)); }); - testWidgets('Shifting BottomNavigationBar defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shifting BottomNavigationBar defaults', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -440,7 +440,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(8.0)); }); - testWidgets('Fixed BottomNavigationBar custom font size, color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed BottomNavigationBar custom font size, color', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -505,7 +505,7 @@ void main() { }); - testWidgets('Shifting BottomNavigationBar custom font size, color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shifting BottomNavigationBar custom font size, color', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -552,7 +552,7 @@ void main() { expect(unselectedIcon.color, equals(unselectedColor)); }); - testWidgets('label style color should override itemColor only for the label for BottomNavigationBarType.fixed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label style color should override itemColor only for the label for BottomNavigationBarType.fixed', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -598,7 +598,7 @@ void main() { expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style!.color, equals(unselectedLabelColor)); }); - testWidgets('label style color should override itemColor only for the label for BottomNavigationBarType.shifting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label style color should override itemColor only for the label for BottomNavigationBarType.shifting', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -644,7 +644,7 @@ void main() { expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style!.color, equals(unselectedLabelColor)); }); - testWidgets('iconTheme color should override itemColor for BottomNavigationBarType.fixed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iconTheme color should override itemColor for BottomNavigationBarType.fixed', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -696,7 +696,7 @@ void main() { expect(unselectedIcon.color, equals(unselectedIconThemeColor)); }); - testWidgets('iconTheme color should override itemColor for BottomNavigationBarType.shifted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iconTheme color should override itemColor for BottomNavigationBarType.shifted', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedLabelColor = Color(0xFFFF9900); @@ -744,7 +744,7 @@ void main() { expect(unselectedIcon.color, equals(unselectedIconThemeColor)); }); - testWidgets('iconTheme color should override itemColor color for BottomNavigationBarType.fixed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iconTheme color should override itemColor color for BottomNavigationBarType.fixed', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedIconThemeColor = Color(0xFF1E7723); @@ -786,7 +786,7 @@ void main() { expect(unselectedIcon.color, equals(unselectedIconThemeColor)); }); - testWidgets('iconTheme color should override itemColor for BottomNavigationBarType.shifted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iconTheme color should override itemColor for BottomNavigationBarType.shifted', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); const Color selectedColor = Color(0xFF0004FF); @@ -832,7 +832,7 @@ void main() { expect(unselectedIcon.color, equals(unselectedIconThemeColor)); }); - testWidgets('Fixed BottomNavigationBar can hide unselected labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed BottomNavigationBar can hide unselected labels', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -858,7 +858,7 @@ void main() { expect(_getOpacity(tester, 'Alarm'), equals(0.0)); }); - testWidgets('Fixed BottomNavigationBar can update background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed BottomNavigationBar can update background color', (WidgetTester tester) async { const Color color = Colors.yellow; await tester.pumpWidget( @@ -885,7 +885,7 @@ void main() { expect(_getMaterial(tester).color, equals(color)); }); - testWidgets('Shifting BottomNavigationBar background color is overridden by item color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shifting BottomNavigationBar background color is overridden by item color', (WidgetTester tester) async { const Color itemColor = Colors.yellow; const Color backgroundColor = Colors.blue; @@ -914,7 +914,7 @@ void main() { expect(_getMaterial(tester).color, equals(itemColor)); }); - testWidgets('Specifying both selectedItemColor and fixedColor asserts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Specifying both selectedItemColor and fixedColor asserts', (WidgetTester tester) async { expect( () { return BottomNavigationBar( @@ -936,7 +936,7 @@ void main() { ); }); - testWidgets('Fixed BottomNavigationBar uses fixedColor when selectedItemColor not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed BottomNavigationBar uses fixedColor when selectedItemColor not provided', (WidgetTester tester) async { const Color fixedColor = Colors.black; await tester.pumpWidget( @@ -963,7 +963,7 @@ void main() { expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style!.color, equals(fixedColor)); }); - testWidgets('setting selectedFontSize to zero hides all labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setting selectedFontSize to zero hides all labels', (WidgetTester tester) async { const double customElevation = 3.0; await tester.pumpWidget( @@ -990,7 +990,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(customElevation)); }); - testWidgets('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1018,7 +1018,7 @@ void main() { expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); }); - testWidgets('BottomNavigationBar adds bottom padding to height with a custom font size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar adds bottom padding to height with a custom font size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: MediaQuery( @@ -1046,7 +1046,7 @@ void main() { expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); }); - testWidgets('BottomNavigationBar height will not change when toggle keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar height will not change when toggle keyboard', (WidgetTester tester) async { final Widget child = Scaffold( bottomNavigationBar: BottomNavigationBar( @@ -1098,7 +1098,7 @@ void main() { expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); }); - testWidgets('BottomNavigationBar action size test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar action size test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1153,7 +1153,7 @@ void main() { expect(actions.elementAt(1).size.width, 480.0); }); - testWidgets('BottomNavigationBar multiple taps test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar multiple taps test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1211,7 +1211,7 @@ void main() { expect(actions.elementAt(3).localToGlobal(Offset.zero), equals(originalOrigin)); }); - testWidgets('BottomNavigationBar inherits shadowed app theme for shifting navbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar inherits shadowed app theme for shifting navbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), @@ -1249,7 +1249,7 @@ void main() { expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark)); }); - testWidgets('BottomNavigationBar inherits shadowed app theme for fixed navbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar inherits shadowed app theme for fixed navbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), @@ -1287,7 +1287,7 @@ void main() { expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark)); }); - testWidgets('BottomNavigationBar iconSize test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar iconSize test', (WidgetTester tester) async { late double builderIconSize; await tester.pumpWidget( MaterialApp( @@ -1323,7 +1323,7 @@ void main() { expect(builderIconSize, 12.0); }); - testWidgets('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1397,7 +1397,7 @@ void main() { expect(box.size.height, equals(56.0)); }); - testWidgets('BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1471,7 +1471,7 @@ void main() { expect(box.size.height, equals(kBottomNavigationBarHeight)); }); - testWidgets('BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async { const String label = 'Foo'; Widget buildApp({ required double textScaleFactor }) { @@ -1529,7 +1529,7 @@ void main() { expect(tester.getSize(find.text(label).last), equals(const Size(168.0, 56.0))); }); - testWidgets('Different behaviour of tool tip in BottomNavigationBarItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Different behaviour of tool tip in BottomNavigationBarItem', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1568,7 +1568,7 @@ void main() { expect(find.byTooltip('C'), findsNothing); }); - testWidgets('BottomNavigationBar limits width of tiles with long labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar limits width of tiles with long labels', (WidgetTester tester) async { final String longTextA = List<String>.generate(100, (int index) => 'A').toString(); final String longTextB = List<String>.generate(100, (int index) => 'B').toString(); @@ -1601,7 +1601,7 @@ void main() { expect(itemBoxB.size, equals(const Size(400.0, 14.0))); }); - testWidgets('BottomNavigationBar paints circles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar paints circles', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( useMaterial3: false, @@ -1672,7 +1672,7 @@ void main() { ); }); - testWidgets('BottomNavigationBar inactiveIcon shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar inactiveIcon shown', (WidgetTester tester) async { const Key filled = Key('filled'); const Key stroked = Key('stroked'); int selectedItem = 0; @@ -1725,7 +1725,7 @@ void main() { expect(find.byKey(stroked), findsOneWidget); }); - testWidgets('BottomNavigationBar.fixed semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.fixed semantics', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, @@ -1778,7 +1778,7 @@ void main() { ); }); - testWidgets('BottomNavigationBar.shifting semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.shifting semantics', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, @@ -1832,7 +1832,7 @@ void main() { ); }); - testWidgets('BottomNavigationBar handles items.length changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar handles items.length changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/10322 Widget buildFrame(int itemCount) { @@ -1870,7 +1870,7 @@ void main() { expect(find.text('item 3'), findsNothing); }); - testWidgets('BottomNavigationBar change backgroundColor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar change backgroundColor test', (WidgetTester tester) async { // Regression test for: https://github.com/flutter/flutter/issues/19653 Color backgroundColor = Colors.red; @@ -1967,7 +1967,7 @@ void main() { ); } for (int pump = 1; pump < 9; pump++) { - testWidgets('pump $pump', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pump $pump', (WidgetTester tester) async { await tester.pumpWidget(runTest()); await tester.tap(find.text('Green')); @@ -1982,7 +1982,7 @@ void main() { } }); - testWidgets('BottomNavigationBar item label should not be nullable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar item label should not be nullable', (WidgetTester tester) async { expect(() { MaterialApp( home: Scaffold( @@ -2003,7 +2003,7 @@ void main() { }, throwsAssertionError); }); - testWidgets( + testWidgetsWithLeakTracking( 'BottomNavigationBar [showSelectedLabels]=false and [showUnselectedLabels]=false ' 'for shifting navbar, expect that there is no rendered text', (WidgetTester tester) async { @@ -2040,7 +2040,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'BottomNavigationBar [showSelectedLabels]=false and [showUnselectedLabels]=false ' 'for fixed navbar, expect that there is no rendered text', (WidgetTester tester) async { @@ -2078,7 +2078,7 @@ void main() { }, ); - testWidgets('BottomNavigationBar.fixed [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.fixed [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, @@ -2120,7 +2120,7 @@ void main() { ); }); - testWidgets('BottomNavigationBar.shifting [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.shifting [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, @@ -2163,7 +2163,7 @@ void main() { ); }); - testWidgets('BottomNavigationBar changes mouse cursor when the tile is hovered over', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar changes mouse cursor when the tile is hovered over', (WidgetTester tester) async { // Test BottomNavigationBar() constructor await tester.pumpWidget( MaterialApp( @@ -2239,7 +2239,7 @@ void main() { ); } - testWidgets('BottomNavigationBar with enabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar with enabled feedback', (WidgetTester tester) async { const bool enableFeedback = true; await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback)); @@ -2250,7 +2250,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('BottomNavigationBar with disabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar with disabled feedback', (WidgetTester tester) async { const bool enableFeedback = false; await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback)); @@ -2261,7 +2261,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('BottomNavigationBar with enabled feedback by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar with enabled feedback by default', (WidgetTester tester) async { await tester.pumpWidget(feedbackBoilerplate()); await tester.tap(find.byType(InkResponse).first); @@ -2270,7 +2270,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('BottomNavigationBar with disabled feedback using BottomNavigationBarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar with disabled feedback using BottomNavigationBarTheme', (WidgetTester tester) async { const bool enableFeedbackTheme = false; await tester.pumpWidget(feedbackBoilerplate(enableFeedbackTheme: enableFeedbackTheme)); @@ -2281,7 +2281,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('BottomNavigationBar.enableFeedback overrides BottomNavigationBarTheme.enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar.enableFeedback overrides BottomNavigationBarTheme.enableFeedback', (WidgetTester tester) async { const bool enableFeedbackTheme = false; const bool enableFeedback = true; @@ -2297,7 +2297,7 @@ void main() { }); }); - testWidgets('BottomNavigationBar excludes semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar excludes semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2368,7 +2368,7 @@ void main() { semantics.dispose(); }); - testWidgets('BottomNavigationBar default layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar default layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); @@ -2416,7 +2416,7 @@ void main() { expect(tester.getRect(find.byKey(icon1)), const Rect.fromLTRB(500.0, 560.0, 700.0, 570.0)); }); - testWidgets('BottomNavigationBar centered landscape layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar centered landscape layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); @@ -2462,7 +2462,7 @@ void main() { expect(tester.getRect(find.byKey(icon1)), const Rect.fromLTRB(450.0, 560.0, 650.0, 570.0)); }); - testWidgets('BottomNavigationBar linear landscape layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar linear landscape layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); diff --git a/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart b/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart index f68c66babefe3..70ae2b6833771 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:vector_math/vector_math_64.dart' show Vector3; @@ -52,7 +53,7 @@ void main() { expect(themeData.mouseCursor, null); }); - testWidgets('Default BottomNavigationBarThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default BottomNavigationBarThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BottomNavigationBarThemeData().debugFillProperties(builder); @@ -64,7 +65,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('BottomNavigationBarThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBarThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BottomNavigationBarThemeData( backgroundColor: Color(0xfffffff0), @@ -105,7 +106,7 @@ void main() { expect(description[11], 'mouseCursor: MaterialStateMouseCursor(clickable)'); }); - testWidgets('BottomNavigationBar is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar is themeable', (WidgetTester tester) async { const Color backgroundColor = Color(0xFF000001); const Color selectedItemColor = Color(0xFF000002); const Color unselectedItemColor = Color(0xFF000003); @@ -209,7 +210,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move); }); - testWidgets('BottomNavigationBar properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBar properties are taken over the theme values', (WidgetTester tester) async { const Color themeBackgroundColor = Color(0xFF000001); const Color themeSelectedItemColor = Color(0xFF000002); const Color themeUnselectedItemColor = Color(0xFF000003); @@ -336,7 +337,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('BottomNavigationBarTheme can be used to hide all labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBarTheme can be used to hide all labels', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66738. await tester.pumpWidget( MaterialApp( @@ -374,7 +375,7 @@ void main() { expect(tester.widget<Visibility>(findVisibility.at(1)).visible, false); }); - testWidgets('BottomNavigationBarTheme can be used to hide selected labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBarTheme can be used to hide selected labels', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66738. await tester.pumpWidget( MaterialApp( @@ -412,7 +413,7 @@ void main() { expect(tester.widget<FadeTransition>(findFadeTransition.at(1)).opacity.value, 1.0); }); - testWidgets('BottomNavigationBarTheme can be used to hide unselected labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomNavigationBarTheme can be used to hide unselected labels', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index b6e5db84011c8..1fe54d95d8faa 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; @@ -26,7 +27,7 @@ void main() { expect(dyDelta1, isNot(moreOrLessEquals(dyDelta2, epsilon: 0.1))); } - testWidgets('Throw if enable drag without an animation controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throw if enable drag without an animation controller', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/89168 await tester.pumpWidget( MaterialApp( @@ -53,7 +54,7 @@ void main() { FlutterError.onError = handler; }); - testWidgets('Disposing app while bottom sheet is disappearing does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing app while bottom sheet is disappearing does not crash', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget( @@ -90,7 +91,7 @@ void main() { await tester.pumpWidget(Container()); }); - testWidgets('Swiping down a BottomSheet should dismiss it by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a BottomSheet should dismiss it by default', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); bool showBottomSheetThenCalled = false; @@ -125,7 +126,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Swiping down a BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); bool showBottomSheetThenCalled = false; @@ -162,7 +163,7 @@ void main() { expect(find.text('BottomSheet'), findsOneWidget); }); - testWidgets('Swiping down a BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); bool showBottomSheetThenCalled = false; @@ -199,7 +200,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Tapping on a BottomSheet should not trigger a rebuild when enableDrag is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping on a BottomSheet should not trigger a rebuild when enableDrag is true', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/126833. final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); int buildCount = 0; @@ -236,7 +237,7 @@ void main() { expect(find.text('BottomSheet'), findsOneWidget); }); - testWidgets('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -267,7 +268,7 @@ void main() { expect(numBuilderCalls, 1); }); - testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget( @@ -303,7 +304,7 @@ void main() { expect(showBottomSheetThenCalled, isFalse); }); - testWidgets('Tapping outside a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping outside a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -337,7 +338,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Tapping outside a modal BottomSheet should dismiss it when isDismissible=true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping outside a modal BottomSheet should dismiss it when isDismissible=true', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -371,7 +372,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Verify that the BottomSheet animates non-linearly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that the BottomSheet animates non-linearly', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -404,7 +405,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/121098 - testWidgets('Verify that accessibleNavigation has no impact on the BottomSheet animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that accessibleNavigation has no impact on the BottomSheet animation', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( builder: (BuildContext context, Widget? child) { return MediaQuery( @@ -429,7 +430,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget( @@ -466,7 +467,7 @@ void main() { expect(find.text('BottomSheet'), findsOneWidget); }); - testWidgets('Swiping down a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -501,7 +502,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Swiping down a modal BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a modal BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -537,7 +538,7 @@ void main() { expect(find.text('BottomSheet'), findsOneWidget); }); - testWidgets('Swiping down a modal BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swiping down a modal BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -572,7 +573,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget(MaterialApp( @@ -603,7 +604,7 @@ void main() { expect(numBuilderCalls, 1); }); - testWidgets('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); bool showBottomSheetThenCalled = false; @@ -660,7 +661,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('Verify that dragging past the bottom dismisses a persistent BottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that dragging past the bottom dismisses a persistent BottomSheet', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/5528 final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -690,7 +691,7 @@ void main() { expect(find.text('BottomSheet'), findsNothing); }); - testWidgets('modal BottomSheet has no top MediaQuery', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal BottomSheet has no top MediaQuery', (WidgetTester tester) async { late BuildContext outerContext; late BuildContext innerContext; @@ -741,7 +742,7 @@ void main() { ); }); - testWidgets('modal BottomSheet can insert a SafeArea', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal BottomSheet can insert a SafeArea', (WidgetTester tester) async { late BuildContext outerContext; late BuildContext innerContext; @@ -813,7 +814,7 @@ void main() { expect(MediaQuery.of(innerContext).padding, const EdgeInsets.fromLTRB(0, 0, 0, 50.0)); }); - testWidgets('modal BottomSheet has semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal BottomSheet has semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -870,7 +871,7 @@ void main() { semantics.dispose(); }); - testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that visual properties are passed through', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const Color color = Colors.pink; const double elevation = 9.0; @@ -910,7 +911,7 @@ void main() { expect(modalBarrier.color, barrierColor); }); - testWidgets('BottomSheet uses fallback values in material3', + testWidgetsWithLeakTracking('BottomSheet uses fallback values in material3', (WidgetTester tester) async { const Color surfaceColor = Colors.pink; const Color surfaceTintColor = Colors.blue; @@ -950,7 +951,7 @@ void main() { expect(tester.getSize(finder).width, 640); }); - testWidgets('BottomSheet has transparent shadow in material3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomSheet has transparent shadow in material3', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: true, @@ -974,7 +975,7 @@ void main() { expect(material.shadowColor, Colors.transparent); }); - testWidgets('modal BottomSheet with scrollController has semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal BottomSheet with scrollController has semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -1047,7 +1048,7 @@ void main() { semantics.dispose(); }); - testWidgets('modal BottomSheet with drag handle has semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal BottomSheet with drag handle has semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -1116,7 +1117,7 @@ void main() { semantics.dispose(); }); - testWidgets('Drag handle color can take MaterialStateProperty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag handle color can take MaterialStateProperty', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const Color defaultColor=Colors.blue; const Color hoveringColor=Colors.green; @@ -1179,7 +1180,7 @@ void main() { expect(boxDecoration.color, hoveringColor); }); - testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -1209,7 +1210,7 @@ void main() { expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 544.0); }); - testWidgets('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) { @@ -1238,7 +1239,7 @@ void main() { expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600.0); }); - testWidgets('Verify that route settings can be set in the showModalBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that route settings can be set in the showModalBottomSheet', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const RouteSettings routeSettings = RouteSettings(name: 'route_name', arguments: 'route_argument'); @@ -1266,7 +1267,7 @@ void main() { expect(retrievedRouteSettings, routeSettings); }); - testWidgets('Verify showModalBottomSheet use AnimationController if provided.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify showModalBottomSheet use AnimationController if provided.', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -1323,7 +1324,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/87592 - testWidgets('the framework do not dispose the transitionAnimationController provided by user.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the framework do not dispose the transitionAnimationController provided by user.', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); final AnimationController controller = AnimationController( vsync: const TestVSync(), @@ -1385,7 +1386,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Verify persistence BottomSheet use AnimationController if provided.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify persistence BottomSheet use AnimationController if provided.', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); const Key tapTargetToClose = Key('tap-target-to-close'); await tester.pumpWidget(MaterialApp( @@ -1447,7 +1448,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/87708 - testWidgets('Each of the internal animation controllers should be disposed by the framework.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Each of the internal animation controllers should be disposed by the framework.', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -1490,7 +1491,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/99627 - testWidgets('The old route entry should be removed when a new sheet popup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The old route entry should be removed when a new sheet popup', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); PersistentBottomSheetController<void>? sheetController; @@ -1534,7 +1535,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/87708 - testWidgets('The framework does not dispose of the transitionAnimationController provided by user.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The framework does not dispose of the transitionAnimationController provided by user.', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); const Key tapTargetToClose = Key('tap-target-to-close'); final AnimationController controller = AnimationController( @@ -1590,7 +1591,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Calling PersistentBottomSheetController.close does not crash when it is not the current bottom sheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Calling PersistentBottomSheetController.close does not crash when it is not the current bottom sheet', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/93717 PersistentBottomSheetController<void>? sheetController1; await tester.pumpWidget(MaterialApp( @@ -1642,7 +1643,7 @@ void main() { expect(find.text('BottomSheet 2'), findsOneWidget); }); - testWidgets('ModalBottomSheetRoute shows BottomSheet correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ModalBottomSheetRoute shows BottomSheet correctly', (WidgetTester tester) async { late BuildContext savedContext; await tester.pumpWidget( @@ -1672,7 +1673,7 @@ void main() { }); group('Modal BottomSheet avoids overlapping display features', () { - testWidgets('positioning using anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning using anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1710,7 +1711,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); }); - testWidgets('positioning using Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning using Directionality', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1750,7 +1751,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); }); - testWidgets('default positioning', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default positioning', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1789,7 +1790,7 @@ void main() { }); group('constraints', () { - testWidgets('default constraints are max width 640 in material 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default constraints are max width 640 in material 3', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true), @@ -1805,7 +1806,7 @@ void main() { expect(tester.getSize(find.byType(Placeholder)).width, 640); }); - testWidgets('No constraints by default for bottomSheet property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No constraints by default for bottomSheet property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -1820,7 +1821,7 @@ void main() { ); }); - testWidgets('No constraints by default for showBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No constraints by default for showBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -1848,7 +1849,7 @@ void main() { ); }); - testWidgets('No constraints by default for showModalBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No constraints by default for showModalBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -1877,7 +1878,7 @@ void main() { ); }); - testWidgets('Theme constraints used for bottomSheet property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme constraints used for bottomSheet property', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -1905,7 +1906,7 @@ void main() { ); }); - testWidgets('Theme constraints used for showBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme constraints used for showBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -1939,7 +1940,7 @@ void main() { ); }); - testWidgets('Theme constraints used for showModalBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme constraints used for showModalBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -1974,7 +1975,7 @@ void main() { ); }); - testWidgets('constraints param overrides theme for showBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constraints param overrides theme for showBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -2009,7 +2010,7 @@ void main() { ); }); - testWidgets('constraints param overrides theme for showModalBottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constraints param overrides theme for showModalBottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -2045,10 +2046,62 @@ void main() { ); }); + group('scrollControlDisabledMaxHeightRatio', () { + Future<void> test( + WidgetTester tester, + bool isScrollControlled, + double scrollControlDisabledMaxHeightRatio, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: ElevatedButton( + child: const Text('Press me'), + onPressed: () { + showModalBottomSheet<void>( + context: context, + isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, + builder: (BuildContext context) => const SizedBox.expand( + child: Text('BottomSheet'), + ), + ); + }, + ), + ); + }), + ), + ), + ); + await tester.tap(find.text('Press me')); + await tester.pumpAndSettle(); + expect( + tester.getRect(find.text('BottomSheet')), + Rect.fromLTRB( + 80, + 600 * (isScrollControlled ? 0 : (1 - scrollControlDisabledMaxHeightRatio)), + 720, + 600, + ), + ); + } + + testWidgetsWithLeakTracking('works at 9 / 16', (WidgetTester tester) { + return test(tester, false, 9.0 / 16.0); + }); + testWidgetsWithLeakTracking('works at 8 / 16', (WidgetTester tester) { + return test(tester, false, 8.0 / 16.0); + }); + testWidgetsWithLeakTracking('works at isScrollControlled', (WidgetTester tester) { + return test(tester, true, 8.0 / 16.0); + }); + }); }); group('showModalBottomSheet modalBarrierDismissLabel', () { - testWidgets('Verify that modalBarrierDismissLabel is used if provided', + testWidgetsWithLeakTracking('Verify that modalBarrierDismissLabel is used if provided', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const String customLabel = 'custom label'; @@ -2074,7 +2127,7 @@ void main() { expect(modalBarrier.semanticsLabel, customLabel); }); - testWidgets('Verify that modalBarrierDismissLabel from context is used if barrierLabel is not provided', + testWidgetsWithLeakTracking('Verify that modalBarrierDismissLabel from context is used if barrierLabel is not provided', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/material/bottom_sheet_theme_test.dart b/packages/flutter/test/material/bottom_sheet_theme_test.dart index 7a965b59711b4..6b9b526c732b4 100644 --- a/packages/flutter/test/material/bottom_sheet_theme_test.dart +++ b/packages/flutter/test/material/bottom_sheet_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('BottomSheetThemeData copyWith, ==, hashCode basics', () { @@ -36,7 +37,7 @@ void main() { expect(bottomSheetTheme.dragHandleSize, null); }); - testWidgets('Default BottomSheetThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default BottomSheetThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BottomSheetThemeData().debugFillProperties(builder); @@ -48,7 +49,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('BottomSheetThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomSheetThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const BottomSheetThemeData( backgroundColor: Color(0xFFFFFFFF), @@ -78,7 +79,7 @@ void main() { ]); }); - testWidgets('Passing no BottomSheetThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no BottomSheetThemeData returns defaults', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -103,7 +104,7 @@ void main() { expect(material.clipBehavior, Clip.none); }); - testWidgets('BottomSheet uses values from BottomSheetThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomSheet uses values from BottomSheetThemeData', (WidgetTester tester) async { final BottomSheetThemeData bottomSheetTheme = _bottomSheetTheme(); await tester.pumpWidget(MaterialApp( @@ -130,7 +131,7 @@ void main() { expect(material.clipBehavior, bottomSheetTheme.clipBehavior); }); - testWidgets('BottomSheet widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomSheet widget properties take priority over theme', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; const Color shadowColor = Colors.blue; const double elevation = 7.0; @@ -169,7 +170,7 @@ void main() { expect(material.clipBehavior, clipBehavior); }); - testWidgets('Modal bottom sheet-specific parameters are used for modal bottom sheets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Modal bottom sheet-specific parameters are used for modal bottom sheets', (WidgetTester tester) async { const double modalElevation = 5.0; const double persistentElevation = 7.0; const Color modalBackgroundColor = Colors.yellow; @@ -200,7 +201,7 @@ void main() { expect(modalBarrier.color, modalBarrierColor); }); - testWidgets('General bottom sheet parameters take priority over modal bottom sheet-specific parameters for persistent bottom sheets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('General bottom sheet parameters take priority over modal bottom sheet-specific parameters for persistent bottom sheets', (WidgetTester tester) async { const double modalElevation = 5.0; const double persistentElevation = 7.0; const Color modalBackgroundColor = Colors.yellow; @@ -226,7 +227,7 @@ void main() { expect(material.color, persistentBackgroundColor); }); - testWidgets("Modal bottom sheet-specific parameters don't apply to persistent bottom sheets", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Modal bottom sheet-specific parameters don't apply to persistent bottom sheets", (WidgetTester tester) async { const double modalElevation = 5.0; const Color modalBackgroundColor = Colors.yellow; const BottomSheetThemeData bottomSheetTheme = BottomSheetThemeData( @@ -248,7 +249,7 @@ void main() { expect(material.color, null); }); - testWidgets('Modal bottom sheets respond to theme changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Modal bottom sheets respond to theme changes', (WidgetTester tester) async { const double lightElevation = 5.0; const double darkElevation = 3.0; const Color lightBackgroundColor = Colors.green; diff --git a/packages/flutter/test/material/button_bar_test.dart b/packages/flutter/test/material/button_bar_test.dart index 9207fc13953e2..a04d9115de168 100644 --- a/packages/flutter/test/material/button_bar_test.dart +++ b/packages/flutter/test/material/button_bar_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ButtonBar default control smoketest', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar default control smoketest', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -17,7 +18,7 @@ void main() { group('alignment', () { - testWidgets('default alignment is MainAxisAlignment.end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default alignment is MainAxisAlignment.end', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: ButtonBar( @@ -34,7 +35,7 @@ void main() { expect(tester.getRect(child).right, 792.0); // bar width - default padding }); - testWidgets('ButtonBarTheme.alignment overrides default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarTheme.alignment overrides default', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: ButtonBarTheme( @@ -56,7 +57,7 @@ void main() { expect(tester.getRect(child).right, 405.0); // (bar width - padding) / 2 - 10 / 2 + 10 }); - testWidgets('ButtonBar.alignment overrides ButtonBarTheme.alignment and default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar.alignment overrides ButtonBarTheme.alignment and default', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: ButtonBarTheme( @@ -83,7 +84,7 @@ void main() { group('mainAxisSize', () { - testWidgets('Default mainAxisSize is MainAxisSize.max', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default mainAxisSize is MainAxisSize.max', (WidgetTester tester) async { const Key buttonBarKey = Key('row'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -130,7 +131,7 @@ void main() { expect(childRect.right, 800.0); }); - testWidgets('ButtonBarTheme.mainAxisSize overrides default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarTheme.mainAxisSize overrides default', (WidgetTester tester) async { const Key buttonBarKey = Key('row'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -185,7 +186,7 @@ void main() { expect(childRect.left, ((800.0 - buttonBarRect.width) / 2.0) + 200.0); }); - testWidgets('ButtonBar.mainAxisSize overrides ButtonBarTheme.mainAxisSize and default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar.mainAxisSize overrides ButtonBarTheme.mainAxisSize and default', (WidgetTester tester) async { const Key buttonBarKey = Key('row'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -240,7 +241,7 @@ void main() { group('button properties override ButtonTheme', () { - testWidgets('default button properties override ButtonTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default button properties override ButtonTheme properties', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( @@ -263,7 +264,7 @@ void main() { expect(buttonTheme.layoutBehavior, equals(ButtonBarLayoutBehavior.padded)); }); - testWidgets('ButtonBarTheme button properties override defaults and ButtonTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarTheme button properties override defaults and ButtonTheme properties', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( @@ -296,7 +297,7 @@ void main() { expect(buttonTheme.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained)); }); - testWidgets('ButtonBar button properties override ButtonBarTheme, defaults and ButtonTheme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar button properties override ButtonBarTheme, defaults and ButtonTheme properties', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( @@ -339,7 +340,7 @@ void main() { group('layoutBehavior', () { - testWidgets('ButtonBar has a min height of 52 when using ButtonBarLayoutBehavior.constrained', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar has a min height of 52 when using ButtonBarLayoutBehavior.constrained', (WidgetTester tester) async { await tester.pumpWidget( const SingleChildScrollView( child: ListBody( @@ -362,7 +363,7 @@ void main() { expect(tester.getBottomRight(buttonBar).dy - tester.getTopRight(buttonBar).dy, 52.0); }); - testWidgets('ButtonBar has padding applied when using ButtonBarLayoutBehavior.padded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBar has padding applied when using ButtonBarLayoutBehavior.padded', (WidgetTester tester) async { await tester.pumpWidget( const SingleChildScrollView( child: ListBody( @@ -387,7 +388,7 @@ void main() { }); group("ButtonBar's children wrap when they overflow horizontally", () { - testWidgets("ButtonBar's children wrap when buttons overflow", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ButtonBar's children wrap when buttons overflow", (WidgetTester tester) async { final Key keyOne = UniqueKey(); final Key keyTwo = UniqueKey(); await tester.pumpWidget( @@ -409,7 +410,7 @@ void main() { expect(containerOneRect.left, containerTwoRect.left); }); - testWidgets( + testWidgetsWithLeakTracking( "ButtonBar's children overflow defaults - MainAxisAlignment.end", (WidgetTester tester) async { final Key keyOne = UniqueKey(); final Key keyTwo = UniqueKey(); @@ -437,7 +438,7 @@ void main() { }, ); - testWidgets("ButtonBar's children overflow - MainAxisAlignment.start", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ButtonBar's children overflow - MainAxisAlignment.start", (WidgetTester tester) async { final Key keyOne = UniqueKey(); final Key keyTwo = UniqueKey(); await tester.pumpWidget( @@ -464,7 +465,7 @@ void main() { expect(containerOneRect.left, buttonBarRect.left); }); - testWidgets("ButtonBar's children overflow - MainAxisAlignment.center", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ButtonBar's children overflow - MainAxisAlignment.center", (WidgetTester tester) async { final Key keyOne = UniqueKey(); final Key keyTwo = UniqueKey(); await tester.pumpWidget( @@ -491,7 +492,7 @@ void main() { expect(containerOneRect.center.dx, buttonBarRect.center.dx); }); - testWidgets( + testWidgetsWithLeakTracking( "ButtonBar's children default to MainAxisAlignment.start for horizontal " 'alignment when overflowing in spaceBetween, spaceAround and spaceEvenly ' 'cases when overflowing.', (WidgetTester tester) async { @@ -545,7 +546,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( "ButtonBar's children respects verticalDirection when overflowing", (WidgetTester tester) async { final Key keyOne = UniqueKey(); @@ -574,7 +575,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'ButtonBar has no spacing by default when overflowing', (WidgetTester tester) async { final Key keyOne = UniqueKey(); @@ -599,7 +600,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( "ButtonBar's children respects overflowButtonSpacing when overflowing", (WidgetTester tester) async { final Key keyOne = UniqueKey(); @@ -628,7 +629,7 @@ void main() { ); }); - testWidgets('_RenderButtonBarRow.constraints does not work before layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_RenderButtonBarRow.constraints does not work before layout', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: ButtonBar()), Duration.zero, diff --git a/packages/flutter/test/material/button_bar_theme_test.dart b/packages/flutter/test/material/button_bar_theme_test.dart index ab8037b1b5618..158c9dbb5176f 100644 --- a/packages/flutter/test/material/button_bar_theme_test.dart +++ b/packages/flutter/test/material/button_bar_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { @@ -36,7 +37,7 @@ void main() { expect(const ButtonBarThemeData().hashCode, const ButtonBarThemeData().copyWith().hashCode); }); - testWidgets('ButtonBarThemeData lerps correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarThemeData lerps correctly', (WidgetTester tester) async { const ButtonBarThemeData barThemePrimary = ButtonBarThemeData( alignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -72,7 +73,7 @@ void main() { expect(lerp.overflowDirection, equals(VerticalDirection.up)); }); - testWidgets('Default ButtonBarThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ButtonBarThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ButtonBarThemeData().debugFillProperties(builder); @@ -84,7 +85,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ButtonBarThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ButtonBarThemeData( alignment: MainAxisAlignment.center, @@ -116,7 +117,7 @@ void main() { ]); }); - testWidgets('ButtonBarTheme.of falls back to ThemeData.buttonBarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarTheme.of falls back to ThemeData.buttonBarTheme', (WidgetTester tester) async { const ButtonBarThemeData buttonBarTheme = ButtonBarThemeData(buttonMinWidth: 42.0); late BuildContext capturedContext; await tester.pumpWidget( @@ -134,7 +135,7 @@ void main() { expect(ButtonBarTheme.of(capturedContext).buttonMinWidth, equals(42.0)); }); - testWidgets('ButtonBarTheme overrides ThemeData.buttonBarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonBarTheme overrides ThemeData.buttonBarTheme', (WidgetTester tester) async { const ButtonBarThemeData defaultBarTheme = ButtonBarThemeData(buttonMinWidth: 42.0); const ButtonBarThemeData buttonBarTheme = ButtonBarThemeData(buttonMinWidth: 84.0); late BuildContext capturedContext; diff --git a/packages/flutter/test/material/button_style_test.dart b/packages/flutter/test/material/button_style_test.dart index 2244c2940a72d..f6b096abd5dbe 100644 --- a/packages/flutter/test/material/button_style_test.dart +++ b/packages/flutter/test/material/button_style_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('ButtonStyle copyWith, merge, ==, hashCode basics', () { @@ -44,7 +45,7 @@ void main() { expect(style.enableFeedback, null); }); - testWidgets('Default ButtonStyle debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ButtonStyle debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ButtonStyle().debugFillProperties(builder); @@ -56,7 +57,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ButtonStyle debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonStyle debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ButtonStyle( textStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 10.0)), @@ -106,7 +107,7 @@ void main() { ]); }); - testWidgets('ButtonStyle copyWith, merge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonStyle copyWith, merge', (WidgetTester tester) async { const MaterialStateProperty<TextStyle> textStyle = MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 10)); const MaterialStateProperty<Color> backgroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff1)); const MaterialStateProperty<Color> foregroundColor = MaterialStatePropertyAll<Color>(Color(0xfffffff2)); diff --git a/packages/flutter/test/material/button_theme_test.dart b/packages/flutter/test/material/button_theme_test.dart index d33dbc71d562a..1b8f3689e2378 100644 --- a/packages/flutter/test/material/button_theme_test.dart +++ b/packages/flutter/test/material/button_theme_test.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + void main() { test('ButtonThemeData defaults', () { @@ -66,7 +68,7 @@ void main() { expect(theme.colorScheme, const ColorScheme.dark()); }); - testWidgets('ButtonTheme alignedDropdown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ButtonTheme alignedDropdown', (WidgetTester tester) async { final Key dropdownKey = UniqueKey(); Widget buildFrame({ required bool alignedDropdown, required TextDirection textDirection }) { diff --git a/packages/flutter/test/material/calendar_date_picker_test.dart b/packages/flutter/test/material/calendar_date_picker_test.dart index 038302dfd7afc..7caae93b9a770 100644 --- a/packages/flutter/test/material/calendar_date_picker_test.dart +++ b/packages/flutter/test/material/calendar_date_picker_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'feedback_tester.dart'; void main() { @@ -26,15 +26,16 @@ void main() { DatePickerMode initialCalendarMode = DatePickerMode.day, SelectableDayPredicate? selectableDayPredicate, TextDirection textDirection = TextDirection.ltr, + bool? useMaterial3, }) { return MaterialApp( - theme: ThemeData(useMaterial3: false), + theme: ThemeData(useMaterial3: useMaterial3), home: Material( child: Directionality( textDirection: textDirection, child: CalendarDatePicker( key: key, - initialDate: initialDate ?? DateTime(2016, DateTime.january, 15), + initialDate: initialDate, firstDate: firstDate ?? DateTime(2001), lastDate: lastDate ?? DateTime(2031, DateTime.december, 31), currentDate: currentDate ?? DateTime(2016, DateTime.january, 3), @@ -65,7 +66,6 @@ void main() { child: YearPicker( key: key, selectedDate: selectedDate ?? DateTime(2016, DateTime.january, 15), - initialDate: initialDate ?? DateTime(2016, DateTime.january, 15), firstDate: firstDate ?? DateTime(2001), lastDate: lastDate ?? DateTime(2031, DateTime.december, 31), currentDate: currentDate ?? DateTime(2016, DateTime.january, 3), @@ -77,18 +77,29 @@ void main() { } group('CalendarDatePicker', () { - testWidgets('Can select a day', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a day', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); await tester.tap(find.text('12')); expect(selectedDate, equals(DateTime(2016, DateTime.january, 12))); }); - testWidgets('Can select a month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a day with nothing first selected', (WidgetTester tester) async { + DateTime? selectedDate; + await tester.pumpWidget(calendarDatePicker( + onDateChanged: (DateTime date) => selectedDate = date, + )); + await tester.tap(find.text('12')); + expect(selectedDate, equals(DateTime(2016, DateTime.january, 12))); + }); + + testWidgetsWithLeakTracking('Can select a month', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); expect(find.text('January 2016'), findsOneWidget); @@ -110,11 +121,36 @@ void main() { expect(displayedMonth, equals(DateTime(2015, DateTime.december))); }); - testWidgets('Can select a year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a month with nothing first selected', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); + expect(find.text('January 2016'), findsOneWidget); + + // Go back two months + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('December 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.december))); + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('November 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.november))); + + // Go forward a month + await tester.tap(nextMonthIcon); + await tester.pumpAndSettle(); + expect(find.text('December 2015'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2015, DateTime.december))); + }); + + testWidgetsWithLeakTracking('Can select a year', (WidgetTester tester) async { + DateTime? displayedMonth; + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); await tester.tap(find.text('January 2016')); // Switch to year mode. await tester.pumpAndSettle(); @@ -124,7 +160,21 @@ void main() { expect(displayedMonth, equals(DateTime(2018))); }); - testWidgets('Selecting date does not change displayed month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a year with nothing first selected', (WidgetTester tester) async { + DateTime? displayedMonth; + await tester.pumpWidget(calendarDatePicker( + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); + + await tester.tap(find.text('January 2016')); // Switch to year mode. + await tester.pumpAndSettle(); + await tester.tap(find.text('2018')); + await tester.pumpAndSettle(); + expect(find.text('January 2018'), findsOneWidget); + expect(displayedMonth, equals(DateTime(2018))); + }); + + testWidgetsWithLeakTracking('Selecting date does not change displayed month', (WidgetTester tester) async { DateTime? selectedDate; DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( @@ -147,9 +197,10 @@ void main() { expect(find.text('31'), findsNothing); }); - testWidgets('Changing year does not change selected date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing year does change selected date', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); await tester.tap(find.text('4')); @@ -158,12 +209,32 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.text('2018')); await tester.pumpAndSettle(); - expect(selectedDate, equals(DateTime(2016, DateTime.january, 4))); + expect(selectedDate, equals(DateTime(2018, DateTime.january, 4))); }); - testWidgets('Changing year does not change the month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing year for february 29th', (WidgetTester tester) async { + DateTime? selectedDate; + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2020, DateTime.february, 29), + onDateChanged: (DateTime date) => selectedDate = date, + )); + await tester.tap(find.text('February 2020')); + await tester.pumpAndSettle(); + await tester.tap(find.text('2018')); + await tester.pumpAndSettle(); + expect(selectedDate, equals(DateTime(2018, DateTime.february, 28))); + await tester.tap(find.text('February 2018')); + await tester.pumpAndSettle(); + await tester.tap(find.text('2020')); + await tester.pumpAndSettle(); + // Changing back to 2020 the 29th is not selected anymore. + expect(selectedDate, equals(DateTime(2020, DateTime.february, 28))); + }); + + testWidgetsWithLeakTracking('Changing year does not change the month', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); await tester.tap(nextMonthIcon); @@ -178,9 +249,10 @@ void main() { expect(displayedMonth, equals(DateTime(2018, DateTime.march))); }); - testWidgets('Can select a year and then a day', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a year and then a day', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); await tester.tap(find.text('January 2016')); // Switch to year mode. @@ -191,7 +263,7 @@ void main() { expect(selectedDate, equals(DateTime(2017, DateTime.january, 19))); }); - testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select a day outside bounds', (WidgetTester tester) async { final DateTime validDate = DateTime(2017, DateTime.january, 15); DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( @@ -214,7 +286,7 @@ void main() { expect(selectedDate, validDate); }); - testWidgets('Cannot navigate to a month outside bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot navigate to a month outside bounds', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( firstDate: DateTime(2016, DateTime.december, 15), @@ -238,7 +310,7 @@ void main() { expect(previousMonthIcon, findsNothing); }); - testWidgets('Cannot select disabled year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select disabled year', (WidgetTester tester) async { DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( firstDate: DateTime(2018, DateTime.june, 9), @@ -259,12 +331,14 @@ void main() { expect(displayedMonth, isNull); }); - testWidgets('Selecting firstDate year respects firstDate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selecting firstDate year respects firstDate', (WidgetTester tester) async { + DateTime? selectedDate; DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( firstDate: DateTime(2016, DateTime.june, 9), initialDate: DateTime(2018, DateTime.may, 4), lastDate: DateTime(2019, DateTime.january, 15), + onDateChanged: (DateTime date) => selectedDate = date, onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); await tester.tap(find.text('May 2018')); @@ -274,26 +348,55 @@ void main() { // Month should be clamped to June as the range starts at June 2016. expect(find.text('June 2016'), findsOneWidget); expect(displayedMonth, DateTime(2016, DateTime.june)); + expect(selectedDate, DateTime(2016, DateTime.june, 9)); }); - testWidgets('Selecting lastDate year respects lastDate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selecting lastDate year respects lastDate', (WidgetTester tester) async { + DateTime? selectedDate; DateTime? displayedMonth; await tester.pumpWidget(calendarDatePicker( firstDate: DateTime(2016, DateTime.june, 9), initialDate: DateTime(2018, DateTime.may, 4), lastDate: DateTime(2019, DateTime.january, 15), + onDateChanged: (DateTime date) => selectedDate = date, + onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, + )); + // Selected date is now 2018-05-04 (initialDate). + await tester.tap(find.text('May 2018')); + // Selected date is still 2018-05-04. + await tester.pumpAndSettle(); + await tester.tap(find.text('2019')); + // Selected date would become 2019-05-04 but gets clamped to the month of lastDate, so 2019-01-04. + await tester.pumpAndSettle(); + expect(find.text('January 2019'), findsOneWidget); + expect(displayedMonth, DateTime(2019)); + expect(selectedDate, DateTime(2019, DateTime.january, 4)); + }); + + testWidgetsWithLeakTracking('Selecting lastDate year respects lastDate', (WidgetTester tester) async { + DateTime? selectedDate; + DateTime? displayedMonth; + await tester.pumpWidget(calendarDatePicker( + firstDate: DateTime(2016, DateTime.june, 9), + initialDate: DateTime(2018, DateTime.may, 15), + lastDate: DateTime(2019, DateTime.january, 4), + onDateChanged: (DateTime date) => selectedDate = date, onDisplayedMonthChanged: (DateTime date) => displayedMonth = date, )); + // Selected date is now 2018-05-15 (initialDate). await tester.tap(find.text('May 2018')); + // Selected date is still 2018-05-15. await tester.pumpAndSettle(); await tester.tap(find.text('2019')); + // Selected date would become 2019-05-15 but gets clamped to the month of lastDate, so 2019-01-15. + // Day is now beyond the lastDate so that also gets clamped, to 2019-01-04. await tester.pumpAndSettle(); - // Month should be clamped to January as the range ends at January 2019. expect(find.text('January 2019'), findsOneWidget); expect(displayedMonth, DateTime(2019)); + expect(selectedDate, DateTime(2019, DateTime.january, 4)); }); - testWidgets('Only predicate days are selectable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Only predicate days are selectable', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( firstDate: DateTime(2017, DateTime.january, 10), @@ -310,7 +413,7 @@ void main() { expect(selectedDate, DateTime(2017, DateTime.january, 10)); }); - testWidgets('Can select initial calendar picker mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select initial calendar picker mode', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( initialDate: DateTime(2014, DateTime.january, 15), initialCalendarMode: DatePickerMode.year, @@ -322,8 +425,10 @@ void main() { expect(find.text('January 2018'), findsOneWidget); }); - testWidgets('currentDate is highlighted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - currentDate is highlighted', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( + useMaterial3: false, + initialDate: DateTime(2016, DateTime.january, 15), currentDate: DateTime(2016, 1, 2), )); const Color todayColor = Color(0xff2196f3); // default primary color @@ -338,8 +443,27 @@ void main() { ); }); - testWidgets('currentDate is highlighted even if it is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - currentDate is highlighted', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + useMaterial3: true, + initialDate: DateTime(2016, DateTime.january, 15), + currentDate: DateTime(2016, 1, 2), + )); + const Color todayColor = Color(0xff6750a4); // default primary color + expect( + Material.of(tester.element(find.text('2'))), + // The current day should be painted with a circle outline. + paints..circle( + color: todayColor, + style: PaintingStyle.stroke, + strokeWidth: 1.0, + ), + ); + }); + + testWidgetsWithLeakTracking('Material2 - currentDate is highlighted even if it is disabled', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( + useMaterial3: false, firstDate: DateTime(2016, 1, 3), lastDate: DateTime(2016, 1, 31), currentDate: DateTime(2016, 1, 2), // not between first and last @@ -358,7 +482,28 @@ void main() { ); }); - testWidgets('Selecting date does not switch picker to year selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - currentDate is highlighted even if it is disabled', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + useMaterial3: true, + firstDate: DateTime(2016, 1, 3), + lastDate: DateTime(2016, 1, 31), + currentDate: DateTime(2016, 1, 2), // not between first and last + initialDate: DateTime(2016, 1, 5), + )); + const Color disabledColor = Color(0x616750a4); // default disabled color + expect( + Material.of(tester.element(find.text('2'))), + // The current day should be painted with a circle outline. + paints + ..circle( + color: disabledColor, + style: PaintingStyle.stroke, + strokeWidth: 1.0, + ), + ); + }); + + testWidgetsWithLeakTracking('Selecting date does not switch picker to year selection', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( initialDate: DateTime(2020, DateTime.may, 10), initialCalendarMode: DatePickerMode.year, @@ -372,57 +517,87 @@ void main() { expect(find.text('2017'), findsNothing); }); - testWidgets('Updates to initialDate parameter is reflected in the state', (WidgetTester tester) async { - final Key pickerKey = UniqueKey(); - final DateTime initialDate = DateTime(2020, 1, 21); - final DateTime updatedDate = DateTime(1976, 2, 23); - final DateTime firstDate = DateTime(1970); - final DateTime lastDate = DateTime(2099, 31, 12); - const Color selectedColor = Color(0xff2196f3); // default primary color + testWidgetsWithLeakTracking('Selecting disabled date does not change current selection', (WidgetTester tester) async { + DateTime day(int day) => DateTime(2020, DateTime.may, day); + DateTime selection = day(2); await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, + initialDate: selection, + firstDate: day(2), + lastDate: day(3), + onDateChanged: (DateTime date) { + selection = date; + }, )); + + await tester.tap(find.text('3')); + await tester.pumpAndSettle(); + expect(selection, day(3)); + await tester.tap(find.text('4')); + await tester.pumpAndSettle(); + expect(selection, day(3)); + await tester.tap(find.text('5')); await tester.pumpAndSettle(); + expect(selection, day(3)); + }); - // Month should show as January 2020 - expect(find.text('January 2020'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('21'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); + for (final bool useMaterial3 in <bool>[false, true]) { + testWidgetsWithLeakTracking('Updates to initialDate parameter are not reflected in the state (useMaterial3=$useMaterial3)', (WidgetTester tester) async { + final Key pickerKey = UniqueKey(); + final DateTime initialDate = DateTime(2020, 1, 21); + final DateTime updatedDate = DateTime(1976, 2, 23); + final DateTime firstDate = DateTime(1970); + final DateTime lastDate = DateTime(2099, 31, 12); + final Color selectedColor = useMaterial3 ? const Color(0xff6750a4) : const Color(0xff2196f3); // default primary color - // Change to the updated initialDate - await tester.pumpWidget(calendarDatePicker( - key: pickerKey, - initialDate: updatedDate, - firstDate: firstDate, - lastDate: lastDate, - onDateChanged: (DateTime value) {}, - )); - // Wait for the page scroll animation to finish. - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - // Month should show as February 1976 - expect(find.text('January 2020'), findsNothing); - expect(find.text('February 1976'), findsOneWidget); - // Selected date should be painted with a colored circle. - expect( - Material.of(tester.element(find.text('23'))), - paints..circle(color: selectedColor, style: PaintingStyle.fill), - ); - }); + await tester.pumpWidget(calendarDatePicker( + key: pickerKey, + useMaterial3: useMaterial3, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + onDateChanged: (DateTime value) {}, + )); + await tester.pumpAndSettle(); + + // Month should show as January 2020. + expect(find.text('January 2020'), findsOneWidget); + // Selected date should be painted with a colored circle. + expect( + Material.of(tester.element(find.text('21'))), + paints..circle(color: selectedColor, style: PaintingStyle.fill), + ); - testWidgets('Updates to initialCalendarMode parameter is reflected in the state', (WidgetTester tester) async { + // Change to the updated initialDate. + // This should have no effect, the initialDate is only the _initial_ date. + await tester.pumpWidget(calendarDatePicker( + key: pickerKey, + useMaterial3: useMaterial3, + initialDate: updatedDate, + firstDate: firstDate, + lastDate: lastDate, + onDateChanged: (DateTime value) {}, + )); + // Wait for the page scroll animation to finish. + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // Month should show as January 2020 still. + expect(find.text('January 2020'), findsOneWidget); + expect(find.text('February 1976'), findsNothing); + // Selected date should be painted with a colored circle. + expect( + Material.of(tester.element(find.text('21'))), + paints..circle(color: selectedColor, style: PaintingStyle.fill), + ); + }); + } + + testWidgetsWithLeakTracking('Updates to initialCalendarMode parameter is not reflected in the state', (WidgetTester tester) async { final Key pickerKey = UniqueKey(); await tester.pumpWidget(calendarDatePicker( key: pickerKey, + initialDate: DateTime(2016, DateTime.january, 15), initialCalendarMode: DatePickerMode.year, )); await tester.pumpAndSettle(); @@ -434,17 +609,20 @@ void main() { await tester.pumpWidget(calendarDatePicker( key: pickerKey, + initialDate: DateTime(2016, DateTime.january, 15), )); await tester.pumpAndSettle(); - // Should be in day mode. + // Should be in year mode still; updating an _initial_ parameter has no effect. expect(find.text('January 2016'), findsOneWidget); // Day/year selector - expect(find.text('15'), findsOneWidget); // day 15 in grid - expect(find.text('2016'), findsNothing); // 2016 in year grid + expect(find.text('15'), findsNothing); // day 15 in grid + expect(find.text('2016'), findsOneWidget); // 2016 in year grid }); - testWidgets('Dragging more than half the width should not cause a jump', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + testWidgetsWithLeakTracking('Dragging more than half the width should not cause a jump', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(PageView))); // This initial drag is required for the PageView to recognize the gesture, as it uses DragStartBehavior.start. @@ -463,8 +641,10 @@ void main() { }); group('Keyboard navigation', () { - testWidgets('Can toggle to year mode', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + testWidgetsWithLeakTracking('Can toggle to year mode', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); expect(find.text('2016'), findsNothing); expect(find.text('January 2016'), findsOneWidget); // Navigate to the year selector and activate it. @@ -476,8 +656,10 @@ void main() { expect(find.text('January 2016'), findsOneWidget); }); - testWidgets('Can navigate next/previous months', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + testWidgetsWithLeakTracking('Can navigate next/previous months', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); expect(find.text('January 2016'), findsOneWidget); // Navigate to the previous month button and activate it twice. await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -503,9 +685,10 @@ void main() { expect(find.text('March 2016'), findsOneWidget); }); - testWidgets('Can navigate date grid with arrow keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can navigate date grid with arrow keys', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); // Navigate to the grid. @@ -532,9 +715,10 @@ void main() { expect(selectedDate, DateTime(2016, DateTime.january, 18)); }); - testWidgets('Navigating with arrow keys scrolls months', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigating with arrow keys scrolls months', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, )); // Navigate to the grid. @@ -572,9 +756,10 @@ void main() { expect(selectedDate, DateTime(2015, DateTime.november, 26)); }); - testWidgets('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), onDateChanged: (DateTime date) => selectedDate = date, textDirection: TextDirection.rtl, )); @@ -615,8 +800,10 @@ void main() { feedback.dispose(); }); - testWidgets('Selecting date vibrates', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + testWidgetsWithLeakTracking('Selecting date vibrates', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.tap(find.text('10')); await tester.pump(hapticFeedbackInterval); expect(feedback.hapticCount, 1); @@ -628,7 +815,7 @@ void main() { expect(feedback.hapticCount, 3); }); - testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping unselectable date does not vibrate', (WidgetTester tester) async { await tester.pumpWidget(calendarDatePicker( initialDate: DateTime(2016, DateTime.january, 10), selectableDayPredicate: (DateTime date) => date.day.isEven, @@ -644,8 +831,10 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('Changing modes and year vibrates', (WidgetTester tester) async { - await tester.pumpWidget(calendarDatePicker()); + testWidgetsWithLeakTracking('Changing modes and year vibrates', (WidgetTester tester) async { + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); await tester.tap(find.text('January 2016')); await tester.pump(hapticFeedbackInterval); expect(feedback.hapticCount, 1); @@ -656,10 +845,12 @@ void main() { }); group('Semantics', () { - testWidgets('day mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('day mode', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); - await tester.pumpWidget(calendarDatePicker()); + await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), + )); // Year mode drop down button. expect(tester.getSemantics(find.text('January 2016')), matchesSemantics( @@ -870,10 +1061,11 @@ void main() { semantics.dispose(); }); - testWidgets('calendar year mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('calendar year mode', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); await tester.pumpWidget(calendarDatePicker( + initialDate: DateTime(2016, DateTime.january, 15), initialCalendarMode: DatePickerMode.year, )); @@ -899,12 +1091,12 @@ void main() { }); group('YearPicker', () { - testWidgets('Current year is visible in year picker', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Current year is visible in year picker', (WidgetTester tester) async { await tester.pumpWidget(yearPicker()); expect(find.text('2016'), findsOneWidget); }); - testWidgets('Can select a year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a year', (WidgetTester tester) async { DateTime? selectedDate; await tester.pumpWidget(yearPicker( onChanged: (DateTime date) => selectedDate = date, @@ -915,11 +1107,11 @@ void main() { expect(selectedDate, equals(DateTime(2018))); }); - testWidgets('Cannot select disabled year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select disabled year', (WidgetTester tester) async { DateTime? selectedYear; await tester.pumpWidget(yearPicker( firstDate: DateTime(2018, DateTime.june, 9), - initialDate: DateTime(2018, DateTime.july, 4), + selectedDate: DateTime(2018, DateTime.july, 4), lastDate: DateTime(2018, DateTime.december, 15), onChanged: (DateTime date) => selectedYear = date, )); @@ -933,5 +1125,43 @@ void main() { await tester.pumpAndSettle(); expect(selectedYear, equals(DateTime(2018, DateTime.july))); }); + + testWidgetsWithLeakTracking('Selecting year with no selected month uses earliest month', (WidgetTester tester) async { + DateTime? selectedYear; + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2018')); + expect(selectedYear, equals(DateTime(2018, DateTime.june))); + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + selectedDate: DateTime(2018, DateTime.june), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2019')); + expect(selectedYear, equals(DateTime(2019, DateTime.june))); + }); + + testWidgetsWithLeakTracking('Selecting year with no selected month uses January', (WidgetTester tester) async { + DateTime? selectedYear; + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2019')); + expect(selectedYear, equals(DateTime(2019))); // january implied + await tester.pumpWidget(yearPicker( + firstDate: DateTime(2018, DateTime.june, 9), + lastDate: DateTime(2019, DateTime.december, 15), + selectedDate: DateTime(2018), + onChanged: (DateTime date) => selectedYear = date, + )); + await tester.tap(find.text('2018')); + expect(selectedYear, equals(DateTime(2018, DateTime.june))); + }); }); } diff --git a/packages/flutter/test/material/card_test.dart b/packages/flutter/test/material/card_test.dart index 738104686adf9..1f08017a13257 100644 --- a/packages/flutter/test/material/card_test.dart +++ b/packages/flutter/test/material/card_test.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Card can take semantic text from multiple children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card can take semantic text from multiple children', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -77,7 +77,7 @@ void main() { semantics.dispose(); }); - testWidgets('Card merges children when it is a semanticContainer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card merges children when it is a semanticContainer', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); debugResetSemanticsIdCounter(); @@ -116,7 +116,7 @@ void main() { semantics.dispose(); }); - testWidgets('Card margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card margin', (WidgetTester tester) async { const Key contentsKey = ValueKey<String>('contents'); await tester.pumpWidget( @@ -163,7 +163,7 @@ void main() { expect(tester.getSize(find.byKey(contentsKey)), const Size(100.0, 100.0)); }); - testWidgets('Card clipBehavior property passes through to the Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card clipBehavior property passes through to the Material', (WidgetTester tester) async { await tester.pumpWidget(const Card()); expect(tester.widget<Material>(find.byType(Material)).clipBehavior, Clip.none); @@ -171,7 +171,7 @@ void main() { expect(tester.widget<Material>(find.byType(Material)).clipBehavior, Clip.antiAlias); }); - testWidgets('Card clipBehavior property defers to theme when null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card clipBehavior property defers to theme when null', (WidgetTester tester) async { await tester.pumpWidget(Builder(builder: (BuildContext context) { final ThemeData themeData = Theme.of(context); return Theme( @@ -186,7 +186,7 @@ void main() { expect(tester.widget<Material>(find.byType(Material)).clipBehavior, Clip.antiAliasWithSaveLayer); }); - testWidgets('Card shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card shadowColor', (WidgetTester tester) async { Material getCardMaterial(WidgetTester tester) { return tester.widget<Material>( find.descendant( diff --git a/packages/flutter/test/material/card_theme_test.dart b/packages/flutter/test/material/card_theme_test.dart index 992fbf937b222..f9ba9ce3fe571 100644 --- a/packages/flutter/test/material/card_theme_test.dart +++ b/packages/flutter/test/material/card_theme_test.dart @@ -9,6 +9,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('CardTheme copyWith, ==, hashCode basics', () { @@ -22,7 +23,7 @@ void main() { expect(identical(CardTheme.lerp(theme, theme, 0.5), theme), true); }); - testWidgets('Passing no CardTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Passing no CardTheme returns defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, @@ -45,7 +46,7 @@ void main() { )); }); - testWidgets('Card uses values from CardTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card uses values from CardTheme', (WidgetTester tester) async { final CardTheme cardTheme = _cardTheme(); await tester.pumpWidget(MaterialApp( @@ -67,7 +68,7 @@ void main() { expect(material.shape, cardTheme.shape); }); - testWidgets('Card widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Card widget properties take priority over theme', (WidgetTester tester) async { const Clip clip = Clip.hardEdge; const Color color = Colors.orange; const Color shadowColor = Colors.pink; @@ -102,7 +103,7 @@ void main() { expect(material.shape, shape); }); - testWidgets('CardTheme properties take priority over ThemeData properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CardTheme properties take priority over ThemeData properties', (WidgetTester tester) async { final CardTheme cardTheme = _cardTheme(); final ThemeData themeData = _themeData().copyWith(cardTheme: cardTheme); @@ -117,9 +118,8 @@ void main() { expect(material.color, cardTheme.color); }); - testWidgets('ThemeData properties are used when no CardTheme is set', (WidgetTester tester) async { - final ThemeData themeData = _themeData(); - final bool material3 = themeData.useMaterial3; + testWidgetsWithLeakTracking('Material3 - ThemeData properties are used when no CardTheme is set', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: themeData, @@ -129,10 +129,10 @@ void main() { )); final Material material = _getCardMaterial(tester); - expect(material.color, material3 ? themeData.colorScheme.surface: themeData.cardColor); + expect(material.color, themeData.colorScheme.surface); }); - testWidgets('CardTheme customizes shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - CardTheme customizes shape', (WidgetTester tester) async { const CardTheme cardTheme = CardTheme( color: Colors.white, shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(7))), @@ -166,7 +166,21 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Passing no CardTheme returns defaults - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - ThemeData properties are used when no CardTheme is set', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(useMaterial3: false); + + await tester.pumpWidget(MaterialApp( + theme: themeData, + home: const Scaffold( + body: Card(), + ), + )); + + final Material material = _getCardMaterial(tester); + expect(material.color, themeData.cardColor); + }); + + testWidgetsWithLeakTracking('Material2 - Passing no CardTheme returns defaults', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -188,7 +202,7 @@ void main() { )); }); - testWidgets('CardTheme customizes shape - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - CardTheme customizes shape', (WidgetTester tester) async { const CardTheme cardTheme = CardTheme( color: Colors.white, shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(7))), diff --git a/packages/flutter/test/material/checkbox_list_tile_test.dart b/packages/flutter/test/material/checkbox_list_tile_test.dart index 5a7754e68847d..34d57a4e65bb6 100644 --- a/packages/flutter/test/material/checkbox_list_tile_test.dart +++ b/packages/flutter/test/material/checkbox_list_tile_test.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'feedback_tester.dart'; Widget wrap({ required Widget child }) { @@ -22,7 +22,7 @@ Widget wrap({ required Widget child }) { } void main() { - testWidgets('CheckboxListTile control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile control test', (WidgetTester tester) async { final List<dynamic> log = <dynamic>[]; await tester.pumpWidget(wrap( child: CheckboxListTile( @@ -37,7 +37,7 @@ void main() { expect(log, equals(<dynamic>[false, '-', false])); }); - testWidgets('CheckboxListTile checkColor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - CheckboxListTile checkColor test', (WidgetTester tester) async { const Color checkBoxBorderColor = Color(0xff2196f3); Color checkBoxCheckColor = const Color(0xffFFFFFF); @@ -69,7 +69,39 @@ void main() { expect(getCheckboxListTileRenderer(), paints..path(color: checkBoxBorderColor)..path(color: checkBoxCheckColor)); }); - testWidgets('CheckboxListTile activeColor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - CheckboxListTile checkColor test', (WidgetTester tester) async { + const Color checkBoxBorderColor = Color(0xff6750a4); + Color checkBoxCheckColor = const Color(0xffFFFFFF); + + Widget buildFrame(Color? color) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: CheckboxListTile( + value: true, + checkColor: color, + onChanged: (bool? value) {}, + ), + ), + ); + } + + RenderBox getCheckboxListTileRenderer() { + return tester.renderObject<RenderBox>(find.byType(CheckboxListTile)); + } + + await tester.pumpWidget(buildFrame(null)); + await tester.pumpAndSettle(); + expect(getCheckboxListTileRenderer(), paints..path(color: checkBoxBorderColor)..path(color: checkBoxCheckColor)); + + checkBoxCheckColor = const Color(0xFF000000); + + await tester.pumpWidget(buildFrame(checkBoxCheckColor)); + await tester.pumpAndSettle(); + expect(getCheckboxListTileRenderer(), paints..path(color: checkBoxBorderColor)..path(color: checkBoxCheckColor)); + }); + + testWidgetsWithLeakTracking('CheckboxListTile activeColor test', (WidgetTester tester) async { Widget buildFrame(Color? themeColor, Color? activeColor) { return wrap( child: Theme( @@ -101,7 +133,7 @@ void main() { expect(getCheckboxListTileRenderer(), paints..path(color: const Color(0xFFFFFFFF))); }); - testWidgets('CheckboxListTile can autofocus unless disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile can autofocus unless disabled.', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( @@ -133,7 +165,7 @@ void main() { expect(Focus.maybeOf(childKey.currentContext!)!.hasPrimaryFocus, isFalse); }); - testWidgets('CheckboxListTile contentPadding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile contentPadding test', (WidgetTester tester) async { await tester.pumpWidget( wrap( child: const Center( @@ -163,7 +195,7 @@ void main() { expect(paddingRect.bottom, tallerWidget.bottom + remainingHeight / 2 + 2); }); - testWidgets('CheckboxListTile tristate test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile tristate test', (WidgetTester tester) async { bool? value = false; bool tristate = false; @@ -241,7 +273,7 @@ void main() { expect(value, false); }); - testWidgets('CheckboxListTile respects shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects shape', (WidgetTester tester) async { const ShapeBorder shapeBorder = RoundedRectangleBorder( borderRadius: BorderRadius.horizontal(right: Radius.circular(100)), ); @@ -258,7 +290,7 @@ void main() { expect(tester.widget<InkWell>(find.byType(InkWell)).customBorder, shapeBorder); }); - testWidgets('CheckboxListTile respects tileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects tileColor', (WidgetTester tester) async { final Color tileColor = Colors.red.shade500; await tester.pumpWidget( @@ -277,7 +309,7 @@ void main() { expect(find.byType(Material), paints..rect(color: tileColor)); }); - testWidgets('CheckboxListTile respects selectedTileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects selectedTileColor', (WidgetTester tester) async { final Color selectedTileColor = Colors.green.shade500; await tester.pumpWidget( @@ -297,7 +329,7 @@ void main() { expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); - testWidgets('CheckboxListTile selected item text Color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile selected item text Color', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/76908 const Color activeColor = Color(0xff00ff00); @@ -336,7 +368,7 @@ void main() { expect(textColor('title'), activeColor); }); - testWidgets('CheckboxListTile respects checkbox shape and side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects checkbox shape and side', (WidgetTester tester) async { Widget buildApp(BorderSide side, OutlinedBorder shape) { return MaterialApp( home: Material( @@ -390,7 +422,7 @@ void main() { ); }); - testWidgets('CheckboxListTile respects visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects visualDensity', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -414,7 +446,7 @@ void main() { expect(box.size, equals(const Size(800, 56))); }); - testWidgets('CheckboxListTile respects focusNode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects focusNode', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( wrap( @@ -436,7 +468,7 @@ void main() { expect(tileNode.hasPrimaryFocus, isTrue); }); - testWidgets('CheckboxListTile onFocusChange callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile onFocusChange callback', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'CheckboxListTile onFocusChange'); bool gotFocus = false; await tester.pumpWidget( @@ -463,9 +495,11 @@ void main() { await tester.pump(); expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('CheckboxListTile can be disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile can be disabled', (WidgetTester tester) async { bool? value = false; bool enabled = true; @@ -506,7 +540,7 @@ void main() { expect(tester.widget<Checkbox>(checkbox).value, true); }); - testWidgets('CheckboxListTile respects mouseCursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects mouseCursor when hovered', (WidgetTester tester) async { // Test Checkbox() constructor await tester.pumpWidget( wrap( @@ -577,7 +611,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('CheckboxListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async { const Color activeEnabledFillColor = Color(0xFF000001); const Color activeDisabledFillColor = Color(0xFF000002); @@ -613,7 +647,7 @@ void main() { expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor)); }); - testWidgets('CheckboxListTile respects fillColor in hovered state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects fillColor in hovered state', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredFillColor = Color(0xFF000001); @@ -657,7 +691,7 @@ void main() { expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor)); }); - testWidgets('CheckboxListTile respects hoverColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects hoverColor', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp({bool enabled = true}) { @@ -709,7 +743,7 @@ void main() { ); }); - testWidgets('CheckboxListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - CheckboxListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color fillColor = Color(0xFF000000); @@ -825,7 +859,130 @@ void main() { ); }); - testWidgets('CheckboxListTile respects splashRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - CheckboxListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + + const Color fillColor = Color(0xFF000000); + const Color activePressedOverlayColor = Color(0xFF000001); + const Color inactivePressedOverlayColor = Color(0xFF000002); + const Color hoverOverlayColor = Color(0xFF000003); + const Color hoverColor = Color(0xFF000005); + + Color? getOverlayColor(Set<MaterialState> states) { + if (states.contains(MaterialState.pressed)) { + if (states.contains(MaterialState.selected)) { + return activePressedOverlayColor; + } + return inactivePressedOverlayColor; + } + if (states.contains(MaterialState.hovered)) { + return hoverOverlayColor; + } + return null; + } + const double splashRadius = 24.0; + + Widget buildCheckbox({bool active = false, bool useOverlay = true}) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: CheckboxListTile( + value: active, + onChanged: (_) { }, + fillColor: const MaterialStatePropertyAll<Color>(fillColor), + overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null, + hoverColor: hoverColor, + splashRadius: splashRadius, + ), + ), + ); + } + + await tester.pumpWidget(buildCheckbox(useOverlay: false)); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + kIsWeb ? (paints..circle()..circle( + color: fillColor.withAlpha(kRadialReactionAlpha), + radius: splashRadius, + )) : (paints..circle( + color: fillColor.withAlpha(kRadialReactionAlpha), + radius: splashRadius, + )), + reason: 'Default inactive pressed Checkbox should have overlay color from fillColor', + ); + + await tester.pumpWidget(buildCheckbox(active: true, useOverlay: false)); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + kIsWeb ? (paints..circle()..circle( + color: fillColor.withAlpha(kRadialReactionAlpha), + radius: splashRadius, + )) : (paints..circle( + color: fillColor.withAlpha(kRadialReactionAlpha), + radius: splashRadius, + )), + reason: 'Default active pressed Checkbox should have overlay color from fillColor', + ); + + await tester.pumpWidget(buildCheckbox()); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + kIsWeb ? (paints..circle()..circle( + color: inactivePressedOverlayColor, + radius: splashRadius, + )) : (paints..circle( + color: inactivePressedOverlayColor, + radius: splashRadius, + )), + reason: 'Inactive pressed Checkbox should have overlay color: $inactivePressedOverlayColor', + ); + + await tester.pumpWidget(buildCheckbox(active: true)); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + kIsWeb ? (paints..circle()..circle( + color: activePressedOverlayColor, + radius: splashRadius, + )) : (paints..circle( + color: activePressedOverlayColor, + radius: splashRadius, + )), + reason: 'Active pressed Checkbox should have overlay color: $activePressedOverlayColor', + ); + + // Start hovering + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildCheckbox()); + await tester.pumpAndSettle(); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints..circle( + color: hoverOverlayColor, + radius: splashRadius, + ), + reason: 'Hovered Checkbox should use overlay color $hoverOverlayColor over $hoverColor', + ); + }); + + testWidgetsWithLeakTracking('CheckboxListTile respects splashRadius', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { @@ -854,7 +1011,7 @@ void main() { ); }); - testWidgets('CheckboxListTile respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( wrap( child: CheckboxListTile( @@ -880,7 +1037,7 @@ void main() { expect(tester.getSize(find.byType(Checkbox)), const Size(48.0, 48.0)); }); - testWidgets('CheckboxListTile respects isError - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - CheckboxListTile respects isError', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; @@ -928,7 +1085,7 @@ void main() { ); }); - testWidgets('CheckboxListTile.adaptive shows the correct checkbox platform widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile.adaptive shows the correct checkbox platform widget', (WidgetTester tester) async { Widget buildApp(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), @@ -971,7 +1128,7 @@ void main() { feedback.dispose(); }); - testWidgets('CheckboxListTile respects enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile respects enableFeedback', (WidgetTester tester) async { Future<void> buildTest(bool enableFeedback) async { return tester.pumpWidget( wrap( @@ -1000,7 +1157,7 @@ void main() { }); }); - testWidgets('CheckboxListTile has proper semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxListTile has proper semantics', (WidgetTester tester) async { final List<dynamic> log = <dynamic>[]; final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(wrap( diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart index 8a7a592f24e12..40edadc0b15bc 100644 --- a/packages/flutter/test/material/checkbox_test.dart +++ b/packages/flutter/test/material/checkbox_test.dart @@ -11,8 +11,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/gestures/constants.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -21,7 +21,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('Checkbox size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded), @@ -61,7 +61,7 @@ void main() { expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0)); }); - testWidgets('Checkbox semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(Theme( @@ -219,7 +219,7 @@ void main() { handle.dispose(); }); - testWidgets('Can wrap Checkbox with Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can wrap Checkbox with Semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(Theme( @@ -248,7 +248,7 @@ void main() { handle.dispose(); }); - testWidgets('Checkbox tristate: true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox tristate: true', (WidgetTester tester) async { bool? checkBoxValue; await tester.pumpWidget( @@ -295,7 +295,7 @@ void main() { expect(checkBoxValue, null); }); - testWidgets('has semantics for tristate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantics for tristate', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( @@ -371,7 +371,7 @@ void main() { semantics.dispose(); }); - testWidgets('has semantic events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantic events', (WidgetTester tester) async { dynamic semanticEvent; bool? checkboxValue = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { @@ -414,7 +414,8 @@ void main() { semanticsTester.dispose(); }); - testWidgets('Checkbox tristate rendering, programmatic transitions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox tristate rendering, programmatic transitions', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); Widget buildFrame(bool? checkboxValue) { return Theme( data: theme, @@ -446,8 +447,8 @@ void main() { await tester.pumpAndSettle(); expect(getCheckboxRenderer(), paints - ..path(color: theme.useMaterial3 ? theme.colorScheme.primary : theme.colorScheme.secondary) - ..path(color: theme.useMaterial3 ? theme.colorScheme.onPrimary : const Color(0xFFFFFFFF)) + ..path(color: theme.colorScheme.secondary) + ..path(color: const Color(0xFFFFFFFF)) ); // checkmark is rendered as a path await tester.pumpWidget(buildFrame(false)); @@ -464,8 +465,8 @@ void main() { await tester.pumpAndSettle(); expect(getCheckboxRenderer(), paints - ..path(color: theme.useMaterial3 ? theme.colorScheme.primary : theme.colorScheme.secondary) - ..path(color: theme.useMaterial3 ? theme.colorScheme.onPrimary : const Color(0xFFFFFFFF)) + ..path(color: theme.colorScheme.secondary) + ..path(color: const Color(0xFFFFFFFF)) ); // checkmark is rendered as a path await tester.pumpWidget(buildFrame(null)); @@ -473,10 +474,69 @@ void main() { expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash") }); - testWidgets('Checkbox color rendering', (WidgetTester tester) async { - final ThemeData theme = ThemeData(); + testWidgetsWithLeakTracking('Material3 - Checkbox tristate rendering, programmatic transitions', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + Widget buildFrame(bool? checkboxValue) { + return Theme( + data: theme, + child: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Checkbox( + tristate: true, + value: checkboxValue, + onChanged: (bool? value) { }, + ); + }, + ), + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject<RenderBox>(find.byType(Checkbox)); + } + + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: Colors.transparent)); // paint transparent border + expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash") + expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox + + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), + paints + ..path(color: theme.colorScheme.primary) + ..path(color: theme.colorScheme.onPrimary) + ); // checkmark is rendered as a path + + await tester.pumpWidget(buildFrame(false)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: Colors.transparent)); // paint transparent border + expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash") + expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox + + await tester.pumpWidget(buildFrame(null)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash") + + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), + paints + ..path(color: theme.colorScheme.primary) + ..path(color: theme.colorScheme.onPrimary) + ); // checkmark is rendered as a path + + await tester.pumpWidget(buildFrame(null)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..line()); // null is rendered as a line (a "dash") + }); + + testWidgetsWithLeakTracking('Material2 - Checkbox color rendering', (WidgetTester tester) async { + ThemeData theme = ThemeData(useMaterial3: false); const Color borderColor = Color(0xff2196f3); - const Color m3BorderColor = Color(0xFF6750A4); Color checkColor = const Color(0xffFFFFFF); Color activeColor; @@ -504,24 +564,20 @@ void main() { await tester.pumpWidget(buildFrame(checkColor: checkColor)); await tester.pumpAndSettle(); - expect(getCheckboxRenderer(), paints..path(color: theme.useMaterial3 ? m3BorderColor : borderColor)..path(color: checkColor)); // paints's color is 0xFFFFFFFF (default color) + expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFFFFFFFF (default color) checkColor = const Color(0xFF000000); await tester.pumpWidget(buildFrame(checkColor: checkColor)); await tester.pumpAndSettle(); - expect(getCheckboxRenderer(), paints..path(color: theme.useMaterial3 ? m3BorderColor : borderColor)..path(color: checkColor)); // paints's color is 0xFF000000 (params) + expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFF000000 (params) activeColor = const Color(0xFF00FF00); - ThemeData themeData = ThemeData(); - final bool material3 = themeData.useMaterial3; - final ColorScheme colorScheme = material3 - ? const ColorScheme.light().copyWith(primary: activeColor) - : const ColorScheme.light().copyWith(secondary: activeColor); - themeData = themeData.copyWith(colorScheme: colorScheme); + final ColorScheme colorScheme = const ColorScheme.light().copyWith(secondary: activeColor); + theme = theme.copyWith(colorScheme: colorScheme); await tester.pumpWidget(buildFrame( - themeData: themeData), + themeData: theme), ); await tester.pumpAndSettle(); expect(getCheckboxRenderer(), paints..path(color: activeColor)); // paints's color is 0xFF00FF00 (theme) @@ -530,11 +586,137 @@ void main() { await tester.pumpWidget(buildFrame(activeColor: activeColor)); await tester.pumpAndSettle(); - expect(getCheckboxRenderer(), paints..path(color: activeColor)); // paints's color is 0xFF000000 (params) + expect(getCheckboxRenderer(), paints..path(color: activeColor)); + }); + + testWidgetsWithLeakTracking('Material3 - Checkbox color rendering', (WidgetTester tester) async { + ThemeData theme = ThemeData(useMaterial3: true); + const Color borderColor = Color(0xFF6750A4); + Color checkColor = const Color(0xffFFFFFF); + Color activeColor; + + Widget buildFrame({Color? activeColor, Color? checkColor, ThemeData? themeData}) { + return Material( + child: Theme( + data: themeData ?? theme, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Checkbox( + value: true, + activeColor: activeColor, + checkColor: checkColor, + onChanged: (bool? value) { }, + ); + }, + ), + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject<RenderBox>(find.byType(Checkbox)); + } + + await tester.pumpWidget(buildFrame(checkColor: checkColor)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFFFFFFFF (default color) + + checkColor = const Color(0xFF000000); + + await tester.pumpWidget(buildFrame(checkColor: checkColor)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFF000000 (params) + + activeColor = const Color(0xFF00FF00); + + final ColorScheme colorScheme = const ColorScheme.light().copyWith(primary: activeColor); + theme = theme.copyWith(colorScheme: colorScheme); + await tester.pumpWidget(buildFrame(themeData: theme)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeColor)); // paints's color is 0xFF00FF00 (theme) + + activeColor = const Color(0xFF000000); + + await tester.pumpWidget(buildFrame(activeColor: activeColor)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeColor)); }); - testWidgets('Checkbox is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + bool? value = true; + Widget buildApp({bool enabled = true}) { + return MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Material( + child: Center( + child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return Checkbox( + value: value, + onChanged: enabled ? (bool? newValue) { + setState(() { + value = newValue; + }); + } : null, + focusColor: Colors.orange[500], + autofocus: true, + focusNode: focusNode, + ); + }), + ), + ), + ); + } + await tester.pumpWidget(buildApp()); + + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..circle(color: Colors.orange[500]) + ..path(color: const Color(0xff2196f3)) + ..path(color: Colors.white) + ); + + // Check the false value. + value = false; + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..circle(color: Colors.orange[500]) + ..drrect( + color: const Color(0x8a000000), + outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)), + inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero), + ), + ); + + // Check what happens when disabled. + value = false; + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isFalse); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..drrect( + color: const Color(0x61000000), + outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)), + inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero), + ), + ); + }); + + testWidgetsWithLeakTracking('Material3 - Checkbox is focusable and has correct focus color', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); + final ThemeData theme = ThemeData(useMaterial3: true); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp({bool enabled = true}) { @@ -562,19 +744,13 @@ void main() { await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); - final bool material3 = theme.useMaterial3; expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Checkbox))), - material3 - ? (paints - ..circle(color: Colors.orange[500]) - ..path(color: theme.colorScheme.primary) - ..path(color: theme.colorScheme.onPrimary)) - : (paints - ..circle(color: Colors.orange[500]) - ..path(color: const Color(0xff2196f3)) - ..path(color: Colors.white)) + paints + ..circle(color: Colors.orange[500]) + ..path(color: theme.colorScheme.primary) + ..path(color: theme.colorScheme.onPrimary) ); // Check the false value. @@ -587,8 +763,8 @@ void main() { paints ..circle(color: Colors.orange[500]) ..drrect( - color: material3 ? theme.colorScheme.onSurface : const Color(0x8a000000), - outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, material3 ? const Radius.circular(2.0) : const Radius.circular(1.0)), + color: theme.colorScheme.onSurface, + outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(2.0)), inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero), ), ); @@ -602,14 +778,14 @@ void main() { Material.of(tester.element(find.byType(Checkbox))), paints ..drrect( - color: material3 ? theme.colorScheme.onSurface.withOpacity(0.38) : const Color(0x61000000), - outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, material3 ? const Radius.circular(2.0) : const Radius.circular(1.0)), + color: theme.colorScheme.onSurface.withOpacity(0.38), + outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(2.0)), inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, Radius.zero), ), ); }); - testWidgets('Checkbox with splash radius set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox with splash radius set', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { @@ -638,7 +814,7 @@ void main() { ); }); - testWidgets('Checkbox starts the splash in center, even when tap is on the corner', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox starts the splash in center, even when tap is on the corner', (WidgetTester tester) async { Widget buildApp() { return MaterialApp( theme: theme, @@ -670,10 +846,10 @@ void main() { ); }); - testWidgets('Checkbox can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; - final bool material3 = theme.useMaterial3; + final ThemeData theme = ThemeData(useMaterial3: false); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, @@ -699,8 +875,8 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), paints - ..path(color: material3 ? const Color(0xff6750a4) : const Color(0xff2196f3)) - ..path(color: material3 ? theme.colorScheme.onPrimary : const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), + ..path(color: const Color(0xff2196f3)) + ..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), ); // Start hovering @@ -712,8 +888,8 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), paints - ..path(color: material3 ? const Color(0xff6750a4) : const Color(0xff2196f3)) - ..path(color: material3 ? theme.colorScheme.onPrimary : const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), + ..path(color: const Color(0xff2196f3)) + ..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), ); // Check what happens when disabled. @@ -722,12 +898,69 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), paints - ..path(color: material3 ? theme.colorScheme.onSurface.withOpacity(0.38) : const Color(0x61000000)) - ..path(color: material3 ? theme.colorScheme.surface : const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), + ..path(color: const Color(0x61000000)) + ..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0), ); }); - testWidgets('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Checkbox can be hovered and has correct hover color', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + bool? value = true; + final ThemeData theme = ThemeData(useMaterial3: true); + Widget buildApp({bool enabled = true}) { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return Checkbox( + value: value, + onChanged: enabled ? (bool? newValue) { + setState(() { + value = newValue; + }); + } : null, + hoverColor: Colors.orange[500], + ); + }), + ), + ), + ); + } + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..path(color: const Color(0xff6750a4)) + ..path(color: theme.colorScheme.onPrimary, style: PaintingStyle.stroke, strokeWidth: 2.0), + ); + + // Start hovering + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(tester.getCenter(find.byType(Checkbox))); + + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..path(color: const Color(0xff6750a4)) + ..path(color: theme.colorScheme.onPrimary, style: PaintingStyle.stroke, strokeWidth: 2.0), + ); + + // Check what happens when disabled. + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints + ..path(color: theme.colorScheme.onSurface.withOpacity(0.38)) + ..path(color: theme.colorScheme.surface, style: PaintingStyle.stroke, strokeWidth: 2.0), + ); + }); + + testWidgetsWithLeakTracking('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp({bool enabled = true}) { @@ -768,7 +1001,7 @@ void main() { expect(value, isTrue); }); - testWidgets('Checkbox responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -806,7 +1039,7 @@ void main() { expect(box.size, equals(const Size(60, 36))); }); - testWidgets('Checkbox stops hover animation when removed from the tree.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox stops hover animation when removed from the tree.', (WidgetTester tester) async { const Key checkboxKey = Key('checkbox'); bool? checkboxVal = true; @@ -860,7 +1093,7 @@ void main() { }); - testWidgets('Checkbox changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox changes mouse cursor when hovered', (WidgetTester tester) async { // Test Checkbox() constructor await tester.pumpWidget( MaterialApp( @@ -965,7 +1198,7 @@ void main() { }); - testWidgets('Checkbox fill color resolves in enabled/disabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox fill color resolves in enabled/disabled states', (WidgetTester tester) async { const Color activeEnabledFillColor = Color(0xFF000001); const Color activeDisabledFillColor = Color(0xFF000002); @@ -1009,8 +1242,10 @@ void main() { expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor)); }); - testWidgets('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'checkbox'); + addTearDown(focusNode.dispose); + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredFillColor = Color(0xFF000001); const Color focusedFillColor = Color(0xFF000002); @@ -1065,7 +1300,7 @@ void main() { expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor)); }); - testWidgets('Checkbox respects shape and side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox respects shape and side', (WidgetTester tester) async { const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))); @@ -1108,12 +1343,13 @@ void main() { ); }); - testWidgets('Checkbox default overlay color in active/pressed/focused/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox default overlay color in active/pressed/focused/hovered states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + final ThemeData theme = ThemeData(useMaterial3: false); final ColorScheme colors = theme.colorScheme; - final bool material3 = theme.useMaterial3; Widget buildCheckbox({bool active = false, bool focused = false}) { return MaterialApp( theme: theme, @@ -1134,11 +1370,8 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), - material3 - ? (paints..circle(color: colors.primary.withOpacity(0.12))) - : (paints - ..circle(color: theme.unselectedWidgetColor.withAlpha(kRadialReactionAlpha),) - ), + paints + ..circle(color: theme.unselectedWidgetColor.withAlpha(kRadialReactionAlpha)), reason: 'Default inactive pressed Checkbox should have overlay color from default fillColor', ); @@ -1148,11 +1381,74 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), - material3 - ? (paints..circle(color: colors.onSurface.withOpacity(0.12))) - : (paints - ..circle(color: colors.secondary.withAlpha(kRadialReactionAlpha),) + paints + ..circle(color: colors.secondary.withAlpha(kRadialReactionAlpha)), + reason: 'Default active pressed Checkbox should have overlay color from default fillColor', + ); + + await tester.pumpWidget(Container()); // reset test + await tester.pumpWidget(buildCheckbox(focused: true)); + await tester.pumpAndSettle(); + + expect(focusNode.hasPrimaryFocus, isTrue); + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints..circle(color: theme.focusColor), + reason: 'Focused Checkbox should use default focused overlay color', + ); + + await tester.pumpWidget(Container()); // reset test + await tester.pumpWidget(buildCheckbox()); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints..circle(color: theme.hoverColor), + reason: 'Hovered Checkbox should use default hovered overlay color', + ); + }); + + testWidgetsWithLeakTracking('Material3 - Checkbox default overlay color in active/pressed/focused/hovered states', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + + final ThemeData theme = ThemeData(useMaterial3: true); + final ColorScheme colors = theme.colorScheme; + Widget buildCheckbox({bool active = false, bool focused = false}) { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Checkbox( + focusNode: focusNode, + autofocus: focused, + value: active, + onChanged: (_) { }, + ), ), + ); + } + + await tester.pumpWidget(buildCheckbox()); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints..circle(color: colors.primary.withOpacity(0.12)), + reason: 'Default inactive pressed Checkbox should have overlay color from default fillColor', + ); + + await tester.pumpWidget(buildCheckbox(active: true)); + await tester.startGesture(tester.getCenter(find.byType(Checkbox))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Checkbox))), + paints..circle(color: colors.onSurface.withOpacity(0.12)), reason: 'Default active pressed Checkbox should have overlay color from default fillColor', ); @@ -1163,9 +1459,7 @@ void main() { expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Checkbox))), - material3 - ? (paints..circle(color: colors.onSurface.withOpacity(0.12))) - : (paints..circle(color: theme.focusColor)), + paints..circle(color: colors.onSurface.withOpacity(0.12)), reason: 'Focused Checkbox should use default focused overlay color', ); @@ -1178,15 +1472,14 @@ void main() { expect( Material.of(tester.element(find.byType(Checkbox))), - material3 - ? (paints..circle(color: colors.onSurface.withOpacity(0.08))) - : (paints..circle(color: theme.hoverColor)), + paints..circle(color: colors.onSurface.withOpacity(0.08)), reason: 'Hovered Checkbox should use default hovered overlay color', ); }); - testWidgets('Checkbox overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color fillColor = Color(0xFF000000); @@ -1321,7 +1614,7 @@ void main() { ); }); - testWidgets('Tristate Checkbox overlay color resolves in pressed active/inactive states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tristate Checkbox overlay color resolves in pressed active/inactive states', (WidgetTester tester) async { const Color activePressedOverlayColor = Color(0xFF000001); const Color inactivePressedOverlayColor = Color(0xFF000002); @@ -1428,7 +1721,7 @@ void main() { await gesture.up(); }); - testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { Widget buildCheckbox(bool show) { return MaterialApp( theme: theme, @@ -1452,7 +1745,7 @@ void main() { await gesture.up(); }); - testWidgets('Checkbox BorderSide side only applies when unselected in M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox BorderSide side only applies when unselected in M2', (WidgetTester tester) async { const Color borderColor = Color(0xfff44336); const Color activeColor = Color(0xff123456); const BorderSide side = BorderSide( @@ -1516,13 +1809,13 @@ void main() { expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill }); - testWidgets('Checkbox MaterialStateBorderSide applies unconditionally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox MaterialStateBorderSide applies unconditionally', (WidgetTester tester) async { const Color borderColor = Color(0xfff44336); const BorderSide side = BorderSide( width: 4, color: borderColor, ); - final bool material3 = theme.useMaterial3; + final ThemeData theme = ThemeData(useMaterial3: false); Widget buildApp({ bool? value, bool enabled = true }) { return MaterialApp( @@ -1546,7 +1839,7 @@ void main() { paints ..drrect( color: borderColor, - outer: material3 ? RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(2)) : RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)), + outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)), inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero), ), ); @@ -1570,7 +1863,61 @@ void main() { expectBorder(); }); - testWidgets('disabled checkbox shows tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Checkbox MaterialStateBorderSide applies unconditionally', (WidgetTester tester) async { + const Color borderColor = Color(0xfff44336); + const BorderSide side = BorderSide( + width: 4, + color: borderColor, + ); + final ThemeData theme = ThemeData(useMaterial3: true); + + Widget buildApp({ bool? value, bool enabled = true }) { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Checkbox( + value: value, + tristate: value == null, + onChanged: enabled ? (bool? newValue) { } : null, + side: MaterialStateBorderSide.resolveWith((Set<MaterialState> states) => side), + ), + ), + ), + ); + } + + void expectBorder() { + expect( + tester.renderObject<RenderBox>(find.byType(Checkbox)), + paints + ..drrect( + color: borderColor, + outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(2)), + inner: RRect.fromLTRBR(19, 19, 29, 29, Radius.zero), + ), + ); + } + + await tester.pumpWidget(buildApp(value: false)); + await tester.pumpAndSettle(); + expectBorder(); + + + await tester.pumpWidget(buildApp(value: false, enabled: false)); + await tester.pumpAndSettle(); + expectBorder(); + + await tester.pumpWidget(buildApp(value: true)); + await tester.pumpAndSettle(); + expectBorder(); + + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expectBorder(); + }); + + testWidgetsWithLeakTracking('disabled checkbox shows tooltip', (WidgetTester tester) async { const String longPressTooltip = 'long press tooltip'; const String tapTooltip = 'tap tooltip'; await tester.pumpWidget( @@ -1624,8 +1971,9 @@ void main() { expect(find.text(tapTooltip), findsOneWidget); }); - testWidgets('Checkbox has default error color when isError is set to true - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Checkbox has default error color when isError is set to true', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); final ThemeData themeData = ThemeData(useMaterial3: true); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; @@ -1696,8 +2044,9 @@ void main() { await tester.pump(); }); - testWidgets('Checkbox MaterialStateBorderSide applies in error states - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Checkbox MaterialStateBorderSide applies in error states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox'); + addTearDown(focusNode.dispose); final ThemeData themeData = ThemeData(useMaterial3: true); const Color borderColor = Color(0xffffeb3b); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1775,7 +2124,7 @@ void main() { await tester.pump(); }); - testWidgets('Checkbox has correct default shape - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Checkbox has correct default shape', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); Widget buildApp() { @@ -1809,7 +2158,7 @@ void main() { ); }); - testWidgets('Checkbox.adaptive shows the correct platform widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox.adaptive shows the correct platform widget', (WidgetTester tester) async { Widget buildApp(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), @@ -1841,7 +2190,8 @@ void main() { } }); - testWidgets('Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); const Color activeBackgroundColor = Color(0xff123456); const Color inactiveBackgroundColor = Color(0xff654321); @@ -1870,14 +2220,66 @@ void main() { } // Checkbox is unselected, so the default BorderSide appears and fillColor is checkbox's background color. + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect( + getCheckboxRenderer(), + paints + ..drrect( + color: theme.unselectedWidgetColor, + ), + ); + expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor)); + + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); + expect( + getCheckboxRenderer(), + paints + ..drrect( + color: theme.disabledColor, + ), + ); + expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor)); + }); + + testWidgetsWithLeakTracking('Material3 - Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + const Color activeBackgroundColor = Color(0xff123456); + const Color inactiveBackgroundColor = Color(0xff654321); + + Widget buildApp({ bool enabled = true }) { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Checkbox( + fillColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return activeBackgroundColor; + } + return inactiveBackgroundColor; + }), + value: false, + onChanged: enabled ? (bool? newValue) { } : null, + ), + ), + ), + ); + } + RenderBox getCheckboxRenderer() { + return tester.renderObject<RenderBox>(find.byType(Checkbox)); + } + + // Checkbox is unselected, so the default BorderSide appears and fillColor is checkbox's background color. await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( getCheckboxRenderer(), paints ..drrect( - color: theme.useMaterial3 ? theme.colorScheme.onSurfaceVariant : theme.unselectedWidgetColor, + color: theme.colorScheme.onSurfaceVariant, ), ); expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor)); @@ -1888,7 +2290,7 @@ void main() { getCheckboxRenderer(), paints ..drrect( - color: theme.useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : theme.disabledColor, + color: theme.colorScheme.onSurface.withOpacity(0.38), ), ); expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor)); diff --git a/packages/flutter/test/material/checkbox_theme_test.dart b/packages/flutter/test/material/checkbox_theme_test.dart index f14f975d8a80a..32d0e7c16b92b 100644 --- a/packages/flutter/test/material/checkbox_theme_test.dart +++ b/packages/flutter/test/material/checkbox_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('CheckboxThemeData copyWith, ==, hashCode basics', () { @@ -41,7 +40,7 @@ void main() { expect(theme.data.visualDensity, null); }); - testWidgets('Default CheckboxThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default CheckboxThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CheckboxThemeData().debugFillProperties(builder); @@ -53,7 +52,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('CheckboxThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckboxThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CheckboxThemeData( mouseCursor: MaterialStatePropertyAll<MouseCursor?>(SystemMouseCursors.click), @@ -84,7 +83,7 @@ void main() { ); }); - testWidgets('Checkbox is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox is themeable', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const MouseCursor mouseCursor = SystemMouseCursors.text; @@ -166,7 +165,7 @@ void main() { expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor)..path(color: focusedCheckColor)); }); - testWidgets('Checkbox properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const MouseCursor themeMouseCursor = SystemMouseCursors.click; @@ -264,7 +263,7 @@ void main() { expect(_getCheckboxMaterial(tester), paints..circle(color: focusColor, radius: splashRadius)); }); - testWidgets('Checkbox activeColor property is taken over the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox activeColor property is taken over the theme', (WidgetTester tester) async { const Color themeSelectedFillColor = Color(0xfffffff1); const Color themeDefaultFillColor = Color(0xfffffff0); const Color selectedFillColor = Color(0xfffffff6); @@ -302,7 +301,7 @@ void main() { expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor)); }); - testWidgets('Checkbox theme overlay color resolves in active/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checkbox theme overlay color resolves in active/pressed states', (WidgetTester tester) async { const Color activePressedOverlayColor = Color(0xFF000001); const Color inactivePressedOverlayColor = Color(0xFF000002); @@ -363,7 +362,7 @@ void main() { ); }); - testWidgets('Local CheckboxTheme can override global CheckboxTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Local CheckboxTheme can override global CheckboxTheme', (WidgetTester tester) async { const Color globalThemeFillColor = Color(0xfffffff1); const Color globalThemeCheckColor = Color(0xff000000); const Color localThemeFillColor = Color(0xffff0000); diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index baa306a29404a..8b9b923a1cbc0 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -7,8 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -139,7 +138,6 @@ Widget chipWithOptionalDeleteButton({ Key? labelKey, required bool deletable, TextDirection textDirection = TextDirection.ltr, - bool useDeleteButtonTooltip = true, String? chipTooltip, String? deleteButtonTooltipMessage, VoidCallback? onPressed = doNothing, @@ -155,7 +153,6 @@ Widget chipWithOptionalDeleteButton({ onPressed: onPressed, onDeleted: deletable ? doNothing : null, deleteIcon: Icon(Icons.close, key: deleteButtonKey), - useDeleteButtonTooltip: useDeleteButtonTooltip, deleteButtonTooltipMessage: deleteButtonTooltipMessage, label: Text( deletable @@ -218,7 +215,7 @@ Finder findTooltipContainer(String tooltipText) { } void main() { - testWidgets('M2 Chip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M2 Chip defaults', (WidgetTester tester) async { late TextTheme textTheme; Widget buildFrame(Brightness brightness) { @@ -295,7 +292,7 @@ void main() { expect(labelStyle.wordSpacing, textTheme.bodyLarge?.wordSpacing); }); - testWidgets('M3 Chip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 Chip defaults', (WidgetTester tester) async { late TextTheme textTheme; final ThemeData lightTheme = ThemeData.light(useMaterial3: true); final ThemeData darkTheme = ThemeData.dark(useMaterial3: true); @@ -376,7 +373,7 @@ void main() { expect(labelStyle.wordSpacing, textTheme.labelLarge?.wordSpacing); }); - testWidgets('Chip control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip control test', (WidgetTester tester) async { final FeedbackTester feedback = FeedbackTester(); final List<String> deletedChipLabels = <String>[]; await tester.pumpWidget( @@ -425,7 +422,7 @@ void main() { feedback.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'Chip does not constrain size of label widget if it does not exceed ' 'the available space', (WidgetTester tester) async { @@ -461,7 +458,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Chip constrains the size of the label widget when it exceeds the ' 'available space', (WidgetTester tester) async { @@ -469,7 +466,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Chip constrains the size of the label widget when it exceeds the ' 'available space and the avatar is present', (WidgetTester tester) async { @@ -480,7 +477,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Chip constrains the size of the label widget when it exceeds the ' 'available space and the delete icon is present', (WidgetTester tester) async { @@ -491,7 +488,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Chip constrains the size of the label widget when it exceeds the ' 'available space and both avatar and delete icons are present', (WidgetTester tester) async { @@ -503,7 +500,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Chip constrains the avatar, label, and delete icons to the bounds of ' 'the chip when it exceeds the available space', (WidgetTester tester) async { @@ -578,7 +575,7 @@ void main() { }, ); - testWidgets('Chip in row works ok', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip in row works ok', (WidgetTester tester) async { const TextStyle style = TextStyle(fontSize: 10.0); await tester.pumpWidget( wrapForChip( @@ -616,7 +613,7 @@ void main() { expect(tester.getSize(find.byType(Chip)), const Size(800.0, 48.0)); }); - testWidgets('Chip responds to materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip responds to materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( wrapForChip( useMaterial3: false, @@ -639,7 +636,7 @@ void main() { }, ); - testWidgets('delete button tap target is the right proportion of the chip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button tap target is the right proportion of the chip', (WidgetTester tester) async { final UniqueKey deleteKey = UniqueKey(); bool calledDelete = false; await tester.pumpWidget( @@ -657,12 +654,15 @@ void main() { ), ), ); - await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(24.0, 0.0)); + + // Test correct tap target size. + await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(18.0, 0.0)); // Half the width of the delete button + right label padding. await tester.pump(); expect(calledDelete, isTrue); calledDelete = false; - await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(25.0, 0.0)); + // Test incorrect tap target size. + await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(19.0, 0.0)); await tester.pump(); expect(calledDelete, isFalse); calledDelete = false; @@ -698,11 +698,13 @@ void main() { expect(calledDelete, isFalse); }); - testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip elements are ordered horizontally for locale', (WidgetTester tester) async { final UniqueKey iconKey = UniqueKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); final Widget test = Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Material( child: Chip( @@ -733,7 +735,7 @@ void main() { expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byKey(iconKey)).dx)); }); - testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip responds to textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( wrapForChip( useMaterial3: false, @@ -810,7 +812,7 @@ void main() { expect(tester.getSize(find.byType(Chip).last), const Size(132.0, 48.0)); }); - testWidgets('Labels can be non-text widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Labels can be non-text widgets', (WidgetTester tester) async { final Key keyA = GlobalKey(); final Key keyB = GlobalKey(); await tester.pumpWidget( @@ -837,7 +839,7 @@ void main() { expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 48.0)); }); - testWidgets('Avatars can be non-circle avatar widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Avatars can be non-circle avatar widgets', (WidgetTester tester) async { final Key keyA = GlobalKey(); await tester.pumpWidget( wrapForChip( @@ -855,7 +857,7 @@ void main() { expect(tester.getSize(find.byKey(keyA)), equals(const Size(20.0, 20.0))); }); - testWidgets('Delete icons can be non-icon widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete icons can be non-icon widgets', (WidgetTester tester) async { final Key keyA = GlobalKey(); await tester.pumpWidget( wrapForChip( @@ -877,11 +879,14 @@ void main() { testWidgets('Chip padding - LTR', (WidgetTester tester) async { final GlobalKey keyA = GlobalKey(); final GlobalKey keyB = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( wrapForChip( child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Material( child: Center( @@ -913,12 +918,16 @@ void main() { testWidgets('Chip padding - RTL', (WidgetTester tester) async { final GlobalKey keyA = GlobalKey(); final GlobalKey keyB = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( wrapForChip( textDirection: TextDirection.rtl, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Material( child: Center( @@ -948,7 +957,7 @@ void main() { expect(tester.getBottomRight(find.byType(Icon)), const Offset(361.0, 309.0)); }); - testWidgets('Avatar drawer works as expected on RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Avatar drawer works as expected on RawChip', (WidgetTester tester) async { final GlobalKey labelKey = GlobalKey(); Future<void> pushChip({ Widget? avatar }) async { return tester.pumpWidget( @@ -1061,7 +1070,7 @@ void main() { expect(find.byKey(avatarKey), findsNothing); }); - testWidgets('Delete button drawer works as expected on RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button drawer works as expected on RawChip', (WidgetTester tester) async { const Key labelKey = Key('label'); const Key deleteButtonKey = Key('delete'); bool wasDeleted = false; @@ -1178,7 +1187,7 @@ void main() { expect(find.byKey(deleteButtonKey), findsNothing); }); - testWidgets('Delete button takes up at most half of the chip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button takes up at most half of the chip', (WidgetTester tester) async { final UniqueKey chipKey = UniqueKey(); bool chipPressed = false; bool deletePressed = false; @@ -1214,7 +1223,7 @@ void main() { expect(deletePressed, isTrue); }); - testWidgets('Chip creates centered, unique ripple when label is tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip creates centered, unique ripple when label is tapped', (WidgetTester tester) async { final UniqueKey labelKey = UniqueKey(); final UniqueKey deleteButtonKey = UniqueKey(); @@ -1264,7 +1273,7 @@ void main() { await gesture.up(); }); - testWidgets('Delete button is focusable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button is focusable', (WidgetTester tester) async { final GlobalKey labelKey = GlobalKey(); final GlobalKey deleteButtonKey = GlobalKey(); @@ -1297,7 +1306,7 @@ void main() { expect(Focus.of(labelKey.currentContext!).hasPrimaryFocus, isTrue); }); - testWidgets('Delete button creates non-centered, unique ripple when tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button creates non-centered, unique ripple when tapped', (WidgetTester tester) async { final UniqueKey labelKey = UniqueKey(); final UniqueKey deleteButtonKey = UniqueKey(); @@ -1351,7 +1360,7 @@ void main() { await gesture.up(); }); - testWidgets('Delete button in a chip with null onPressed creates ripple when tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delete button in a chip with null onPressed creates ripple when tapped', (WidgetTester tester) async { final UniqueKey labelKey = UniqueKey(); final UniqueKey deleteButtonKey = UniqueKey(); @@ -1406,7 +1415,7 @@ void main() { await gesture.up(); }); - testWidgets('RTL delete button responds to tap on the left of the chip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RTL delete button responds to tap on the left of the chip', (WidgetTester tester) async { // Creates an RTL chip with a delete button. final UniqueKey labelKey = UniqueKey(); final UniqueKey deleteButtonKey = UniqueKey(); @@ -1436,7 +1445,7 @@ void main() { await gesture.up(); }); - testWidgets('Chip without delete button creates correct ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip without delete button creates correct ripple', (WidgetTester tester) async { // Creates a chip with a delete button. final UniqueKey labelKey = UniqueKey(); @@ -1491,7 +1500,7 @@ void main() { await gesture.up(); }); - testWidgets('Selection with avatar works as expected on RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selection with avatar works as expected on RawChip', (WidgetTester tester) async { bool selected = false; final UniqueKey labelKey = UniqueKey(); Future<void> pushChip({ Widget? avatar, bool selectable = false }) async { @@ -1572,7 +1581,7 @@ void main() { expect(getDeleteDrawerProgress(tester), equals(0.0)); }); - testWidgets('Selection without avatar works as expected on RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selection without avatar works as expected on RawChip', (WidgetTester tester) async { bool selected = false; final UniqueKey labelKey = UniqueKey(); Future<void> pushChip({ bool selectable = false }) async { @@ -1646,7 +1655,7 @@ void main() { expect(getDeleteDrawerProgress(tester), equals(0.0)); }); - testWidgets('Activation works as expected on RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activation works as expected on RawChip', (WidgetTester tester) async { bool selected = false; final UniqueKey labelKey = UniqueKey(); Future<void> pushChip({ Widget? avatar, bool selectable = false }) async { @@ -1703,7 +1712,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Chip uses ThemeData chip theme if present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses ThemeData chip theme if present', (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: false, platform: TargetPlatform.android, @@ -1734,7 +1743,7 @@ void main() { expect(materialBox, paints..rrect(color: chipTheme.disabledColor)); }); - testWidgets('Chip merges ChipThemeData label style with the provided label style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip merges ChipThemeData label style with the provided label style', (WidgetTester tester) async { // The font family should be preserved even if the chip overrides some label style properties final ThemeData theme = ThemeData( fontFamily: 'MyFont', @@ -1760,7 +1769,7 @@ void main() { expect(labelStyle.fontWeight, FontWeight.w200); }); - testWidgets('ChipTheme labelStyle with inherit:true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChipTheme labelStyle with inherit:true', (WidgetTester tester) async { Widget buildChip() { return wrapForChip( child: Theme( @@ -1780,7 +1789,7 @@ void main() { expect(labelStyle.height, 4); }); - testWidgets('Chip does not merge inherit:false label style with the theme label style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip does not merge inherit:false label style with the theme label style', (WidgetTester tester) async { Widget buildChip() { return wrapForChip( child: Theme( @@ -1804,7 +1813,7 @@ void main() { expect(labelStyle.fontWeight, FontWeight.w200); }); - testWidgets('Chip size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( wrapForChip( @@ -1964,7 +1973,7 @@ void main() { }); group('Chip semantics', () { - testWidgets('label only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label only', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(const MaterialApp( @@ -2012,7 +2021,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('delete', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -2072,7 +2081,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('with onPressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with onPressed', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -2126,7 +2135,7 @@ void main() { }); - testWidgets('with onSelected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with onSelected', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); bool selected = false; @@ -2236,7 +2245,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -2288,7 +2297,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('tapEnabled explicitly false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapEnabled explicitly false', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(const MaterialApp( @@ -2336,7 +2345,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('enabled when tapEnabled and canTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('enabled when tapEnabled and canTap', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); // These settings make a Chip which can be tapped, both in general and at this moment. @@ -2390,7 +2399,7 @@ void main() { semanticsTester.dispose(); }); - testWidgets('disabled when tapEnabled but not canTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled when tapEnabled but not canTap', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); // These settings make a Chip which _could_ be tapped, but not currently (ensures `canTap == false`). await tester.pumpWidget(const MaterialApp( @@ -2440,7 +2449,7 @@ void main() { }); }); - testWidgets('can be tapped outside of chip delete icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can be tapped outside of chip delete icon', (WidgetTester tester) async { bool deleted = false; await tester.pumpWidget( wrapForChip( @@ -2466,7 +2475,7 @@ void main() { expect(deleted, true); }); - testWidgets('Chips can be tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chips can be tapped', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -2481,7 +2490,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Chip elevation and shadow color work correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip elevation and shadow color work correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: false, platform: TargetPlatform.android, @@ -2532,7 +2541,7 @@ void main() { expect(material.shadowColor, Colors.blue); }); - testWidgets('can be tapped outside of chip body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can be tapped outside of chip body', (WidgetTester tester) async { bool pressed = false; await tester.pumpWidget( wrapForChip( @@ -2557,7 +2566,7 @@ void main() { expect(pressed, true); }); - testWidgets('is hitTestable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is hitTestable', (WidgetTester tester) async { await tester.pumpWidget( wrapForChip( child: InputChip( @@ -2578,7 +2587,7 @@ void main() { expect(materials.last.clipBehavior, clipBehavior); } - testWidgets('Chip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip clipBehavior properly passes through to the Material', (WidgetTester tester) async { const Text label = Text('label'); await tester.pumpWidget(wrapForChip(child: const Chip(label: label))); checkChipMaterialClipBehavior(tester, Clip.none); @@ -2615,7 +2624,7 @@ void main() { ])); }); - testWidgets('Chips should use InkWell instead of InkResponse.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chips should use InkWell instead of InkResponse.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28646 await tester.pumpWidget( MaterialApp( @@ -3039,7 +3048,7 @@ void main() { expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>()); }); - testWidgets('Chip defers to theme, if shape and side resolves to null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip defers to theme, if shape and side resolves to null', (WidgetTester tester) async { const OutlinedBorder themeShape = StadiumBorder(); const OutlinedBorder selectedShape = RoundedRectangleBorder(); const BorderSide themeBorderSide = BorderSide(color: Color(0x00000001)); @@ -3091,7 +3100,7 @@ void main() { expect(find.byType(RawChip), paints..rect()..drrect(color: selectedBorderSide.color)); }); - testWidgets('Chip responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key textKey = Key('test text'); const Key iconKey = Key('test icon'); @@ -3197,32 +3206,7 @@ void main() { expect(box.size, equals(const Size(128, 24.0 + 16.0))); }); - testWidgets('Chip delete button tooltip can be disabled using useDeleteButtonTooltip', (WidgetTester tester) async { - await tester.pumpWidget( - chipWithOptionalDeleteButton( - deletable: true, - useDeleteButtonTooltip: false, - ), - ); - - // Tap at the delete icon of the chip, which is at the right side of the - // chip - final Offset topRightOfInkwell = tester.getTopLeft(find.byType(InkWell).first); - final Offset tapLocationOfDeleteButton = topRightOfInkwell + const Offset(8, 8); - final TestGesture tapGesture = await tester.startGesture(tapLocationOfDeleteButton); - - await tester.pump(); - - // Wait for some more time while pressing and holding the delete button - await tester.pumpAndSettle(); - - // There should be no delete button tooltip - expect(findTooltipContainer('Delete'), findsNothing); - - await tapGesture.up(); - }); - - testWidgets('Chip delete button tooltip is disabled if deleteButtonTooltipMessage is empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip delete button tooltip is disabled if deleteButtonTooltipMessage is empty', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( chipWithOptionalDeleteButton( @@ -3247,7 +3231,7 @@ void main() { expect(findTooltipContainer(''), findsNothing); }); - testWidgets('Disabling delete button tooltip does not disable chip tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabling delete button tooltip does not disable chip tooltip', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( chipWithOptionalDeleteButton( @@ -3275,7 +3259,7 @@ void main() { expect(findTooltipContainer('Chip Tooltip'), findsOneWidget); }); - testWidgets('Triggering delete button tooltip does not trigger Chip tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Triggering delete button tooltip does not trigger Chip tooltip', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( chipWithOptionalDeleteButton( @@ -3302,7 +3286,7 @@ void main() { expect(findTooltipContainer('Delete'), findsOneWidget); }); - testWidgets('intrinsicHeight implementation meets constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('intrinsicHeight implementation meets constraints', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/49478. await tester.pumpWidget(wrapForChip( child: const Chip( @@ -3314,7 +3298,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Chip background color and shape are drawn on Ink', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip background color and shape are drawn on Ink', (WidgetTester tester) async { const Color backgroundColor = Color(0xff00ff00); const OutlinedBorder shape = ContinuousRectangleBorder(); @@ -3364,7 +3348,7 @@ void main() { ); }); - testWidgets('RawChip.color resolves material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawChip.color resolves material states', (WidgetTester tester) async { const Color disabledSelectedColor = Color(0xffffff00); const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); @@ -3420,7 +3404,7 @@ void main() { expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); }); - testWidgets('RawChip uses provided state color properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawChip uses provided state color properties', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); const Color selectedColor = Color(0xffff0000); @@ -3459,6 +3443,127 @@ void main() { expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); }); + testWidgets('Delete button tap target area does not include label', (WidgetTester tester) async { + bool calledDelete = false; + await tester.pumpWidget( + wrapForChip( + child: Column( + children: <Widget>[ + Chip( + label: const Text('Chip'), + onDeleted: () { + calledDelete = true; + }, + ), + ], + ), + ), + ); + + // Tap on the delete button. + await tester.tapAt(tester.getCenter(find.byType(Icon))); + await tester.pump(); + expect(calledDelete, isTrue); + calledDelete = false; + + final Offset labelCenter = tester.getCenter(find.text('Chip')); + + // Tap on the label. + await tester.tapAt(labelCenter); + await tester.pump(); + expect(calledDelete, isFalse); + + // Tap before end of the label. + final Size labelSize = tester.getSize(find.text('Chip')); + await tester.tapAt(Offset(labelCenter.dx + (labelSize.width / 2) - 1, labelCenter.dy)); + await tester.pump(); + expect(calledDelete, isFalse); + + // Tap after end of the label. + await tester.tapAt(Offset(labelCenter.dx + (labelSize.width / 2) + 0.01, labelCenter.dy)); + await tester.pump(); + expect(calledDelete, isTrue); + }); + + // This is a regression test for https://github.com/flutter/flutter/pull/133615. + testWidgets('Material3 - Custom shape without provided side uses default side', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: Center( + child: RawChip( + // No side provided. + shape: StadiumBorder(), + label: Text('RawChip'), + ), + ), + ), + ), + ); + + // Chip should have the default side. + expect( + getMaterial(tester).shape, + StadiumBorder(side: BorderSide(color: theme.colorScheme.outline)), + ); + }); + + testWidgets("Material3 - RawChip.shape's side is used when provided", (WidgetTester tester) async { + Widget buildChip({ OutlinedBorder? shape, BorderSide? side }) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: Center( + child: RawChip( + shape: shape, + side: side, + label: const Text('RawChip'), + ), + ), + ), + ); + } + + // Test [RawChip.shape] with a side. + await tester.pumpWidget(buildChip( + shape: const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + )), + ); + + // Chip should have the provided shape and the side from [RawChip.shape]. + expect( + getMaterial(tester).shape, + const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + ); + + // Test [RawChip.shape] with a side and [RawChip.side]. + await tester.pumpWidget(buildChip( + shape: const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + side: const BorderSide(color: Color(0xfffff000))), + ); + await tester.pumpAndSettle(); + + // Chip use shape from [RawChip.shape] and the side from [RawChip.side]. + // [RawChip.shape]'s side should be ignored. + expect( + getMaterial(tester).shape, + const RoundedRectangleBorder( + side: BorderSide(color: Color(0xfffff000)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + ); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests diff --git a/packages/flutter/test/material/chip_theme_test.dart b/packages/flutter/test/material/chip_theme_test.dart index 85e901b2a1064..4ff0c5c31773e 100644 --- a/packages/flutter/test/material/chip_theme_test.dart +++ b/packages/flutter/test/material/chip_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; RenderBox getMaterialBox(WidgetTester tester) { return tester.firstRenderObject<RenderBox>( @@ -70,9 +69,10 @@ void main() { expect(themeData.brightness, null); expect(themeData.elevation, null); expect(themeData.pressElevation, null); + expect(themeData.iconTheme, null); }); - testWidgets('Default ChipThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ChipThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ChipThemeData().debugFillProperties(builder); @@ -84,7 +84,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ChipThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChipThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ChipThemeData( color: MaterialStatePropertyAll<Color>(Color(0xfffffff0)), @@ -107,6 +107,7 @@ void main() { brightness: Brightness.dark, elevation: 5, pressElevation: 6, + iconTheme: IconThemeData(color: Color(0xffffff10)), ).debugFillProperties(builder); final List<String> description = builder.properties @@ -114,7 +115,7 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description, <String>[ + expect(description, equalsIgnoringHashCodes(<String>[ 'color: MaterialStatePropertyAll(Color(0xfffffff0))', 'backgroundColor: Color(0xfffffff1)', 'deleteIconColor: Color(0xfffffff2)', @@ -135,10 +136,11 @@ void main() { 'brightness: dark', 'elevation: 5.0', 'pressElevation: 6.0', - ]); + 'iconTheme: IconThemeData#00000(color: Color(0xffffff10))' + ])); }); - testWidgets('Chip uses ThemeData chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses ThemeData chip theme', (WidgetTester tester) async { const ChipThemeData chipTheme = ChipThemeData( backgroundColor: Color(0xff112233), elevation: 4, @@ -175,7 +177,7 @@ void main() { expect(getLabelStyle(tester).style.fontSize, 32); }); - testWidgets('Chip uses ChipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses ChipTheme', (WidgetTester tester) async { const ChipThemeData chipTheme = ChipThemeData( backgroundColor: Color(0xff112233), elevation: 4, @@ -228,7 +230,7 @@ void main() { expect(getLabelStyle(tester).style.fontSize, 32); }); - testWidgets('Chip uses constructor parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses constructor parameters', (WidgetTester tester) async { const ChipThemeData shadowedChipTheme = ChipThemeData( backgroundColor: Color(0xff112233), elevation: 4, @@ -281,7 +283,7 @@ void main() { expect(getLabelStyle(tester).style.fontSize, 32); }); - testWidgets('ChipTheme.fromDefaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChipTheme.fromDefaults', (WidgetTester tester) async { const TextStyle labelStyle = TextStyle(); ChipThemeData chipTheme = ChipThemeData.fromDefaults( brightness: Brightness.light, @@ -333,7 +335,7 @@ void main() { expect(chipTheme.pressElevation, 8.0); }); - testWidgets('ChipThemeData generates correct opacities for defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChipThemeData generates correct opacities for defaults', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); final TextStyle customStyle = ThemeData.fallback().textTheme.bodyLarge!.copyWith(color: customColor2); @@ -396,7 +398,7 @@ void main() { expect(customTheme.brightness, equals(Brightness.light)); }); - testWidgets('ChipThemeData lerps correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChipThemeData lerps correctly', (WidgetTester tester) async { final ChipThemeData chipThemeBlack = ChipThemeData.fromDefaults( secondaryColor: Colors.black, brightness: Brightness.dark, @@ -544,7 +546,7 @@ void main() { expect(lerp.iconTheme, isNull); }); - testWidgets('Chip uses stateful color from chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses stateful color from chip theme', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -638,9 +640,11 @@ void main() { await tester.pumpWidget(chipWidget(enabled: false)); await tester.pumpAndSettle(); expect(textColor(), disabledColor); + + focusNode.dispose(); }); - testWidgets('Chip uses stateful border side from resolveWith pattern', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses stateful border side from resolveWith pattern', (WidgetTester tester) async { const Color selectedColor = Color(0x00000001); const Color defaultColor = Color(0x00000002); @@ -681,7 +685,7 @@ void main() { expect(find.byType(RawChip), paints..rrect()..rrect(color: selectedColor)); }); - testWidgets('Chip uses stateful border side from chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses stateful border side from chip theme', (WidgetTester tester) async { const Color selectedColor = Color(0x00000001); const Color defaultColor = Color(0x00000002); @@ -723,7 +727,7 @@ void main() { expect(find.byType(RawChip), paints..rrect()..rrect(color: selectedColor)); }); - testWidgets('Chip uses stateful shape from chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip uses stateful shape from chip theme', (WidgetTester tester) async { OutlinedBorder? getShape(Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return const RoundedRectangleBorder(); @@ -763,7 +767,7 @@ void main() { expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>()); }); - testWidgets('RawChip uses material state color from ChipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawChip uses material state color from ChipTheme', (WidgetTester tester) async { const Color disabledSelectedColor = Color(0xffffff00); const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); @@ -827,7 +831,7 @@ void main() { expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); }); - testWidgets('RawChip uses state colors from ChipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawChip uses state colors from ChipTheme', (WidgetTester tester) async { const ChipThemeData chipTheme = ChipThemeData( disabledColor: Color(0xadfefafe), backgroundColor: Color(0xcafefeed), @@ -867,6 +871,121 @@ void main() { // Enabled & selected chip should have the provided selectedColor. expect(getMaterialBox(tester), paints..rrect(color: chipTheme.selectedColor)); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/119163. + testWidgetsWithLeakTracking('RawChip respects checkmark properties from ChipTheme', (WidgetTester tester) async { + Widget buildRawChip({ChipThemeData? chipTheme}) { + return MaterialApp( + theme: ThemeData.light(useMaterial3: false).copyWith( + chipTheme: chipTheme, + ), + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: RawChip( + selected: true, + label: const SizedBox(width: 100, height: 100), + onSelected: (bool newValue) { }, + ), + ), + ), + ), + ); + } + + // Test that the checkmark is painted. + await tester.pumpWidget(buildRawChip( + chipTheme: const ChipThemeData( + checkmarkColor: Color(0xffff0000), + ), + )); + + RenderBox materialBox = getMaterialBox(tester); + expect( + materialBox, + paints..path( + color: const Color(0xffff0000), + style: PaintingStyle.stroke, + ), + ); + + // Test that the checkmark is not painted when ChipThemeData.showCheckmark is false. + await tester.pumpWidget(buildRawChip( + chipTheme: const ChipThemeData( + showCheckmark: false, + checkmarkColor: Color(0xffff0000), + ), + )); + await tester.pumpAndSettle(); + + materialBox = getMaterialBox(tester); + expect( + materialBox, + isNot(paints..path( + color: const Color(0xffff0000), + style: PaintingStyle.stroke, + )), + ); + }); + + testWidgets("Material3 - RawChip.shape's side is used when provided", (WidgetTester tester) async { + Widget buildChip({ OutlinedBorder? shape, BorderSide? side }) { + return MaterialApp( + theme: ThemeData( + useMaterial3: true, + chipTheme: ChipThemeData( + shape: shape, + side: side, + ), + ), + home: const Material( + child: Center( + child: RawChip( + label: Text('RawChip'), + ), + ), + ), + ); + } + + // Test [RawChip.shape] with a side. + await tester.pumpWidget(buildChip( + shape: const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + )), + ); + + // Chip should have the provided shape and the side from [RawChip.shape]. + expect( + getMaterial(tester).shape, + const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + ); + + // Test [RawChip.shape] with a side and [RawChip.side]. + await tester.pumpWidget(buildChip( + shape: const RoundedRectangleBorder( + side: BorderSide(color: Color(0xffff00ff)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + side: const BorderSide(color: Color(0xfffff000))), + ); + await tester.pumpAndSettle(); + + // Chip use shape from [RawChip.shape] and the side from [RawChip.side]. + // [RawChip.shape]'s side should be ignored. + expect( + getMaterial(tester).shape, + const RoundedRectangleBorder( + side: BorderSide(color: Color(0xfffff000)), + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + ); + }); } class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder { diff --git a/packages/flutter/test/material/choice_chip_test.dart b/packages/flutter/test/material/choice_chip_test.dart index 187ec8fd2cef9..2f97610fd203c 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; RenderBox getMaterialBox(WidgetTester tester, Finder type) { return tester.firstRenderObject<RenderBox>( @@ -64,7 +63,7 @@ void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { } void main() { - testWidgets('ChoiceChip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'choice chip'; @@ -85,7 +84,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(ChoiceChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(ChoiceChip)), + within(distance: 0.01, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, label).style.color!.value, @@ -196,7 +198,7 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); - testWidgets('ChoiceChip.elevated defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip.elevated defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'choice chip'; @@ -217,7 +219,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(ChoiceChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(ChoiceChip)), + within(distance: 0.01, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, label).style.color!.value, @@ -328,7 +333,7 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); - testWidgets('ChoiceChip.color resolves material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip.color resolves material states', (WidgetTester tester) async { const Color disabledSelectedColor = Color(0xffffff00); const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); @@ -427,7 +432,7 @@ void main() { ); }); - testWidgets('ChoiceChip uses provided state color properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip uses provided state color properties', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); const Color selectedColor = Color(0xffff0000); @@ -502,7 +507,7 @@ void main() { ); }); - testWidgets('ChoiceChip can be tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -518,7 +523,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('ChoiceChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { const Text label = Text('label'); await tester.pumpWidget(wrapForChip(child: const ChoiceChip(label: label, selected: false))); checkChipMaterialClipBehavior(tester, Clip.none); @@ -527,7 +532,7 @@ void main() { checkChipMaterialClipBehavior(tester, Clip.antiAlias); }); - testWidgets('ChoiceChip passes iconTheme property to RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip passes iconTheme property to RawChip', (WidgetTester tester) async { const IconThemeData iconTheme = IconThemeData(color: Colors.red); await tester.pumpWidget(wrapForChip( child: const ChoiceChip( @@ -539,7 +544,7 @@ void main() { expect(rawChip.iconTheme, iconTheme); }); - testWidgets('ChoiceChip passes showCheckmark from ChipTheme to RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip passes showCheckmark from ChipTheme to RawChip', (WidgetTester tester) async { const bool showCheckmark = false; await tester.pumpWidget(wrapForChip( child: const ChipTheme( @@ -555,7 +560,7 @@ void main() { expect(rawChip.showCheckmark, showCheckmark); }); - testWidgets('ChoiceChip passes checkmark properties to RawChip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip passes checkmark properties to RawChip', (WidgetTester tester) async { const bool showCheckmark = false; const Color checkmarkColor = Color(0xff0000ff); await tester.pumpWidget(wrapForChip( @@ -575,7 +580,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('ChoiceChip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChoiceChip defaults', (WidgetTester tester) async { Widget buildFrame(Brightness brightness) { return MaterialApp( theme: ThemeData(useMaterial3: false, brightness: brightness), diff --git a/packages/flutter/test/material/circle_avatar_test.dart b/packages/flutter/test/material/circle_avatar_test.dart index bf4ffe919d00b..8487f124cf343 100644 --- a/packages/flutter/test/material/circle_avatar_test.dart +++ b/packages/flutter/test/material/circle_avatar_test.dart @@ -12,12 +12,12 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; import '../painting/mocks_for_image_cache.dart'; void main() { - testWidgets('CircleAvatar with dark background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar with dark background color', (WidgetTester tester) async { final Color backgroundColor = Colors.blue.shade900; await tester.pumpWidget( wrap( @@ -39,7 +39,7 @@ void main() { expect(paragraph.text.style!.color, equals(ThemeData.fallback().primaryColorLight)); }); - testWidgets('CircleAvatar with light background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar with light background color', (WidgetTester tester) async { final Color backgroundColor = Colors.blue.shade100; await tester.pumpWidget( wrap( @@ -61,7 +61,7 @@ void main() { expect(paragraph.text.style!.color, equals(ThemeData.fallback().primaryColorDark)); }); - testWidgets('CircleAvatar with image background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar with image background', (WidgetTester tester) async { await tester.pumpWidget( wrap( child: CircleAvatar( @@ -78,7 +78,7 @@ void main() { expect(decoration.image!.fit, equals(BoxFit.cover)); }); - testWidgets('CircleAvatar with image foreground', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar with image foreground', (WidgetTester tester) async { await tester.pumpWidget( wrap( child: CircleAvatar( @@ -95,7 +95,7 @@ void main() { expect(decoration.image!.fit, equals(BoxFit.cover)); }); - testWidgets('CircleAvatar backgroundImage is used as a fallback for foregroundImage', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar backgroundImage is used as a fallback for foregroundImage', (WidgetTester tester) async { final ErrorImageProvider errorImage = ErrorImageProvider(); bool caughtForegroundImageError = false; await tester.pumpWidget( @@ -123,7 +123,7 @@ void main() { ); }); - testWidgets('CircleAvatar with foreground color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar with foreground color', (WidgetTester tester) async { final Color foregroundColor = Colors.red.shade100; await tester.pumpWidget( wrap( @@ -146,7 +146,7 @@ void main() { expect(paragraph.text.style!.color, equals(foregroundColor)); }); - testWidgets('CircleAvatar default colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar default colors', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( wrap( @@ -168,7 +168,7 @@ void main() { expect(paragraph.text.style!.color, equals(theme.colorScheme.onPrimaryContainer)); }); - testWidgets('CircleAvatar text does not expand with textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar text does not expand with textScaleFactor', (WidgetTester tester) async { final Color foregroundColor = Colors.red.shade100; await tester.pumpWidget( wrap( @@ -212,7 +212,7 @@ void main() { expect(tester.getSize(find.text('Z')), equals(const Size(16.0, 16.0))); }); - testWidgets('CircleAvatar respects minRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar respects minRadius', (WidgetTester tester) async { final Color backgroundColor = Colors.blue.shade900; await tester.pumpWidget( wrap( @@ -236,7 +236,7 @@ void main() { expect(paragraph.text.style!.color, equals(ThemeData.fallback().primaryColorLight)); }); - testWidgets('CircleAvatar respects maxRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar respects maxRadius', (WidgetTester tester) async { final Color backgroundColor = Colors.blue.shade900; await tester.pumpWidget( wrap( @@ -258,7 +258,7 @@ void main() { expect(paragraph.text.style!.color, equals(ThemeData.fallback().primaryColorLight)); }); - testWidgets('CircleAvatar respects setting both minRadius and maxRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar respects setting both minRadius and maxRadius', (WidgetTester tester) async { final Color backgroundColor = Colors.blue.shade900; await tester.pumpWidget( wrap( @@ -286,7 +286,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('CircleAvatar default colors with light theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar default colors with light theme', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false, primaryColor: Colors.grey.shade100); await tester.pumpWidget( wrap( @@ -308,7 +308,7 @@ void main() { expect(paragraph.text.style!.color, equals(theme.primaryTextTheme.titleLarge!.color)); }); - testWidgets('CircleAvatar default colors with dark theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircleAvatar default colors with dark theme', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false, primaryColor: Colors.grey.shade800); await tester.pumpWidget( wrap( diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index 570cb8645316e..d1e05c17f03be 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; @@ -443,7 +444,7 @@ void main() { ); }); - testWidgets('generated scheme "on" colors meet a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('generated scheme "on" colors meet a11y contrast guidelines', (WidgetTester tester) async { final ColorScheme colors = ColorScheme.fromSeed(seedColor: Colors.teal); Widget label(String text, Color textColor, Color background) { diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 4e9d0af6e9f12..fc738b73a63e3 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -11,13 +11,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix3; -import '../rendering/mock_canvas.dart'; import 'data_table_test_utils.dart'; void main() { - testWidgets('DataTable control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable control test', (WidgetTester tester) async { final List<String> log = <String>[]; Widget buildTable({ int? sortColumnIndex, bool sortAscending = true }) { @@ -160,7 +160,7 @@ void main() { log.clear(); }); - testWidgets('DataTable control test - tristate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable control test - tristate', (WidgetTester tester) async { final List<String> log = <String>[]; const int numItems = 3; Widget buildTable(List<bool> selected, {int? disabledIndex}) { @@ -230,7 +230,7 @@ void main() { log.clear(); }); - testWidgets('DataTable control test - no checkboxes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable control test - no checkboxes', (WidgetTester tester) async { final List<String> log = <String>[]; Widget buildTable({ bool checkboxes = false }) { @@ -296,7 +296,7 @@ void main() { log.clear(); }); - testWidgets('DataTable overflow test - header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable overflow test - header', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -329,7 +329,7 @@ void main() { expect(tester.takeException(), isNull); // column overflows table, but text doesn't overflow cell }); - testWidgets('DataTable overflow test - header with spaces', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable overflow test - header with spaces', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -357,7 +357,7 @@ void main() { expect(tester.takeException(), isNull); // column overflows table, but text doesn't overflow cell }, skip: true); // https://github.com/flutter/flutter/issues/13512 - testWidgets('DataTable overflow test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable overflow test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -385,7 +385,7 @@ void main() { expect(tester.takeException(), isNull); // cell overflows table, but text doesn't overflow cell }); - testWidgets('DataTable overflow test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable overflow test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -413,7 +413,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('DataTable column onSort test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable column onSort test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -441,7 +441,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('DataTable sort indicator orientation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable sort indicator orientation', (WidgetTester tester) async { Widget buildTable({ bool sortAscending = true }) { return DataTable( sortColumnIndex: 0, @@ -469,11 +469,11 @@ void main() { await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); - // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), @@ -493,7 +493,7 @@ void main() { ); }); - testWidgets('DataTable sort indicator orientation does not change on state update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable sort indicator orientation does not change on state update', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/43724 Widget buildTable({String title = 'Name1'}) { return DataTable( @@ -521,11 +521,11 @@ void main() { await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); - // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), @@ -545,7 +545,7 @@ void main() { ); }); - testWidgets('DataTable sort indicator orientation does not change on state update - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable sort indicator orientation does not change on state update - reverse', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/43724 Widget buildTable({String title = 'Name1'}) { return DataTable( @@ -574,11 +574,11 @@ void main() { await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); - // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), @@ -598,7 +598,7 @@ void main() { ); }); - testWidgets('DataTable row onSelectChanged test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable row onSelectChanged test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -626,7 +626,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('DataTable custom row height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable custom row height', (WidgetTester tester) async { Widget buildCustomTable({ int? sortColumnIndex, bool sortAscending = true, @@ -739,7 +739,7 @@ void main() { expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, greaterThan(0.0)); }); - testWidgets('DataTable custom row height one row taller than others', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable custom row height one row taller than others', (WidgetTester tester) async { const String multilineText = 'Line one.\nLine two.\nLine three.\nLine four.'; Widget buildCustomTable({ @@ -780,7 +780,7 @@ void main() { expect(multilineRowHeight, greaterThan(singleLineRowHeight)); }); - testWidgets('DataTable custom row height - separate test for deprecated dataRowHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable custom row height - separate test for deprecated dataRowHeight', (WidgetTester tester) async { Widget buildCustomTable({ double dataRowHeight = 48.0, }) { @@ -830,7 +830,7 @@ void main() { expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, 30.0); }); - testWidgets('DataTable custom horizontal padding - checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable custom horizontal padding - checkbox', (WidgetTester tester) async { const double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; @@ -1053,7 +1053,7 @@ void main() { ); }); - testWidgets('DataTable custom horizontal padding - no checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable custom horizontal padding - no checkbox', (WidgetTester tester) async { const double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; @@ -1247,7 +1247,7 @@ void main() { ); }); - testWidgets('DataTable set border width test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable set border width test', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), @@ -1299,7 +1299,7 @@ void main() { expect(boxDecoration.border!.top.width, thickness); }); - testWidgets('DataTable set show bottom border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable set show bottom border', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), @@ -1348,7 +1348,7 @@ void main() { expect(boxDecoration.border!.bottom.width, 0.0); }); - testWidgets('DataTable column heading cell - with and without sorting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable column heading cell - with and without sorting', (WidgetTester tester) async { Widget buildTable({ int? sortColumnIndex, bool sortEnabled = true }) { return DataTable( sortColumnIndex: sortColumnIndex, @@ -1412,7 +1412,7 @@ void main() { } }); - testWidgets('DataTable correctly renders with a mouse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable correctly renders with a mouse', (WidgetTester tester) async { // Regression test for a bug described in // https://github.com/flutter/flutter/pull/43735#issuecomment-589459947 // Filed at https://github.com/flutter/flutter/issues/51152 @@ -1460,7 +1460,7 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 1)); }); - testWidgets('DataRow renders default selected row colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow renders default selected row colors', (WidgetTester tester) async { final ThemeData themeData = ThemeData.light(); Widget buildTable({bool selected = false}) { return MaterialApp( @@ -1502,7 +1502,7 @@ void main() { ); }); - testWidgets('DataRow renders checkbox with colors from CheckboxTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow renders checkbox with colors from CheckboxTheme', (WidgetTester tester) async { const Color fillColor = Color(0xFF00FF00); const Color checkColor = Color(0xFF0000FF); @@ -1547,7 +1547,7 @@ void main() { ); }); - testWidgets('DataRow renders custom colors when selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow renders custom colors when selected', (WidgetTester tester) async { const Color selectedColor = Colors.green; const Color defaultColor = Colors.red; @@ -1596,7 +1596,7 @@ void main() { expect(lastTableRowBoxDecoration().color, selectedColor); }); - testWidgets('DataRow renders custom colors when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow renders custom colors when disabled', (WidgetTester tester) async { const Color disabledColor = Colors.grey; const Color defaultColor = Colors.red; @@ -1651,7 +1651,7 @@ void main() { expect(lastTableRowBoxDecoration().color, disabledColor); }); - testWidgets('DataRow renders custom colors when pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow renders custom colors when pressed', (WidgetTester tester) async { const Color pressedColor = Color(0xff4caf50); Widget buildTable() { return DataTable( @@ -1691,7 +1691,7 @@ void main() { await gesture.up(); }); - testWidgets('DataTable can render inside an AlertDialog', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable can render inside an AlertDialog', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1713,7 +1713,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('DataTable renders with border and background decoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable renders with border and background decoration', (WidgetTester tester) async { const double width = 800; const double height = 600; const double borderHorizontal = 5.0; @@ -1750,7 +1750,7 @@ void main() { ); expect( find.ancestor(of: find.byType(Table), matching: find.byType(Container)), - paints..drrect(color: borderColor), + paints..path(color: borderColor), ); expect( tester.getTopLeft(find.byType(Table)), @@ -1762,7 +1762,7 @@ void main() { ); }); - testWidgets('checkboxHorizontalMargin properly applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('checkboxHorizontalMargin properly applied', (WidgetTester tester) async { const double customCheckboxHorizontalMargin = 15.0; const double customHorizontalMargin = 10.0; Finder cellContent; @@ -1851,7 +1851,7 @@ void main() { ); }); - testWidgets('DataRow is disabled when onSelectChanged is not set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow is disabled when onSelectChanged is not set', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1886,7 +1886,7 @@ void main() { expect(find.widgetWithText(TableRowInkWell, 'GitHub'), findsNothing); }); - testWidgets('DataTable set interior border test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable set interior border test', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), @@ -1952,7 +1952,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/100952 - testWidgets('Do not crashes when paint borders in a narrow space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crashes when paint borders in a narrow space', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), @@ -1989,7 +1989,7 @@ void main() { }); - testWidgets('DataTable clip behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable clip behavior', (WidgetTester tester) async { const Color selectedColor = Colors.green; const Color defaultColor = Colors.red; const BorderRadius borderRadius = BorderRadius.all(Radius.circular(30)); @@ -2038,7 +2038,7 @@ void main() { expect(material.borderRadius, borderRadius); }); - testWidgets('DataTable dataRowMinHeight smaller or equal dataRowMaxHeight validation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable dataRowMinHeight smaller or equal dataRowMaxHeight validation', (WidgetTester tester) async { DataTable createDataTable() => DataTable( columns: const <DataColumn>[DataColumn(label: Text('Column1'))], @@ -2051,7 +2051,7 @@ void main() { e.toString().contains('dataRowMaxHeight >= dataRowMinHeight')))); }); - testWidgets('DataTable dataRowHeight is not used together with dataRowMinHeight or dataRowMaxHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable dataRowHeight is not used together with dataRowMinHeight or dataRowMaxHeight', (WidgetTester tester) async { DataTable createDataTable({double? dataRowHeight, double? dataRowMinHeight, double? dataRowMaxHeight}) => DataTable( columns: const <DataColumn>[DataColumn(label: Text('Column1'))], @@ -2072,7 +2072,7 @@ void main() { }); group('TableRowInkWell', () { - testWidgets('can handle secondary taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can handle secondary taps', (WidgetTester tester) async { bool secondaryTapped = false; bool secondaryTappedDown = false; @@ -2116,7 +2116,7 @@ void main() { }); }); - testWidgets('Heading cell cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heading cell cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2177,7 +2177,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); }); - testWidgets('DataRow cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataRow cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2238,7 +2238,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy); }); - testWidgets("DataRow cursor doesn't update checkbox cursor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("DataRow cursor doesn't update checkbox cursor", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2279,4 +2279,46 @@ void main() { // Test that cursor is updated for the row. expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/114470. + testWidgetsWithLeakTracking('DataTable text styles are merged with default text style', (WidgetTester tester) async { + late DefaultTextStyle defaultTextStyle; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + defaultTextStyle = DefaultTextStyle.of(context); + return DataTable( + headingTextStyle: const TextStyle(), + dataTextStyle: const TextStyle(), + columns: const <DataColumn>[ + DataColumn(label: Text('Header 1')), + DataColumn(label: Text('Header 2')), + ], + rows: const <DataRow>[ + DataRow( + cells: <DataCell>[ + DataCell(Text('Data 1')), + DataCell(Text('Data 2')), + ], + ), + ], + ); + } + ), + ), + ), + ); + + final TextStyle? headingTextStyle = _getTextRenderObject(tester, 'Header 1').text.style; + expect(headingTextStyle, defaultTextStyle.style); + + final TextStyle? dataTextStyle = _getTextRenderObject(tester, 'Data 1').text.style; + expect(dataTextStyle, defaultTextStyle.style); + }); +} + +RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { + return tester.renderObject(find.text(text)); } diff --git a/packages/flutter/test/material/data_table_theme_test.dart b/packages/flutter/test/material/data_table_theme_test.dart index e08169cdd4651..ea5d19235ce5b 100644 --- a/packages/flutter/test/material/data_table_theme_test.dart +++ b/packages/flutter/test/material/data_table_theme_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('DataTableThemeData copyWith, ==, hashCode basics', () { @@ -64,7 +65,7 @@ void main() { expect(theme.data.dataRowCursor, null); }); - testWidgets('Default DataTableThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default DataTableThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DataTableThemeData().debugFillProperties(builder); @@ -76,7 +77,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('DataTableThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTableThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); DataTableThemeData( decoration: const BoxDecoration(color: Color(0xfffffff0)), @@ -120,7 +121,7 @@ void main() { expect(description[13], 'dataRowCursor: MaterialStatePropertyAll(SystemMouseCursor(forbidden))'); }); - testWidgets('DataTable is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable is themeable', (WidgetTester tester) async { const BoxDecoration decoration = BoxDecoration(color: Color(0xfffffff0)); const MaterialStateProperty<Color> dataRowColor = MaterialStatePropertyAll<Color>(Color(0xfffffff1)); const double minMaxDataRowHeight = 41.0; @@ -207,7 +208,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); }); - testWidgets('DataTable is themeable - separate test for deprecated dataRowHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable is themeable - separate test for deprecated dataRowHeight', (WidgetTester tester) async { const double dataRowHeight = 51.0; await tester.pumpWidget( @@ -241,7 +242,7 @@ void main() { expect(tester.getSize(_findFirstContainerFor('Data')).height, dataRowHeight); }); - testWidgets('DataTable properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable properties are taken over the theme values', (WidgetTester tester) async { const BoxDecoration themeDecoration = BoxDecoration(color: Color(0xfffffff1)); const MaterialStateProperty<Color> themeDataRowColor = MaterialStatePropertyAll<Color>(Color(0xfffffff0)); const double minMaxThemeDataRowHeight = 50.0; @@ -354,7 +355,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), dataRowCursor.resolve(<MaterialState>{})); }); - testWidgets('DataTable properties are taken over the theme values - separate test for deprecated dataRowHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DataTable properties are taken over the theme values - separate test for deprecated dataRowHeight', (WidgetTester tester) async { const double themeDataRowHeight = 50.0; const double dataRowHeight = 51.0; @@ -390,7 +391,7 @@ void main() { expect(tester.getSize(_findFirstContainerFor('Data')).height, dataRowHeight); }); - testWidgets('Local DataTableTheme can override global DataTableTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Local DataTableTheme can override global DataTableTheme', (WidgetTester tester) async { const BoxDecoration globalThemeDecoration = BoxDecoration(color: Color(0xfffffff1)); const MaterialStateProperty<Color> globalThemeDataRowColor = MaterialStatePropertyAll<Color>(Color(0xfffffff0)); const double minMaxGlobalThemeDataRowHeight = 50.0; @@ -507,7 +508,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), localDataRowCursor.resolve(<MaterialState>{})); }); - testWidgets('Local DataTableTheme can override global DataTableTheme - separate test for deprecated dataRowHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Local DataTableTheme can override global DataTableTheme - separate test for deprecated dataRowHeight', (WidgetTester tester) async { const double globalThemeDataRowHeight = 50.0; const double localThemeDataRowHeight = 51.0; diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index 514a5f4c28b43..10c2a74f602e5 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -8,15 +8,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late DateTime firstDate; late DateTime lastDate; - late DateTime initialDate; + late DateTime? initialDate; late DateTime today; late SelectableDayPredicate? selectableDayPredicate; late DatePickerEntryMode initialEntryMode; @@ -124,7 +123,7 @@ void main() { } group('showDatePicker Dialog', () { - testWidgets('Default dialog size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default dialog size', (WidgetTester tester) async { Future<void> showPicker(WidgetTester tester, Size size) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; @@ -151,7 +150,7 @@ void main() { expect(dialogContainerSize, calendarPortraitDialogSizeM3); }); - testWidgets('Default dialog properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default dialog properties', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await prepareDatePicker(tester, (Future<DateTime?> date) async { final Material dialogMaterial = tester.widget<Material>( @@ -174,13 +173,13 @@ void main() { }, useMaterial3: theme.useMaterial3); }); - testWidgets('Material3 uses sentence case labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 uses sentence case labels', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.text('Select date'), findsOneWidget); }, useMaterial3: true); }); - testWidgets('Cancel, confirm, and help text is used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cancel, confirm, and help text is used', (WidgetTester tester) async { cancelText = 'nope'; confirmText = 'yep'; helpText = 'help'; @@ -191,21 +190,21 @@ void main() { }); }); - testWidgets('Initial date is the default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial date is the default', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('OK')); expect(await date, DateTime(2016, DateTime.january, 15)); }); }); - testWidgets('Can cancel', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can cancel', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('CANCEL')); expect(await date, isNull); }); }); - testWidgets('Can switch from calendar to input entry mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can switch from calendar to input entry mode', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsNothing); await tester.tap(find.byIcon(Icons.edit)); @@ -214,7 +213,7 @@ void main() { }); }); - testWidgets('Can switch from input to calendar entry mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can switch from input to calendar entry mode', (WidgetTester tester) async { initialEntryMode = DatePickerEntryMode.input; await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsOneWidget); @@ -224,7 +223,7 @@ void main() { }); }); - testWidgets('Can not switch out of calendarOnly mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can not switch out of calendarOnly mode', (WidgetTester tester) async { initialEntryMode = DatePickerEntryMode.calendarOnly; await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsNothing); @@ -232,7 +231,7 @@ void main() { }); }); - testWidgets('Can not switch out of inputOnly mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can not switch out of inputOnly mode', (WidgetTester tester) async { initialEntryMode = DatePickerEntryMode.inputOnly; await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsOneWidget); @@ -240,7 +239,7 @@ void main() { }); }); - testWidgets('Switching to input mode keeps selected date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switching to input mode keeps selected date', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('12')); await tester.tap(find.byIcon(Icons.edit)); @@ -250,7 +249,7 @@ void main() { }); }); - testWidgets('Input only mode should validate date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input only mode should validate date', (WidgetTester tester) async { initialEntryMode = DatePickerEntryMode.inputOnly; await prepareDatePicker(tester, (Future<DateTime?> date) async { // Enter text input mode and type an invalid date to get error. @@ -261,7 +260,7 @@ void main() { }); }); - testWidgets('Switching to input mode resets input error state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switching to input mode resets input error state', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { // Enter text input mode and type an invalid date to get error. await tester.tap(find.byIcon(Icons.edit)); @@ -285,7 +284,7 @@ void main() { }); }); - testWidgets('builder parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('builder parameter', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MaterialApp( home: Material( @@ -332,10 +331,194 @@ void main() { // We expect the left edge of the 'OK' button in the RTL // layout to match the gap between right edge of the 'OK' // button and the right edge of the 800 wide view. - expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight); + expect(tester.getBottomLeft(find.text('OK')).dx, moreOrLessEquals(800 - ltrOkRight)); + }); + + group('Barrier dismissible', () { + late _DatePickerObserver rootObserver; + + setUp(() { + rootObserver = _DatePickerObserver(); + }); + + testWidgetsWithLeakTracking('Barrier is dismissible with default parameter', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, + Widget? child) => const SizedBox(), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.datePickerCount, 1); + + // Tap on the barrier. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.datePickerCount, 0); + }); + + testWidgetsWithLeakTracking('Barrier is not dismissible with barrierDismissible is false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + barrierDismissible: false, + builder: (BuildContext context, + Widget? child) => const SizedBox(), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.datePickerCount, 1); + + // Tap on the barrier, which shouldn't do anything this time. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.datePickerCount, 1); + }); + }); + + testWidgetsWithLeakTracking('Barrier color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, + Widget? child) => const SizedBox(), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.black54); + + // Dismiss the dialog. + await tester.tapAt(const Offset(10.0, 10.0)); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showDatePicker( + context: context, + barrierColor: Colors.pink, + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, + Widget? child) => const SizedBox(), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink); }); - testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Barrier Label', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showDatePicker( + context: context, + barrierLabel: 'Custom Label', + initialDate: DateTime.now(), + firstDate: DateTime(2018), + lastDate: DateTime(2030), + builder: (BuildContext context, + Widget? child) => const SizedBox(), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).semanticsLabel, 'Custom Label'); + }); + + testWidgetsWithLeakTracking('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final _DatePickerObserver rootObserver = _DatePickerObserver(); final _DatePickerObserver nestedObserver = _DatePickerObserver(); @@ -372,7 +555,7 @@ void main() { expect(nestedObserver.datePickerCount, 1); }); - testWidgets('honors DialogTheme for shape and elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('honors DialogTheme for shape and elevation', (WidgetTester tester) async { // Test that the defaults work const DialogTheme datePickerDefaultDialogTheme = DialogTheme( shape: RoundedRectangleBorder( @@ -445,7 +628,7 @@ void main() { expect(themeDialogMaterial.elevation, customDialogTheme.elevation); }); - testWidgets('OK Cancel button layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OK Cancel button layout', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -521,7 +704,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('honors switchToInputEntryModeIcon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('honors switchToInputEntryModeIcon', (WidgetTester tester) async { Widget buildApp({bool? useMaterial3, Icon? switchToInputEntryModeIcon}) { return MaterialApp( theme: ThemeData( @@ -577,7 +760,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('honors switchToCalendarEntryModeIcon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('honors switchToCalendarEntryModeIcon', (WidgetTester tester) async { Widget buildApp({bool? useMaterial3, Icon? switchToCalendarEntryModeIcon}) { return MaterialApp( theme: ThemeData( @@ -636,7 +819,7 @@ void main() { }); group('Calendar mode', () { - testWidgets('Default Calendar mode layout (Landscape)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default Calendar mode layout (Landscape)', (WidgetTester tester) async { final Finder helpText = find.text('Select date'); final Finder headerText = find.text('Fri, Jan 15'); final Finder subHeaderText = find.text('January 2016'); @@ -694,11 +877,9 @@ void main() { final Offset subHeaderTextTopLeft = tester.getTopLeft(subHeaderText); final Offset dividerTopRight = tester.getTopRight(divider); expect(subHeaderTextTopLeft.dx, dividerTopRight.dx + 24.0); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - expect(subHeaderTextTopLeft.dy, dialogTopLeft.dy + 16.0 - (hasIssue99933 ? 0.5 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(subHeaderTextTopLeft.dy, dialogTopLeft.dy + 16.0); + } // Test sub header icon position. final Finder subHeaderIcon = find.byIcon(Icons.arrow_drop_down); @@ -712,7 +893,9 @@ void main() { final Offset calendarPageViewTopLeft = tester.getTopLeft(calendarPageView); final Offset subHeaderTextBottomLeft = tester.getBottomLeft(subHeaderText); expect(calendarPageViewTopLeft.dx, dividerTopRight.dx); - expect(calendarPageViewTopLeft.dy, subHeaderTextBottomLeft.dy + 16.0 - (hasIssue99933 ? 0.5 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(calendarPageViewTopLeft.dy, subHeaderTextBottomLeft.dy + 16.0); + } // Test month navigation icons position. final Finder previousMonthButton = find.widgetWithIcon(IconButton, Icons.chevron_left); @@ -735,7 +918,7 @@ void main() { expect(cancelButtonTopRight.dx, okButtonTopLeft.dx - 8); }); - testWidgets('Default Calendar mode layout (Portrait)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default Calendar mode layout (Portrait)', (WidgetTester tester) async { final Finder helpText = find.text('Select date'); final Finder headerText = find.text('Fri, Jan 15'); final Finder subHeaderText = find.text('January 2016'); @@ -772,11 +955,9 @@ void main() { final Offset headerTextTextTopLeft = tester.getTopLeft(headerText); final Offset helpTextBottomLeft = tester.getBottomLeft(helpText); expect(headerTextTextTopLeft.dx, dialogTopLeft.dx + 24.0); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - expect(headerTextTextTopLeft.dy, helpTextBottomLeft.dy + 28.0 - (hasIssue99933 ? 1.0 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(headerTextTextTopLeft.dy, helpTextBottomLeft.dy + 28.0); + } // Test switch button position. final Finder switchButtonM3 = find.widgetWithIcon(IconButton, Icons.edit_outlined); @@ -796,7 +977,9 @@ void main() { final Offset subHeaderTextTopLeft = tester.getTopLeft(subHeaderText); final Offset dividerBottomLeft = tester.getBottomLeft(divider); expect(subHeaderTextTopLeft.dx, dialogTopLeft.dx + 24.0); - expect(subHeaderTextTopLeft.dy, dividerBottomLeft.dy + 16.0 - (hasIssue99933 ? 0.5 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(subHeaderTextTopLeft.dy, dividerBottomLeft.dy + 16.0); + } // Test sub header icon position. final Finder subHeaderIcon = find.byIcon(Icons.arrow_drop_down); @@ -819,7 +1002,9 @@ void main() { final Offset calendarPageViewTopLeft = tester.getTopLeft(calendarPageView); final Offset subHeaderTextBottomLeft = tester.getBottomLeft(subHeaderText); expect(calendarPageViewTopLeft.dx, dialogTopLeft.dx); - expect(calendarPageViewTopLeft.dy, subHeaderTextBottomLeft.dy + 16.0 - (hasIssue99933 ? 0.5 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(calendarPageViewTopLeft.dy, subHeaderTextBottomLeft.dy + 16.0); + } // Test action buttons position. final Offset dialogBottomRight = tester.getBottomRight(find.byType(AnimatedContainer)); @@ -832,7 +1017,7 @@ void main() { expect(cancelButtonTopRight.dx, okButtonTopLeft.dx - 8); }); - testWidgets('Can select a day', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a day', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('12')); await tester.tap(find.text('OK')); @@ -840,7 +1025,7 @@ void main() { }); }); - testWidgets('Can select a month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a month', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(previousMonthIcon); await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -850,7 +1035,7 @@ void main() { }); }); - testWidgets('Can select a year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a year', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('January 2016')); // Switch to year mode. await tester.pump(); @@ -860,7 +1045,38 @@ void main() { }); }); - testWidgets('Selecting date does not change displayed month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a day with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future<DateTime?> date) async { + await tester.tap(find.text('12')); + await tester.tap(find.text('OK')); + expect(await date, equals(DateTime(2016, DateTime.january, 12))); + }); + }); + + testWidgetsWithLeakTracking('Can select a month with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future<DateTime?> date) async { + await tester.tap(previousMonthIcon); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(find.text('25')); + await tester.tap(find.text('OK')); + expect(await date, DateTime(2015, DateTime.december, 25)); + }); + }); + + testWidgetsWithLeakTracking('Can select a year with no initial date', (WidgetTester tester) async { + initialDate = null; + await prepareDatePicker(tester, (Future<DateTime?> date) async { + await tester.tap(find.text('January 2016')); // Switch to year mode. + await tester.pump(); + await tester.tap(find.text('2018')); + await tester.pump(); + expect(find.text('January 2018'), findsOneWidget); + }); + }); + + testWidgetsWithLeakTracking('Selecting date does not change displayed month', (WidgetTester tester) async { initialDate = DateTime(2020, DateTime.march, 15); await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(nextMonthIcon); @@ -874,18 +1090,18 @@ void main() { }); }); - testWidgets('Changing year does not change selected date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing year does change selected date', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('January 2016')); await tester.pump(); await tester.tap(find.text('2018')); await tester.pump(); await tester.tap(find.text('OK')); - expect(await date, equals(DateTime(2016, DateTime.january, 15))); + expect(await date, equals(DateTime(2018, DateTime.january, 15))); }); }); - testWidgets('Changing year does not change the month', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing year does not change the month', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(nextMonthIcon); await tester.pumpAndSettle(); @@ -899,7 +1115,7 @@ void main() { }); }); - testWidgets('Can select a year and then a day', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select a year and then a day', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('January 2016')); // Switch to year mode. await tester.pump(); @@ -911,7 +1127,7 @@ void main() { }); }); - testWidgets('Current year is visible in year picker', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Current year is visible in year picker', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('January 2016')); // Switch to year mode. await tester.pump(); @@ -919,10 +1135,10 @@ void main() { }); }); - testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select a day outside bounds', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); - firstDate = initialDate; - lastDate = initialDate; + firstDate = initialDate!; + lastDate = initialDate!; await prepareDatePicker(tester, (Future<DateTime?> date) async { // Earlier than firstDate. Should be ignored. await tester.tap(find.text('10')); @@ -934,9 +1150,9 @@ void main() { }); }); - testWidgets('Cannot select a month past last date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select a month past last date', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); - firstDate = initialDate; + firstDate = initialDate!; lastDate = DateTime(2017, DateTime.february, 20); await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(nextMonthIcon); @@ -946,10 +1162,10 @@ void main() { }); }); - testWidgets('Cannot select a month before first date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select a month before first date', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 15); firstDate = DateTime(2016, DateTime.december, 10); - lastDate = initialDate; + lastDate = initialDate!; await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(previousMonthIcon); await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -958,7 +1174,7 @@ void main() { }); }); - testWidgets('Cannot select disabled year', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot select disabled year', (WidgetTester tester) async { initialDate = DateTime(2018, DateTime.july, 4); firstDate = DateTime(2018, DateTime.june, 9); lastDate = DateTime(2018, DateTime.december, 15); @@ -973,7 +1189,7 @@ void main() { }); }); - testWidgets('Selecting firstDate year respects firstDate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selecting firstDate year respects firstDate', (WidgetTester tester) async { initialDate = DateTime(2018, DateTime.may, 4); firstDate = DateTime(2016, DateTime.june, 9); lastDate = DateTime(2019, DateTime.january, 15); @@ -987,7 +1203,7 @@ void main() { }); }); - testWidgets('Selecting lastDate year respects lastDate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selecting lastDate year respects lastDate', (WidgetTester tester) async { initialDate = DateTime(2018, DateTime.may, 4); firstDate = DateTime(2016, DateTime.june, 9); lastDate = DateTime(2019, DateTime.january, 15); @@ -1001,7 +1217,7 @@ void main() { }); }); - testWidgets('Only predicate days are selectable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Only predicate days are selectable', (WidgetTester tester) async { initialDate = DateTime(2017, DateTime.january, 16); firstDate = DateTime(2017, DateTime.january, 10); lastDate = DateTime(2017, DateTime.january, 20); @@ -1015,7 +1231,7 @@ void main() { }); }); - testWidgets('Can select initial calendar picker mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select initial calendar picker mode', (WidgetTester tester) async { initialDate = DateTime(2014, DateTime.january, 15); initialCalendarMode = DatePickerMode.year; await prepareDatePicker(tester, (Future<DateTime?> date) async { @@ -1028,7 +1244,7 @@ void main() { }); }); - testWidgets('currentDate is highlighted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('currentDate is highlighted', (WidgetTester tester) async { today = DateTime(2016, 1, 2); await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.pump(); @@ -1041,7 +1257,7 @@ void main() { }); }); - testWidgets('Date picker dayOverlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Date picker dayOverlayColor resolves pressed state', (WidgetTester tester) async { today = DateTime(2023, 5, 4); final ThemeData theme = ThemeData(); final bool material3 = theme.useMaterial3; @@ -1073,7 +1289,7 @@ void main() { }, theme: theme); }); - testWidgets('Selecting date does not switch picker to year selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selecting date does not switch picker to year selection', (WidgetTester tester) async { initialDate = DateTime(2020, DateTime.may, 10); initialCalendarMode = DatePickerMode.year; await prepareDatePicker(tester, (Future<DateTime?> date) async { @@ -1097,7 +1313,7 @@ void main() { initialEntryMode = DatePickerEntryMode.input; }); - testWidgets('Default InputDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default InputDecoration', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { final InputDecoration decoration = tester.widget<TextField>( find.byType(TextField)).decoration!; @@ -1109,13 +1325,13 @@ void main() { }, useMaterial3: true); }); - testWidgets('Initial entry mode is used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial entry mode is used', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsOneWidget); }); }); - testWidgets('Hint, label, and help text is used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hint, label, and help text is used', (WidgetTester tester) async { cancelText = 'nope'; confirmText = 'yep'; fieldHintText = 'hint'; @@ -1130,7 +1346,7 @@ void main() { }); }); - testWidgets('KeyboardType is used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('KeyboardType is used', (WidgetTester tester) async { keyboardType = TextInputType.text; await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); @@ -1138,14 +1354,14 @@ void main() { }); }); - testWidgets('Initial date is the default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial date is the default', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.text('OK')); expect(await date, DateTime(2016, DateTime.january, 15)); }); }); - testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can toggle to calendar entry mode', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsOneWidget); await tester.tap(find.byIcon(Icons.calendar_today)); @@ -1154,7 +1370,7 @@ void main() { }); }); - testWidgets('Toggle to calendar mode keeps selected date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle to calendar mode keeps selected date', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); field.controller!.clear(); @@ -1167,7 +1383,7 @@ void main() { }); }); - testWidgets('Entered text returns date', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Entered text returns date', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); field.controller!.clear(); @@ -1178,7 +1394,7 @@ void main() { }); }); - testWidgets('Too short entered text shows error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Too short entered text shows error', (WidgetTester tester) async { errorFormatText = 'oops'; await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); @@ -1194,7 +1410,7 @@ void main() { }); }); - testWidgets('Bad format entered text shows error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bad format entered text shows error', (WidgetTester tester) async { errorFormatText = 'oops'; await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); @@ -1211,7 +1427,7 @@ void main() { }); }); - testWidgets('Invalid entered text shows error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Invalid entered text shows error', (WidgetTester tester) async { errorInvalidText = 'oops'; await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); @@ -1227,7 +1443,7 @@ void main() { }); }); - testWidgets('Invalid entered text shows error on autovalidate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Invalid entered text shows error on autovalidate', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/126397. await prepareDatePicker(tester, (Future<DateTime?> date) async { final TextField field = textField(tester); @@ -1254,10 +1470,26 @@ void main() { expect(tester.takeException(), null); }); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/131989. + testWidgetsWithLeakTracking('Dialog contents do not overflow when resized from landscape to portrait', + (WidgetTester tester) async { + addTearDown(tester.view.reset); + // Initial window size is wide for landscape mode. + tester.view.physicalSize = wideWindowSize; + tester.view.devicePixelRatio = 1.0; + + await prepareDatePicker(tester, (Future<DateTime?> date) async { + // Change window size to narrow for portrait mode. + tester.view.physicalSize = narrowWindowSize; + await tester.pump(); + expect(tester.takeException(), null); + }); + }); }); group('Semantics', () { - testWidgets('calendar mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('calendar mode', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); await prepareDatePicker(tester, (Future<DateTime?> date) async { @@ -1306,7 +1538,7 @@ void main() { semantics.dispose(); }); - testWidgets('input mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('input mode', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); initialEntryMode = DatePickerEntryMode.input; @@ -1351,7 +1583,7 @@ void main() { }); group('Keyboard navigation', () { - testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can toggle to calendar entry mode', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.byType(TextField), findsNothing); // Navigate to the entry toggle button and activate it @@ -1367,7 +1599,7 @@ void main() { }); }); - testWidgets('Can toggle to year mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can toggle to year mode', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.text('2016'), findsNothing); // Navigate to the year selector and activate it @@ -1379,7 +1611,7 @@ void main() { }); }); - testWidgets('Can navigate next/previous months', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can navigate next/previous months', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { expect(find.text('January 2016'), findsOneWidget); // Navigate to the previous month button and activate it twice @@ -1407,7 +1639,7 @@ void main() { }); }); - testWidgets('Can navigate date grid with arrow keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can navigate date grid with arrow keys', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { // Navigate to the grid await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -1443,7 +1675,7 @@ void main() { }); }); - testWidgets('Navigating with arrow keys scrolls months', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigating with arrow keys scrolls months', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { // Navigate to the grid await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -1491,7 +1723,7 @@ void main() { }); }); - testWidgets('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { // Navigate to the grid await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -1556,49 +1788,49 @@ void main() { await tester.pumpAndSettle(); } - testWidgets('common screen size - portrait', (WidgetTester tester) async { + testWidgetsWithLeakTracking('common screen size - portrait', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizePortrait); expect(tester.takeException(), isNull); }); - testWidgets('common screen size - landscape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('common screen size - landscape', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizeLandscape); expect(tester.takeException(), isNull); }); - testWidgets('common screen size - portrait - textScale 1.3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('common screen size - portrait - textScale 1.3', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizePortrait, 1.3); expect(tester.takeException(), isNull); }); - testWidgets('common screen size - landscape - textScale 1.3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('common screen size - landscape - textScale 1.3', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizeLandscape, 1.3); expect(tester.takeException(), isNull); }); - testWidgets('small screen size - portrait', (WidgetTester tester) async { + testWidgetsWithLeakTracking('small screen size - portrait', (WidgetTester tester) async { await showPicker(tester, kSmallScreenSizePortrait); expect(tester.takeException(), isNull); }); - testWidgets('small screen size - landscape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('small screen size - landscape', (WidgetTester tester) async { await showPicker(tester, kSmallScreenSizeLandscape); expect(tester.takeException(), isNull); }); - testWidgets('small screen size - portrait -textScale 1.3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('small screen size - portrait -textScale 1.3', (WidgetTester tester) async { await showPicker(tester, kSmallScreenSizePortrait, 1.3); expect(tester.takeException(), isNull); }); - testWidgets('small screen size - landscape - textScale 1.3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('small screen size - landscape - textScale 1.3', (WidgetTester tester) async { await showPicker(tester, kSmallScreenSizeLandscape, 1.3); expect(tester.takeException(), isNull); }); }); group('showDatePicker avoids overlapping display features', () { - testWidgets('positioning with anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1636,7 +1868,7 @@ void main() { expect(tester.getBottomRight(find.byType(DatePickerDialog)), const Offset(800.0, 600.0)); }); - testWidgets('positioning with Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with Directionality', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1676,7 +1908,7 @@ void main() { expect(tester.getBottomRight(find.byType(DatePickerDialog)), const Offset(800.0, 600.0)); }); - testWidgets('positioning with defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with defaults', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1714,7 +1946,7 @@ void main() { }); }); - testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DatePickerDialog is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -1767,7 +1999,7 @@ void main() { expect(find.text('30/7/2021'), findsOneWidget); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('DatePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DatePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -1816,7 +2048,7 @@ void main() { expect(find.byIcon(Icons.edit), findsNothing); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('Test Callback on Toggle of DatePicker Mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test Callback on Toggle of DatePicker Mode', (WidgetTester tester) async { prepareDatePicker(tester, (Future<DateTime?> date) async { await tester.tap(find.byIcon(Icons.edit)); expect(currentMode, DatePickerEntryMode.input); @@ -1844,14 +2076,14 @@ void main() { await prepareDatePicker(tester, (Future<DateTime?> date) async { }, useMaterial3: true); } - testWidgets('portrait', (WidgetTester tester) async { + testWidgetsWithLeakTracking('portrait', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizePortrait); expect(tester.widget<Text>(find.text('Fri, Jan 15')).style?.fontSize, 32); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); }); - testWidgets('landscape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('landscape', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizeLandscape); expect(tester.widget<Text>(find.text('Fri, Jan 15')).style?.fontSize, 24); await tester.tap(find.text('Cancel')); @@ -1865,7 +2097,7 @@ void main() { // can be deleted. group('showDatePicker Dialog', () { - testWidgets('Default dialog size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default dialog size', (WidgetTester tester) async { Future<void> showPicker(WidgetTester tester, Size size) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; @@ -1894,7 +2126,7 @@ void main() { expect(dialogContainerSize, calendarPortraitDialogSizeM2); }); - testWidgets('Default dialog properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default dialog properties', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await prepareDatePicker(tester, (Future<DateTime?> date) async { final Material dialogMaterial = tester.widget<Material>( @@ -1926,7 +2158,7 @@ void main() { initialEntryMode = DatePickerEntryMode.input; }); - testWidgets('Default InputDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default InputDecoration', (WidgetTester tester) async { await prepareDatePicker(tester, (Future<DateTime?> date) async { final InputDecoration decoration = tester.widget<TextField>( find.byType(TextField)).decoration!; @@ -1970,6 +2202,13 @@ class _RestorableDatePickerDialogTestWidgetState extends State<_RestorableDatePi }, ); + @override + void dispose() { + _selectedDate.dispose(); + _restorableDatePickerRouteFuture.dispose(); + super.dispose(); + } + @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_selectedDate, 'selected_date'); @@ -2035,4 +2274,12 @@ class _DatePickerObserver extends NavigatorObserver { } super.didPush(route, previousRoute); } + + @override + void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { + if (route is DialogRoute) { + datePickerCount--; + } + super.didPop(route, previousRoute); + } } diff --git a/packages/flutter/test/material/date_picker_theme_test.dart b/packages/flutter/test/material/date_picker_theme_test.dart index 191c6faa3aa37..58bb87aed2b9f 100644 --- a/packages/flutter/test/material/date_picker_theme_test.dart +++ b/packages/flutter/test/material/date_picker_theme_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -44,7 +46,9 @@ void main() { inputDecorationTheme: InputDecorationTheme( fillColor: Color(0xffffff5f), border: UnderlineInputBorder(), - ) + ), + cancelButtonStyle: ButtonStyle(foregroundColor: MaterialStatePropertyAll<Color>(Color(0xffffff6f))), + confirmButtonStyle: ButtonStyle(foregroundColor: MaterialStatePropertyAll<Color>(Color(0xffffff7f))), ); Material findDialogMaterial(WidgetTester tester) { @@ -75,6 +79,10 @@ void main() { return container.decoration as BoxDecoration?; } + ButtonStyle actionButtonStyle(WidgetTester tester, String text) { + return tester.widget<TextButton>(find.widgetWithText(TextButton, text)).style!; + } + const Size wideWindowSize = Size(1920.0, 1080.0); const Size narrowWindowSize = Size(1070.0, 1770.0); @@ -124,6 +132,8 @@ void main() { expect(theme.rangeSelectionOverlayColor, null); expect(theme.dividerColor, null); expect(theme.inputDecorationTheme, null); + expect(theme.cancelButtonStyle, null); + expect(theme.confirmButtonStyle, null); }); testWidgets('DatePickerTheme.defaults M3 defaults', (WidgetTester tester) async { @@ -199,6 +209,8 @@ void main() { expect(m3.rangePickerHeaderHelpStyle, textTheme.titleSmall); expect(m3.dividerColor, null); expect(m3.inputDecorationTheme, null); + expect(m3.cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + expect(m3.confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); }); testWidgets('DatePickerTheme.defaults M2 defaults', (WidgetTester tester) async { @@ -266,6 +278,8 @@ void main() { expect(m2.rangePickerHeaderHelpStyle, textTheme.labelSmall); expect(m2.dividerColor, null); expect(m2.inputDecorationTheme, null); + expect(m2.cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + expect(m2.confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); }); testWidgets('Default DatePickerThemeData debugFillProperties', (WidgetTester tester) async { @@ -290,9 +304,7 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect( - description, - equalsIgnoringHashCodes(<String>[ + expect(description, equalsIgnoringHashCodes(<String>[ 'backgroundColor: Color(0xfffffff0)', 'elevation: 6.0', 'shadowColor: Color(0xfffffff1)', @@ -326,9 +338,10 @@ void main() { 'rangeSelectionBackgroundColor: Color(0xffffff2f)', 'rangeSelectionOverlayColor: MaterialStatePropertyAll(Color(0xffffff3f))', 'dividerColor: Color(0xffffff4f)', - 'inputDecorationTheme: InputDecorationTheme#00000(fillColor: Color(0xffffff5f), border: UnderlineInputBorder())' - ]), - ); + 'inputDecorationTheme: InputDecorationTheme#00000(fillColor: Color(0xffffff5f), border: UnderlineInputBorder())', + 'cancelButtonStyle: ButtonStyle#00000(foregroundColor: MaterialStatePropertyAll(Color(0xffffff6f)))', + 'confirmButtonStyle: ButtonStyle#00000(foregroundColor: MaterialStatePropertyAll(Color(0xffffff7f)))' + ])); }); testWidgets('DatePickerDialog uses ThemeData datePicker theme (calendar mode)', (WidgetTester tester) async { @@ -389,6 +402,16 @@ void main() { expect(day24Decoration.border?.top.width, datePickerTheme.todayBorder?.width); expect(day24Decoration.border?.bottom.width, datePickerTheme.todayBorder?.width); + // Test the day overlay color. + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('25'))); + await tester.pumpAndSettle(); + expect(inkFeatures, paints..circle(color: datePickerTheme.dayOverlayColor?.resolve(<MaterialState>{}))); + // Show the year selector. await tester.tap(find.text('January 2023')); @@ -409,6 +432,17 @@ void main() { expect(year2023Decoration.border?.bottom.width, datePickerTheme.todayBorder?.width); expect(year2023Decoration.border?.top.color, datePickerTheme.todayForegroundColor?.resolve(<MaterialState>{})); expect(year2023Decoration.border?.bottom.color, datePickerTheme.todayForegroundColor?.resolve(<MaterialState>{})); + + // Test the year overlay color. + await gesture.moveTo(tester.getCenter(find.text('2024'))); + await tester.pumpAndSettle(); + expect(inkFeatures, paints..rect(color: datePickerTheme.yearOverlayColor?.resolve(<MaterialState>{}))); + + final ButtonStyle cancelButtonStyle = actionButtonStyle(tester, 'Cancel'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.cancelButtonStyle.toString())); + + final ButtonStyle confirmButtonStyle = actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.confirmButtonStyle.toString())); }); testWidgets('DatePickerDialog uses ThemeData datePicker theme (input mode)', (WidgetTester tester) async { @@ -450,6 +484,12 @@ void main() { final InputDecoration inputDecoration = tester.widget<TextField>(find.byType(TextField)).decoration!; expect(inputDecoration.fillColor, datePickerTheme.inputDecorationTheme?.fillColor); + + final ButtonStyle cancelButtonStyle = actionButtonStyle(tester, 'Cancel'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.cancelButtonStyle.toString())); + + final ButtonStyle confirmButtonStyle = actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.confirmButtonStyle.toString())); }); testWidgets('DateRangePickerDialog uses ThemeData datePicker theme', (WidgetTester tester) async { @@ -496,6 +536,21 @@ void main() { final Text selectedDate = tester.widget<Text>(find.text('Jan 17')); expect(selectedDate.style?.color, datePickerTheme.rangePickerHeaderForegroundColor); expect(selectedDate.style?.fontSize, datePickerTheme.rangePickerHeaderHeadlineStyle?.fontSize); + + // Test the day overlay color. + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('16'))); + await tester.pumpAndSettle(); + expect(inkFeatures, paints..circle(color: datePickerTheme.dayOverlayColor?.resolve(<MaterialState>{}))); + + // Test the range selection overlay color. + await gesture.moveTo(tester.getCenter(find.text('18'))); + await tester.pumpAndSettle(); + expect(inkFeatures, paints..circle(color: datePickerTheme.rangeSelectionOverlayColor?.resolve(<MaterialState>{}))); }); testWidgets('Dividers use DatePickerThemeData.dividerColor', (WidgetTester tester) async { @@ -594,4 +649,252 @@ void main() { expect(inputDecoration.fillColor, const Color(0xFF00FF00)); expect(inputDecoration.border , const OutlineInputBorder()); }); + + testWidgets('DatePickerDialog resolves DatePickerTheme.dayOverlayColor states', (WidgetTester tester) async { + final MaterialStateProperty<Color> dayOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { + if (states.contains(MaterialState.hovered)) { + return const Color(0xff00ff00); + } + if (states.contains(MaterialState.focused)) { + return const Color(0xffff00ff); + } + if (states.contains(MaterialState.pressed)) { + return const Color(0xffffff00); + } + return Colors.transparent; + }); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + datePickerTheme: DatePickerThemeData( + dayOverlayColor: dayOverlayColor, + ), + useMaterial3: true, + ), + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: Focus( + child: DatePickerDialog( + initialDate: DateTime(2023, DateTime.january, 25), + firstDate: DateTime(2022), + lastDate: DateTime(2024, DateTime.december, 31), + currentDate: DateTime(2023, DateTime.january, 24), + ), + ), + ), + ), + ), + ), + ); + + // Test the hover overlay color. + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('20'))); + await tester.pumpAndSettle(); + expect( + inkFeatures, + paints + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.hovered})), + ); + + // Test the pressed overlay color. + await gesture.down(tester.getCenter(find.text('20'))); + await tester.pumpAndSettle(); + if (kIsWeb) { + // An extra circle is painted on the web for the hovered state. + expect( + inkFeatures, + paints + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.pressed})), + ); + } else { + expect( + inkFeatures, + paints + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.pressed})), + ); + } + + await gesture.removePointer(); + await tester.pumpAndSettle(); + + // Focus day selection. + for (int i = 0; i < 5; i++) { + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + } + + // Test the focused overlay color. + expect( + inkFeatures, + paints + ..circle(color: dayOverlayColor.resolve(<MaterialState>{MaterialState.focused})), + ); + }); + + testWidgets('DatePickerDialog resolves DatePickerTheme.yearOverlayColor states', (WidgetTester tester) async { + final MaterialStateProperty<Color> yearOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { + if (states.contains(MaterialState.hovered)) { + return const Color(0xff00ff00); + } + if (states.contains(MaterialState.focused)) { + return const Color(0xffff00ff); + } + if (states.contains(MaterialState.pressed)) { + return const Color(0xffffff00); + } + return Colors.transparent; + }); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + datePickerTheme: DatePickerThemeData( + yearOverlayColor: yearOverlayColor, + ), + useMaterial3: true, + ), + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: Focus( + child: DatePickerDialog( + initialDate: DateTime(2023, DateTime.january, 25), + firstDate: DateTime(2022), + lastDate: DateTime(2024, DateTime.december, 31), + currentDate: DateTime(2023, DateTime.january, 24), + initialCalendarMode: DatePickerMode.year, + ), + ), + ), + ), + ), + ), + ); + + // Test the hover overlay color. + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('2022'))); + await tester.pumpAndSettle(); + expect( + inkFeatures, + paints + ..rect(color: yearOverlayColor.resolve(<MaterialState>{MaterialState.hovered})), + ); + + // Test the pressed overlay color. + await gesture.down(tester.getCenter(find.text('2022'))); + await tester.pumpAndSettle(); + expect( + inkFeatures, + paints + ..rect(color: yearOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..rect(color: yearOverlayColor.resolve(<MaterialState>{MaterialState.pressed})), + ); + + await gesture.removePointer(); + await tester.pumpAndSettle(); + + // Focus year selection. + for (int i = 0; i < 3; i++) { + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + } + + // Test the focused overlay color. + expect( + inkFeatures, + paints + ..rect(color: yearOverlayColor.resolve(<MaterialState>{MaterialState.focused})), + ); + }); + + testWidgets('DateRangePickerDialog resolves DatePickerTheme.rangeSelectionOverlayColor states', (WidgetTester tester) async { + final MaterialStateProperty<Color> rangeSelectionOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { + if (states.contains(MaterialState.hovered)) { + return const Color(0xff00ff00); + } + if (states.contains(MaterialState.pressed)) { + return const Color(0xffffff00); + } + return Colors.transparent; + }); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + datePickerTheme: DatePickerThemeData( + rangeSelectionOverlayColor: rangeSelectionOverlayColor, + ), + useMaterial3: true, + ), + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: DateRangePickerDialog( + firstDate: DateTime(2023), + lastDate: DateTime(2023, DateTime.january, 31), + initialDateRange: DateTimeRange( + start: DateTime(2023, DateTime.january, 17), + end: DateTime(2023, DateTime.january, 20), + ), + currentDate: DateTime(2023, DateTime.january, 23), + ), + ), + ), + ), + ), + ); + + // Test the hover overlay color. + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('18'))); + await tester.pumpAndSettle(); + expect( + inkFeatures, + paints + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.hovered})), + ); + + // Test the pressed overlay color. + await gesture.down(tester.getCenter(find.text('18'))); + await tester.pumpAndSettle(); + if (kIsWeb) { + // An extra circle is painted on the web for the hovered state. + expect( + inkFeatures, + paints + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.pressed})), + ); + } else { + expect( + inkFeatures, + paints + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.hovered})) + ..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.pressed})), + ); + } + }); } diff --git a/packages/flutter/test/material/date_range_picker_test.dart b/packages/flutter/test/material/date_range_picker_test.dart index 6feafa939d78b..aa57e709cc9a9 100644 --- a/packages/flutter/test/material/date_range_picker_test.dart +++ b/packages/flutter/test/material/date_range_picker_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'feedback_tester.dart'; void main() { @@ -53,6 +52,9 @@ void main() { saveText = null; }); + const Size wideWindowSize = Size(1920.0, 1080.0); + const Size narrowWindowSize = Size(1070.0, 1770.0); + Future<void> preparePicker( WidgetTester tester, Future<void> Function(Future<DateTimeRange?> date) callback, { @@ -132,22 +134,21 @@ void main() { final Offset entryButtonBottomLeft = tester.getBottomLeft( find.widgetWithIcon(IconButton, Icons.edit_outlined), ); - expect( - saveButtonBottomLeft.dx, - const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? moreOrLessEquals(711.6, epsilon: 1e-5) : (800 - 89.0), - ); - expect(saveButtonBottomLeft.dy, helpTextTopLeft.dy); + expect(saveButtonBottomLeft.dx, moreOrLessEquals(711.6, epsilon: 1e-5)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(saveButtonBottomLeft.dy, helpTextTopLeft.dy); + } expect(entryButtonBottomLeft.dx, saveButtonBottomLeft.dx - 48.0); - expect(entryButtonBottomLeft.dy, helpTextTopLeft.dy); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(entryButtonBottomLeft.dy, helpTextTopLeft.dy); + } // Test help text position. final Offset helpTextBottomLeft = tester.getBottomLeft(helpText); expect(helpTextBottomLeft.dx, 72.0); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - expect(helpTextBottomLeft.dy, closeButtonBottomRight.dy + 20.0 + (hasIssue99933 ? 1.0 : 0.0)); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(helpTextBottomLeft.dy, closeButtonBottomRight.dy + 20.0); + } // Test the header position. final Offset firstDateHeaderTopLeft = tester.getTopLeft(firstDateHeaderText); @@ -1059,6 +1060,22 @@ void main() { // Test the end date text field testInputDecorator(tester.widget(borderContainers.last), border, Colors.transparent); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/131989. + testWidgets('Dialog contents do not overflow when resized from landscape to portrait', + (WidgetTester tester) async { + addTearDown(tester.view.reset); + // Initial window size is wide for landscape mode. + tester.view.physicalSize = wideWindowSize; + tester.view.devicePixelRatio = 1.0; + + await preparePicker(tester, (Future<DateTimeRange?> range) async { + // Change window size to narrow for portrait mode. + tester.view.physicalSize = narrowWindowSize; + await tester.pump(); + expect(tester.takeException(), null); + }); + }); }); testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/debug_test.dart b/packages/flutter/test/material/debug_test.dart index 5b36a71c2bad3..780f19c2b3ad0 100644 --- a/packages/flutter/test/material/debug_test.dart +++ b/packages/flutter/test/material/debug_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('debugCheckHasMaterial control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasMaterial control test', (WidgetTester tester) async { await tester.pumpWidget(const Center(child: Chip(label: Text('label')))); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); @@ -47,7 +48,7 @@ void main() { )); }); - testWidgets('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async { await tester.pumpWidget(const Center(child: BackButton())); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); @@ -84,7 +85,7 @@ void main() { )); }); - testWidgets('debugCheckHasScaffold control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasScaffold control test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -136,7 +137,7 @@ void main() { )); }); - testWidgets('debugCheckHasScaffoldMessenger control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasScaffoldMessenger control test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); final SnackBar snackBar = SnackBar( diff --git a/packages/flutter/test/material/desktop_text_selection_toolbar_button_test.dart b/packages/flutter/test/material/desktop_text_selection_toolbar_button_test.dart index 80a82acaa25bb..e025948ea09d0 100644 --- a/packages/flutter/test/material/desktop_text_selection_toolbar_button_test.dart +++ b/packages/flutter/test/material/desktop_text_selection_toolbar_button_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('can press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can press', (WidgetTester tester) async { bool pressed = false; await tester.pumpWidget( MaterialApp( @@ -29,7 +30,7 @@ void main() { expect(pressed, true); }); - testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passing null to onPressed disables the button', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( diff --git a/packages/flutter/test/material/desktop_text_selection_toolbar_test.dart b/packages/flutter/test/material/desktop_text_selection_toolbar_test.dart index 3052356febf2f..3a0652b7b33cd 100644 --- a/packages/flutter/test/material/desktop_text_selection_toolbar_test.dart +++ b/packages/flutter/test/material/desktop_text_selection_toolbar_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('positions itself at the anchor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at the anchor', (WidgetTester tester) async { // An arbitrary point on the screen to position at. const Offset anchor = Offset(30.0, 40.0); diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 7e467635a6eb5..0a162059412d9 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; MaterialApp _buildAppWithDialog( @@ -69,7 +69,7 @@ void main() { final ThemeData material3Theme = ThemeData(useMaterial3: true, brightness: Brightness.dark); final ThemeData material2Theme = ThemeData(useMaterial3: false, brightness: Brightness.dark); - testWidgets('Dialog is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog is scrollable', (WidgetTester tester) async { bool didPressOk = false; final AlertDialog dialog = AlertDialog( content: Container( @@ -96,7 +96,7 @@ void main() { expect(didPressOk, true); }); - testWidgets('Dialog background color from AlertDialog', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog background color from AlertDialog', (WidgetTester tester) async { const Color customColor = Colors.pink; const AlertDialog dialog = AlertDialog( backgroundColor: customColor, @@ -111,7 +111,7 @@ void main() { expect(materialWidget.color, customColor); }); - testWidgets('Dialog Defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog Defaults', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( title: Text('Title'), content: Text('Y'), @@ -146,7 +146,7 @@ void main() { expect(material3Widget.elevation, 6.0); }); - testWidgets('Dialog.fullscreen Defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog.fullscreen Defaults', (WidgetTester tester) async { const String dialogTextM2 = 'Fullscreen Dialog - M2'; const String dialogTextM3 = 'Fullscreen Dialog - M3'; @@ -193,7 +193,7 @@ void main() { expect(find.text(dialogTextM3), findsNothing); }); - testWidgets('Custom dialog elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog elevation', (WidgetTester tester) async { const double customElevation = 12.0; const Color shadowColor = Color(0xFF000001); const Color surfaceTintColor = Color(0xFF000002); @@ -214,7 +214,7 @@ void main() { expect(materialWidget.surfaceTintColor, surfaceTintColor); }); - testWidgets('Custom Title Text Style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Title Text Style', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -231,7 +231,7 @@ void main() { expect(title.text.style, titleTextStyle); }); - testWidgets('Custom Content Text Style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Content Text Style', (WidgetTester tester) async { const String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -248,7 +248,7 @@ void main() { expect(content.text.style, contentTextStyle); }); - testWidgets('AlertDialog custom clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog custom clipBehavior', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( actions: <Widget>[], clipBehavior: Clip.antiAlias, @@ -262,7 +262,7 @@ void main() { expect(materialWidget.clipBehavior, Clip.antiAlias); }); - testWidgets('SimpleDialog custom clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SimpleDialog custom clipBehavior', (WidgetTester tester) async { const SimpleDialog dialog = SimpleDialog( clipBehavior: Clip.antiAlias, children: <Widget>[], @@ -276,7 +276,7 @@ void main() { expect(materialWidget.clipBehavior, Clip.antiAlias); }); - testWidgets('Custom dialog shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog shape', (WidgetTester tester) async { const RoundedRectangleBorder customBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); const AlertDialog dialog = AlertDialog( @@ -292,7 +292,7 @@ void main() { expect(materialWidget.shape, customBorder); }); - testWidgets('Null dialog shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Null dialog shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], @@ -306,7 +306,7 @@ void main() { expect(materialWidget.shape, theme.useMaterial3 ? _defaultM3DialogShape : _defaultM2DialogShape); }); - testWidgets('Rectangular dialog shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rectangular dialog shape', (WidgetTester tester) async { const ShapeBorder customBorder = Border(); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], @@ -321,7 +321,7 @@ void main() { expect(materialWidget.shape, customBorder); }); - testWidgets('Custom dialog alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog alignment', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], alignment: Alignment.bottomLeft, @@ -338,7 +338,7 @@ void main() { expect(bottomLeft.dy, 576.0); }); - testWidgets('Simple dialog control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple dialog control test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -381,7 +381,7 @@ void main() { expect(await result, equals(42)); }); - testWidgets('Can show dialog using navigator global key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can show dialog using navigator global key', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -421,7 +421,7 @@ void main() { expect(await result, equals(42)); }); - testWidgets('Custom padding on SimpleDialogOption', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom padding on SimpleDialogOption', (WidgetTester tester) async { const EdgeInsets customPadding = EdgeInsets.fromLTRB(4, 10, 8, 6); final SimpleDialog dialog = SimpleDialog( title: const Text('Title'), @@ -447,7 +447,7 @@ void main() { expect(textRect.bottom, dialogRect.bottom - customPadding.bottom); }); - testWidgets('Barrier dismissible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Barrier dismissible', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -508,7 +508,7 @@ void main() { }); - testWidgets('Barrier color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Barrier color', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center(child: Text('Test')), @@ -539,7 +539,7 @@ void main() { expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink); }); - testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog hides underlying semantics tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const String buttonText = 'A button covered by dialog overlay'; await tester.pumpWidget( @@ -575,7 +575,7 @@ void main() { semantics.dispose(); }); - testWidgets('AlertDialog.actionsPadding defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog.actionsPadding defaults', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), @@ -608,7 +608,7 @@ void main() { expect(actionsSize.width, dialogSize.width); }); - testWidgets('AlertDialog.actionsPadding surrounds actions with padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog.actionsPadding surrounds actions with padding', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), @@ -644,7 +644,7 @@ void main() { expect(actionsSize.width, dialogSize.width - (30.0 * 2)); }); - testWidgets('AlertDialog.buttonPadding defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog.buttonPadding defaults', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -742,7 +742,7 @@ void main() { ); // right }); - testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog.buttonPadding custom values', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -944,7 +944,7 @@ void main() { ]; for (final double textScaleFactor in textScaleFactors) { - testWidgets('AlertDialog padding is correct when only icon and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( icon: icon, actions: actions, @@ -996,7 +996,7 @@ void main() { ); }); - testWidgets('AlertDialog padding is correct when only icon, title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon, title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( icon: icon, title: title, @@ -1068,7 +1068,7 @@ void main() { }); for (final bool isM3 in <bool>[true, false]) { - testWidgets('AlertDialog padding is correct when only icon, content and actions are specified [textScaleFactor]=$textScaleFactor [isM3]=$isM3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon, content and actions are specified [textScaleFactor]=$textScaleFactor [isM3]=$isM3', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( icon: icon, content: content, @@ -1140,7 +1140,7 @@ void main() { }); } - testWidgets('AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: title, actions: actions, @@ -1192,7 +1192,7 @@ void main() { ); }); - testWidgets('AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( content: content, actions: actions, @@ -1244,7 +1244,7 @@ void main() { ); }); - testWidgets('AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: title, content: content, @@ -1315,7 +1315,7 @@ void main() { ); }); - testWidgets('SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final SimpleDialog dialog = SimpleDialog( children: children, ); @@ -1348,7 +1348,7 @@ void main() { ); }); - testWidgets('SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final SimpleDialog dialog = SimpleDialog( title: title, children: children, @@ -1402,7 +1402,7 @@ void main() { } }); - testWidgets('Dialogs can set the vertical direction of overflowing actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialogs can set the vertical direction of overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -1437,7 +1437,7 @@ void main() { expect(buttonTwoRect.bottom, lessThanOrEqualTo(buttonOneRect.top)); }); - testWidgets('Dialogs have no spacing by default for overflowing actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialogs have no spacing by default for overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -1470,7 +1470,7 @@ void main() { expect(buttonOneRect.bottom, buttonTwoRect.top); }); - testWidgets('Dialogs can set the button spacing of overflowing actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialogs can set the button spacing of overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -1504,7 +1504,7 @@ void main() { expect(buttonOneRect.bottom, buttonTwoRect.top - 10.0); }); - testWidgets('Dialogs can set the alignment of the OverflowBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialogs can set the alignment of the OverflowBar', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -1538,7 +1538,7 @@ void main() { expect(buttonOneRect.center.dx, buttonTwoRect.center.dx); }); - testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async { late BuildContext outerContext; late BuildContext routeContext; late BuildContext dialogContext; @@ -1593,7 +1593,7 @@ void main() { expect(MediaQuery.of(dialogContext).viewInsets, EdgeInsets.zero); }); - testWidgets('Dialog widget insets by viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog widget insets by viewInsets', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData( @@ -1627,7 +1627,7 @@ void main() { ); }); - testWidgets('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async { // The default testing screen (800, 600) const Rect screenRect = Rect.fromLTRB(0.0, 0.0, 800.0, 600.0); @@ -1667,7 +1667,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/78229. - testWidgets('AlertDialog has correct semantics for content in iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog has correct semantics for content in iOS', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1732,7 +1732,7 @@ void main() { semantics.dispose(); }); - testWidgets('AlertDialog widget always contains alert route semantics for android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog widget always contains alert route semantics for android', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1788,7 +1788,7 @@ void main() { semantics.dispose(); }); - testWidgets('SimpleDialog does not introduce additional node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SimpleDialog does not introduce additional node', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1831,7 +1831,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/78229. - testWidgets('SimpleDialog has correct semantics for title in iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SimpleDialog has correct semantics for title in iOS', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1904,7 +1904,7 @@ void main() { semantics.dispose(); }); - testWidgets('Dismissible.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final List<int> dismissedItems = <int>[]; @@ -2035,7 +2035,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/28505. - testWidgets('showDialog only gets Theme from context on the first call', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog only gets Theme from context on the first call', (WidgetTester tester) async { Widget buildFrame(Key builderKey) { return MaterialApp( home: Center( @@ -2072,7 +2072,7 @@ void main() { await tester.pump(); }); - testWidgets('showDialog safe area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog safe area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -2113,7 +2113,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog uses root navigator by default', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -2148,7 +2148,7 @@ void main() { expect(nestedObserver.dialogCount, 0); }); - testWidgets('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -2184,7 +2184,7 @@ void main() { expect(nestedObserver.dialogCount, 1); }); - testWidgets('showDialog throws a friendly user message when context is not active', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog throws a friendly user message when context is not active', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/12467 await tester.pumpWidget( const MaterialApp( @@ -2220,7 +2220,7 @@ void main() { }); group('showDialog avoids overlapping display features', () { - testWidgets('positioning with anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -2258,7 +2258,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning with Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with Directionality', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -2298,7 +2298,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -2337,7 +2337,7 @@ void main() { }); group('AlertDialog.scrollable: ', () { - testWidgets('Title is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Title is scrollable', (WidgetTester tester) async { final Key titleKey = UniqueKey(); final AlertDialog dialog = AlertDialog( title: Container( @@ -2357,7 +2357,7 @@ void main() { expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0))); }); - testWidgets('Content is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Content is scrollable', (WidgetTester tester) async { final Key contentKey = UniqueKey(); final AlertDialog dialog = AlertDialog( content: Container( @@ -2377,7 +2377,7 @@ void main() { expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0))); }); - testWidgets('Title and content are scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Title and content are scrollable', (WidgetTester tester) async { final Key titleKey = UniqueKey(); final Key contentKey = UniqueKey(); final AlertDialog dialog = AlertDialog( @@ -2416,7 +2416,7 @@ void main() { }); }); - testWidgets('Dialog with RouteSettings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog with RouteSettings', (WidgetTester tester) async { late RouteSettings currentRouteSetting; await tester.pumpWidget( @@ -2470,7 +2470,7 @@ void main() { expect(currentRouteSetting.name, '/'); }); - testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showDialog - custom barrierLabel', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -2510,7 +2510,7 @@ void main() { semantics.dispose(); }); - testWidgets('DialogRoute is state restorable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DialogRoute is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -2540,7 +2540,7 @@ void main() { expect(find.byType(AlertDialog), findsOneWidget); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('AlertDialog.actionsAlignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlertDialog.actionsAlignment', (WidgetTester tester) async { final Key actionKey = UniqueKey(); Widget buildFrame(MainAxisAlignment? alignment) { @@ -2593,7 +2593,7 @@ void main() { expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20); }); - testWidgets('Uses closed loop focus traversal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses closed loop focus traversal', (WidgetTester tester) async { final FocusNode okNode = FocusNode(); final FocusNode cancelNode = FocusNode(); @@ -2659,9 +2659,12 @@ void main() { expect(await previousFocus(), true); expect(okNode.hasFocus, true); expect(cancelNode.hasFocus, false); + + cancelNode.dispose(); + okNode.dispose(); }); - testWidgets('Adaptive AlertDialog shows correct widget on each platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adaptive AlertDialog shows correct widget on each platform', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog.adaptive( content: Container( height: 5000.0, @@ -2703,7 +2706,7 @@ void main() { } }); - testWidgets('showAdaptiveDialog should not allow dismiss on barrier on iOS by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showAdaptiveDialog should not allow dismiss on barrier on iOS by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), @@ -2763,9 +2766,11 @@ void main() { expect(find.text('Dialog2'), findsOneWidget); }); - testWidgets('Uses open focus traversal when overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses open focus traversal when overridden', (WidgetTester tester) async { final FocusNode okNode = FocusNode(); + addTearDown(okNode.dispose); final FocusNode cancelNode = FocusNode(); + addTearDown(cancelNode.dispose); Future<bool> nextFocus() async { final bool result = Actions.invoke( diff --git a/packages/flutter/test/material/dialog_theme_test.dart b/packages/flutter/test/material/dialog_theme_test.dart index f215649d4826c..e327542e005b7 100644 --- a/packages/flutter/test/material/dialog_theme_test.dart +++ b/packages/flutter/test/material/dialog_theme_test.dart @@ -7,9 +7,11 @@ @Tags(<String>['reduced-test-set']) library; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; MaterialApp _appWithDialog(WidgetTester tester, Widget dialog, { ThemeData? theme }) { return MaterialApp( @@ -53,7 +55,7 @@ void main() { expect(identical(DialogTheme.lerp(theme, theme, 0.5), theme), true); }); - testWidgets('Dialog Theme implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog Theme implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DialogTheme( backgroundColor: Color(0xff123456), @@ -82,7 +84,7 @@ void main() { ]); }); - testWidgets('Dialog background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog background color', (WidgetTester tester) async { const Color customColor = Colors.pink; const AlertDialog dialog = AlertDialog( title: Text('Title'), @@ -98,7 +100,7 @@ void main() { expect(materialWidget.color, customColor); }); - testWidgets('Custom dialog elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog elevation', (WidgetTester tester) async { const double customElevation = 12.0; const Color shadowColor = Color(0xFF000001); const Color surfaceTintColor = Color(0xFF000002); @@ -126,7 +128,7 @@ void main() { expect(materialWidget.surfaceTintColor, surfaceTintColor); }); - testWidgets('Custom dialog shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog shape', (WidgetTester tester) async { const RoundedRectangleBorder customBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); const AlertDialog dialog = AlertDialog( @@ -145,7 +147,7 @@ void main() { expect(materialWidget.shape, customBorder); }); - testWidgets('Custom dialog alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom dialog alignment', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( title: Text('Title'), actions: <Widget>[ ], @@ -165,7 +167,33 @@ void main() { expect(bottomLeft.dy, 576.0); }); - testWidgets('Dialog alignment takes priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Dialog alignment takes priority over theme', (WidgetTester tester) async { + const AlertDialog dialog = AlertDialog( + title: Text('Title'), + actions: <Widget>[ ], + alignment: Alignment.topRight, + ); + final ThemeData theme = ThemeData( + useMaterial3: true, + dialogTheme: const DialogTheme(alignment: Alignment.bottomLeft), + ); + + await tester.pumpWidget( + _appWithDialog(tester, dialog, theme: theme), + ); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Offset bottomLeft = tester.getBottomLeft( + find.descendant(of: find.byType(Dialog), matching: find.byType(Material)), + ); + expect(bottomLeft.dx, 480.0); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(bottomLeft.dy, 124.0); + } + }); + + testWidgetsWithLeakTracking('Material2 - Dialog alignment takes priority over theme', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( title: Text('Title'), actions: <Widget>[ ], @@ -186,7 +214,29 @@ void main() { expect(bottomLeft.dy, 104.0); }); - testWidgets('Custom dialog shape matches golden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Custom dialog shape matches golden', (WidgetTester tester) async { + const RoundedRectangleBorder customBorder = + RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); + const AlertDialog dialog = AlertDialog( + title: Text('Title'), + actions: <Widget>[ ], + ); + final ThemeData theme = ThemeData( + useMaterial3: true, + dialogTheme: const DialogTheme(shape: customBorder), + ); + + await tester.pumpWidget(_appWithDialog(tester, dialog, theme: theme)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('m3_dialog_theme.dialog_with_custom_border.png'), + ); + }); + + testWidgetsWithLeakTracking('Material2 - Custom dialog shape matches golden', (WidgetTester tester) async { const RoundedRectangleBorder customBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); const AlertDialog dialog = AlertDialog( @@ -201,11 +251,11 @@ void main() { await expectLater( find.byKey(_painterKey), - matchesGoldenFile('dialog_theme.dialog_with_custom_border.png'), + matchesGoldenFile('m2_dialog_theme.dialog_with_custom_border.png'), ); }); - testWidgets('Custom Icon Color - Constructor Param - highest preference', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Icon Color - Constructor Param - highest preference', (WidgetTester tester) async { const Color iconColor = Colors.pink, dialogThemeColor = Colors.green, iconThemeColor = Colors.yellow; final ThemeData theme = ThemeData( iconTheme: const IconThemeData(color: iconThemeColor), @@ -226,7 +276,7 @@ void main() { expect(text.text.style!.color, iconColor); }); - testWidgets('Custom Icon Color - Dialog Theme - preference over Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Icon Color - Dialog Theme - preference over Theme', (WidgetTester tester) async { const Color dialogThemeColor = Colors.green, iconThemeColor = Colors.yellow; final ThemeData theme = ThemeData( iconTheme: const IconThemeData(color: iconThemeColor), @@ -246,9 +296,8 @@ void main() { expect(text.text.style!.color, dialogThemeColor); }); - testWidgets('Custom Icon Color - Theme - lowest preference', (WidgetTester tester) async { - const Color iconThemeColor = Colors.yellow; - final ThemeData theme = ThemeData(useMaterial3: false, iconTheme: const IconThemeData(color: iconThemeColor)); + testWidgetsWithLeakTracking('Material3 - Custom Icon Color - Theme - lowest preference', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); const AlertDialog dialog = AlertDialog( icon: Icon(Icons.ac_unit), actions: <Widget>[ ], @@ -260,11 +309,12 @@ void main() { // first is Text('X') final RichText text = tester.widget(find.byType(RichText).last); - expect(text.text.style!.color, iconThemeColor); + expect(text.text.style!.color, theme.colorScheme.secondary); }); - testWidgets('Custom Icon Color - Theme - lowest preference for M3', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); + testWidgetsWithLeakTracking('Material2 - Custom Icon Color - Theme - lowest preference', (WidgetTester tester) async { + const Color iconThemeColor = Colors.yellow; + final ThemeData theme = ThemeData(useMaterial3: false, iconTheme: const IconThemeData(color: iconThemeColor)); const AlertDialog dialog = AlertDialog( icon: Icon(Icons.ac_unit), actions: <Widget>[ ], @@ -276,10 +326,10 @@ void main() { // first is Text('X') final RichText text = tester.widget(find.byType(RichText).last); - expect(text.text.style!.color, theme.colorScheme.secondary); + expect(text.text.style!.color, iconThemeColor); }); - testWidgets('Custom Title Text Style - Constructor Param', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Title Text Style - Constructor Param', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -296,7 +346,7 @@ void main() { expect(title.text.style, titleTextStyle); }); - testWidgets('Custom Title Text Style - Dialog Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Title Text Style - Dialog Theme', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -313,13 +363,24 @@ void main() { expect(title.text.style, titleTextStyle); }); - testWidgets('Custom Title Text Style - Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Custom Title Text Style - Theme', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); - const AlertDialog dialog = AlertDialog( - title: Text(titleText), - actions: <Widget>[ ], - ); + const AlertDialog dialog = AlertDialog(title: Text(titleText)); + final ThemeData theme = ThemeData(useMaterial3: true, textTheme: const TextTheme(headlineSmall: titleTextStyle)); + + await tester.pumpWidget(_appWithDialog(tester, dialog, theme: theme)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final RenderParagraph title = _getTextRenderObject(tester, titleText); + expect(title.text.style!.color, titleTextStyle.color); + }); + + testWidgetsWithLeakTracking('Material2 - Custom Title Text Style - Theme', (WidgetTester tester) async { + const String titleText = 'Title'; + const TextStyle titleTextStyle = TextStyle(color: Colors.pink); + const AlertDialog dialog = AlertDialog(title: Text(titleText)); final ThemeData theme = ThemeData(useMaterial3: false, textTheme: const TextTheme(titleLarge: titleTextStyle)); await tester.pumpWidget(_appWithDialog(tester, dialog, theme: theme)); @@ -330,7 +391,7 @@ void main() { expect(title.text.style!.color, titleTextStyle.color); }); - testWidgets('Simple Dialog - Custom Title Text Style - Constructor Param', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple Dialog - Custom Title Text Style - Constructor Param', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const SimpleDialog dialog = SimpleDialog( @@ -346,7 +407,7 @@ void main() { expect(title.text.style, titleTextStyle); }); - testWidgets('Simple Dialog - Custom Title Text Style - Dialog Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple Dialog - Custom Title Text Style - Dialog Theme', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const SimpleDialog dialog = SimpleDialog( @@ -362,7 +423,7 @@ void main() { expect(title.text.style, titleTextStyle); }); - testWidgets('Simple Dialog - Custom Title Text Style - Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple Dialog - Custom Title Text Style - Theme', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const SimpleDialog dialog = SimpleDialog( @@ -378,7 +439,7 @@ void main() { expect(title.text.style!.color, titleTextStyle.color); }); - testWidgets('Custom Content Text Style - Constructor Param', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Content Text Style - Constructor Param', (WidgetTester tester) async { const String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -395,7 +456,7 @@ void main() { expect(content.text.style, contentTextStyle); }); - testWidgets('Custom Content Text Style - Dialog Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Content Text Style - Dialog Theme', (WidgetTester tester) async { const String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( @@ -412,13 +473,24 @@ void main() { expect(content.text.style, contentTextStyle); }); - testWidgets('Custom Content Text Style - Theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Custom Content Text Style - Theme', (WidgetTester tester) async { const String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); - const AlertDialog dialog = AlertDialog( - content: Text(contentText), - actions: <Widget>[ ], - ); + const AlertDialog dialog = AlertDialog(content: Text(contentText),); + final ThemeData theme = ThemeData(useMaterial3: true, textTheme: const TextTheme(bodyMedium: contentTextStyle)); + + await tester.pumpWidget(_appWithDialog(tester, dialog, theme: theme)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final RenderParagraph content = _getTextRenderObject(tester, contentText); + expect(content.text.style!.color, contentTextStyle.color); + }); + + testWidgetsWithLeakTracking('Material2 - Custom Content Text Style - Theme', (WidgetTester tester) async { + const String contentText = 'Content'; + const TextStyle contentTextStyle = TextStyle(color: Colors.pink); + const AlertDialog dialog = AlertDialog(content: Text(contentText)); final ThemeData theme = ThemeData(useMaterial3: false, textTheme: const TextTheme(titleMedium: contentTextStyle)); await tester.pumpWidget(_appWithDialog(tester, dialog, theme: theme)); diff --git a/packages/flutter/test/material/divider_test.dart b/packages/flutter/test/material/divider_test.dart index 7136767808305..35cd3118883a4 100644 --- a/packages/flutter/test/material/divider_test.dart +++ b/packages/flutter/test/material/divider_test.dart @@ -4,16 +4,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Divider control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Divider control test', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Center(child: Divider()), + ), + ); + final RenderBox box = tester.firstRenderObject(find.byType(Divider)); + expect(box.size.height, 16.0); + final Container container = tester.widget(find.byType(Container)); + final BoxDecoration decoration = container.decoration! as BoxDecoration; + expect(decoration.border!.bottom.width, 1.0); + }); + + testWidgetsWithLeakTracking('Material2 - Divider control test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), - home: const Center( - child: Divider(), - ), + home: const Center(child: Divider()), ), ); final RenderBox box = tester.firstRenderObject(find.byType(Divider)); @@ -23,15 +35,11 @@ void main() { expect(decoration.border!.bottom.width, 0.0); }); - testWidgets('Divider custom thickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Divider custom thickness', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: Divider( - thickness: 5.0, - ), - ), + child: Center(child: Divider(thickness: 5.0)), ), ); final Container container = tester.widget(find.byType(Container)); @@ -39,7 +47,7 @@ void main() { expect(decoration.border!.bottom.width, 5.0); }); - testWidgets('Horizontal divider custom indentation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal divider custom indentation', (WidgetTester tester) async { const double customIndent = 10.0; Rect dividerRect; Rect lineRect; @@ -47,11 +55,7 @@ void main() { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: Divider( - indent: customIndent, - ), - ), + child: Center(child: Divider(indent: customIndent)), ), ); // The divider line is drawn with a DecoratedBox with a border @@ -63,11 +67,7 @@ void main() { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: Divider( - endIndent: customIndent, - ), - ), + child: Center(child: Divider(endIndent: customIndent)), ), ); dividerRect = tester.getRect(find.byType(Divider)); @@ -92,13 +92,26 @@ void main() { expect(lineRect.right, dividerRect.right - customIndent); }); - testWidgets('Vertical Divider Test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Vertical Divider Test', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Center(child: VerticalDivider()), + ), + ); + final RenderBox box = tester.firstRenderObject(find.byType(VerticalDivider)); + expect(box.size.width, 16.0); + final Container container = tester.widget(find.byType(Container)); + final BoxDecoration decoration = container.decoration! as BoxDecoration; + final Border border = decoration.border! as Border; + expect(border.left.width, 1.0); + }); + + testWidgetsWithLeakTracking('Material2 - Vertical Divider Test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), - home: const Center( - child: VerticalDivider(), - ), + home: const Center(child: VerticalDivider()), ), ); final RenderBox box = tester.firstRenderObject(find.byType(VerticalDivider)); @@ -109,15 +122,11 @@ void main() { expect(border.left.width, 0.0); }); - testWidgets('Divider custom thickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Divider custom thickness', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: VerticalDivider( - thickness: 5.0, - ), - ), + child: Center(child: VerticalDivider(thickness: 5.0)), ), ); final Container container = tester.widget(find.byType(Container)); @@ -126,10 +135,11 @@ void main() { expect(border.left.width, 5.0); }); - testWidgets('Vertical Divider Test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical Divider Test 2', (WidgetTester tester) async { await tester.pumpWidget( - const MaterialApp( - home: Material( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: const Material( child: SizedBox( height: 24.0, child: Row( @@ -150,7 +160,7 @@ void main() { expect(find.byType(VerticalDivider), paints..path(strokeWidth: 0.0)); }); - testWidgets('Vertical divider custom indentation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical divider custom indentation', (WidgetTester tester) async { const double customIndent = 10.0; Rect dividerRect; Rect lineRect; @@ -158,11 +168,7 @@ void main() { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: VerticalDivider( - indent: customIndent, - ), - ), + child: Center(child: VerticalDivider(indent: customIndent)), ), ); // The divider line is drawn with a DecoratedBox with a border @@ -174,11 +180,7 @@ void main() { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, - child: Center( - child: VerticalDivider( - endIndent: customIndent, - ), - ), + child: Center(child: VerticalDivider(endIndent: customIndent)), ), ); dividerRect = tester.getRect(find.byType(VerticalDivider)); @@ -204,7 +206,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/39533 - testWidgets('createBorderSide does not throw exception with null context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('createBorderSide does not throw exception with null context', (WidgetTester tester) async { // Passing a null context used to throw an exception but no longer does. expect(() => Divider.createBorderSide(null), isNot(throwsAssertionError)); expect(() => Divider.createBorderSide(null), isNot(throwsNoSuchMethodError)); diff --git a/packages/flutter/test/material/divider_theme_test.dart b/packages/flutter/test/material/divider_theme_test.dart index 49afe07313a0d..6611d6bc750da 100644 --- a/packages/flutter/test/material/divider_theme_test.dart +++ b/packages/flutter/test/material/divider_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('DividerThemeData copyWith, ==, hashCode basics', () { @@ -21,19 +22,19 @@ void main() { expect(dividerTheme.endIndent, null); }); - testWidgets('Default DividerThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default DividerThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DividerThemeData().debugFillProperties(builder); final List<String> description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); expect(description, <String>[]); }); - testWidgets('DividerThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DividerThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DividerThemeData( color: Color(0xFFFFFFFF), @@ -44,9 +45,9 @@ void main() { ).debugFillProperties(builder); final List<String> description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); expect(description, <String>[ 'color: Color(0xffffffff)', @@ -57,8 +58,8 @@ void main() { ]); }); - group('Horizontal Divider', () { - testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { + group('Material3 - Horizontal Divider', () { + testWidgetsWithLeakTracking('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, @@ -82,7 +83,7 @@ void main() { expect(lineRect.right, dividerRect.right); }); - testWidgets('Uses values from DividerThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses values from DividerThemeData', (WidgetTester tester) async { final DividerThemeData dividerTheme = _dividerTheme(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true, dividerTheme: dividerTheme), @@ -105,7 +106,7 @@ void main() { expect(lineRect.right, dividerRect.right - dividerTheme.endIndent!); }); - testWidgets('DividerTheme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DividerTheme overrides defaults', (WidgetTester tester) async { final DividerThemeData dividerTheme = _dividerTheme(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true), @@ -123,7 +124,7 @@ void main() { expect(decoration.border!.bottom.color, dividerTheme.color); }); - testWidgets('Widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget properties take priority over theme', (WidgetTester tester) async { const Color color = Colors.purple; const double height = 10.0; const double thickness = 5.0; @@ -159,8 +160,8 @@ void main() { }); }); - group('Vertical Divider', () { - testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { + group('Material3 - Vertical Divider', () { + testWidgetsWithLeakTracking('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, @@ -185,7 +186,7 @@ void main() { expect(lineRect.bottom, dividerRect.bottom); }); - testWidgets('Uses values from DividerThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses values from DividerThemeData', (WidgetTester tester) async { final DividerThemeData dividerTheme = _dividerTheme(); await tester.pumpWidget(MaterialApp( theme: ThemeData(dividerTheme: dividerTheme), @@ -209,7 +210,7 @@ void main() { expect(lineRect.bottom, dividerRect.bottom - dividerTheme.endIndent!); }); - testWidgets('DividerTheme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DividerTheme overrides defaults', (WidgetTester tester) async { final DividerThemeData dividerTheme = _dividerTheme(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true), @@ -228,7 +229,7 @@ void main() { expect(border.left.color, dividerTheme.color); }); - testWidgets('Widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget properties take priority over theme', (WidgetTester tester) async { const Color color = Colors.purple; const double width = 10.0; const double thickness = 5.0; @@ -270,8 +271,8 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - group('Horizontal Divider', () { - testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { + group('Material2 - Horizontal Divider', () { + testWidgetsWithLeakTracking('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -295,7 +296,7 @@ void main() { expect(lineRect.right, dividerRect.right); }); - testWidgets('DividerTheme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DividerTheme overrides defaults', (WidgetTester tester) async { final DividerThemeData theme = _dividerTheme(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -313,8 +314,8 @@ void main() { }); }); - group('Vertical Divider', () { - testWidgets('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { + group('Material2 - Vertical Divider', () { + testWidgetsWithLeakTracking('Passing no DividerThemeData returns defaults', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( diff --git a/packages/flutter/test/material/drawer_button_test.dart b/packages/flutter/test/material/drawer_button_test.dart index 2979788ed5015..f65557d8c27d4 100644 --- a/packages/flutter/test/material/drawer_button_test.dart +++ b/packages/flutter/test/material/drawer_button_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('DrawerButton control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerButton control test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -28,7 +29,7 @@ void main() { expect(find.byType(Drawer), findsOneWidget); }); - testWidgets('DrawerButton onPressed overrides default end drawer open behaviour', + testWidgetsWithLeakTracking('DrawerButton onPressed overrides default end drawer open behaviour', (WidgetTester tester) async { bool customCallbackWasCalled = false; await tester.pumpWidget( @@ -57,7 +58,7 @@ void main() { expect(customCallbackWasCalled, true); }); - testWidgets('DrawerButton icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerButton icon', (WidgetTester tester) async { final Key androidKey = UniqueKey(); final Key iOSKey = UniqueKey(); final Key linuxKey = UniqueKey(); @@ -111,7 +112,7 @@ void main() { expect(windowsIcon.icon == androidIcon.icon, isTrue); }); - testWidgets('DrawerButton color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerButton color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -132,7 +133,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('DrawerButton semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerButton semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( const MaterialApp( @@ -169,7 +170,7 @@ void main() { handle.dispose(); }, variant: TargetPlatformVariant.all()); - testWidgets('EndDrawerButton control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EndDrawerButton control test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -190,7 +191,7 @@ void main() { expect(find.byType(Drawer), findsOneWidget); }); - testWidgets('EndDrawerButton semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EndDrawerButton semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( const MaterialApp( @@ -226,7 +227,7 @@ void main() { handle.dispose(); }, variant: TargetPlatformVariant.all()); - testWidgets('EndDrawerButton color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EndDrawerButton color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -247,7 +248,7 @@ void main() { expect(iconText.text.style!.color, Colors.red); }); - testWidgets('EndDrawerButton onPressed overrides default end drawer open behaviour', + testWidgetsWithLeakTracking('EndDrawerButton onPressed overrides default end drawer open behaviour', (WidgetTester tester) async { bool customCallbackWasCalled = false; await tester.pumpWidget( diff --git a/packages/flutter/test/material/drawer_test.dart b/packages/flutter/test/material/drawer_test.dart index faf3c2522bb09..a83b1bef962bd 100644 --- a/packages/flutter/test/material/drawer_test.dart +++ b/packages/flutter/test/material/drawer_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Drawer control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer control test', (WidgetTester tester) async { const Key containerKey = Key('container'); await tester.pumpWidget( @@ -57,7 +58,7 @@ void main() { expect(find.text('header'), findsOneWidget); }); - testWidgets('Drawer dismiss barrier has label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer dismiss barrier has label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( const MaterialApp( @@ -81,7 +82,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Drawer dismiss barrier has no label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer dismiss barrier has no label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( const MaterialApp( @@ -105,7 +106,7 @@ void main() { semantics.dispose(); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('Scaffold drawerScrimColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold drawerScrimColor', (WidgetTester tester) async { // The scrim is a Container within a Semantics node labeled "Dismiss", // within a DrawerController. Sorry. Container getScrim() { @@ -167,7 +168,7 @@ void main() { expect(find.byType(Drawer), findsNothing); }); - testWidgets('Open/close drawers by flinging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Open/close drawers by flinging', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -212,7 +213,7 @@ void main() { expect(state.isEndDrawerOpen, equals(false)); }); - testWidgets('Scaffold.drawer - null restorationId ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.drawer - null restorationId ', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -235,7 +236,7 @@ void main() { expect(find.text('drawer'), findsNothing); }); - testWidgets('Scaffold.endDrawer - null restorationId ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.endDrawer - null restorationId ', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -258,7 +259,7 @@ void main() { expect(find.text('endDrawer'), findsNothing); }); - testWidgets('Scaffold.drawer state restoration test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.drawer state restoration test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -289,7 +290,7 @@ void main() { expect(find.text('drawer'), findsOneWidget); }); - testWidgets('Scaffold.endDrawer state restoration test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.endDrawer state restoration test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -320,7 +321,7 @@ void main() { expect(find.text('endDrawer'), findsOneWidget); }); - testWidgets('Both drawer and endDrawer state restoration test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Both drawer and endDrawer state restoration test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -381,7 +382,7 @@ void main() { expect(find.text('endDrawer'), findsOneWidget); }); - testWidgets('ScaffoldState close drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldState close drawer', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -404,7 +405,7 @@ void main() { expect(find.text('Drawer'), findsNothing); }); - testWidgets('ScaffoldState close drawer do not crash if drawer is already closed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldState close drawer do not crash if drawer is already closed', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -423,7 +424,7 @@ void main() { expect(find.text('Drawer'), findsNothing); }); - testWidgets('Disposing drawer does not crash if drawer is open and framework is locked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing drawer does not crash if drawer is open and framework is locked', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/34978 addTearDown(tester.view.reset); tester.view.physicalSize = const Size(1800.0, 2400.0); @@ -464,7 +465,7 @@ void main() { expect(find.byType(BackButton), findsNothing); }); - testWidgets('Disposing endDrawer does not crash if endDrawer is open and framework is locked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing endDrawer does not crash if endDrawer is open and framework is locked', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/34978 addTearDown(tester.view.reset); tester.view.physicalSize = const Size(1800.0, 2400.0); @@ -505,7 +506,7 @@ void main() { expect(find.byType(BackButton), findsNothing); }); - testWidgets('ScaffoldState close end drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldState close end drawer', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -528,7 +529,7 @@ void main() { expect(find.text('endDrawer'), findsNothing); }); - testWidgets('Drawer width defaults to Material spec', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer width defaults to Material spec', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -547,7 +548,7 @@ void main() { expect(box.size.width, equals(304.0)); }); - testWidgets('Drawer width can be customized by parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer width can be customized by parameter', (WidgetTester tester) async { const double smallWidth = 200; await tester.pumpWidget( @@ -569,7 +570,7 @@ void main() { expect(box.size.width, equals(smallWidth)); }); - testWidgets('Drawer default shape (ltr)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer default shape (ltr)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -629,7 +630,7 @@ void main() { ); }); - testWidgets('Drawer default shape (rtl)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer default shape (rtl)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -689,7 +690,7 @@ void main() { ); }); - testWidgets('Drawer clip behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer clip behavior', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -745,7 +746,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Drawer default shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer default shape', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -786,7 +787,7 @@ void main() { expect(material.shape, null); }); - testWidgets('Drawer clip behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer clip behavior', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), diff --git a/packages/flutter/test/material/drawer_theme_test.dart b/packages/flutter/test/material/drawer_theme_test.dart index 1a25f288e7f64..80aaccf5d85ba 100644 --- a/packages/flutter/test/material/drawer_theme_test.dart +++ b/packages/flutter/test/material/drawer_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('copyWith, ==, hashCode basics', () { @@ -18,7 +19,7 @@ void main() { expect(identical(DrawerThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Default debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DrawerThemeData().debugFillProperties(builder); @@ -30,7 +31,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('Custom debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DrawerThemeData( backgroundColor: Color(0x00000099), @@ -58,10 +59,9 @@ void main() { ]); }); - testWidgets('Default values are used when no Drawer or DrawerThemeData properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Default values are used when no Drawer or DrawerThemeData properties are specified', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); - final ThemeData theme = ThemeData(); - final bool useMaterial3 = theme.useMaterial3; + final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: theme, @@ -74,24 +74,69 @@ void main() { scaffoldKey.currentState!.openDrawer(); await tester.pumpAndSettle(); - expect(_drawerMaterial(tester).color, useMaterial3 ? theme.colorScheme.surface : null); - expect(_drawerMaterial(tester).elevation, useMaterial3 ? 1.0 : 16.0); - expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor); - expect(_drawerMaterial(tester).surfaceTintColor, useMaterial3 ? theme.colorScheme.surfaceTint : null); + expect(_drawerMaterial(tester).color, null); + expect(_drawerMaterial(tester).elevation, 16.0); + expect(_drawerMaterial(tester).shadowColor, theme.shadowColor); + expect(_drawerMaterial(tester).surfaceTintColor, null); + expect(_drawerMaterial(tester).shape, null); + expect(_scrim(tester).color, Colors.black54); + expect(_drawerRenderBox(tester).size.width, 304.0); + }); + + testWidgetsWithLeakTracking('Material3 - Default values are used when no Drawer or DrawerThemeData properties are specified', (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + final ThemeData theme = ThemeData(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + key: scaffoldKey, + drawer: const Drawer(), + ), + ), + ); + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + + expect(_drawerMaterial(tester).color, theme.colorScheme.surface); + expect(_drawerMaterial(tester).elevation, 1.0); + expect(_drawerMaterial(tester).shadowColor, Colors.transparent); + expect(_drawerMaterial(tester).surfaceTintColor, theme.colorScheme.surfaceTint); expect( _drawerMaterial(tester).shape, - useMaterial3 - ? const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0))) - : null, + const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(right: Radius.circular(16.0))) ); expect(_scrim(tester).color, Colors.black54); expect(_drawerRenderBox(tester).size.width, 304.0); }); - testWidgets('Default values are used when no Drawer or DrawerThemeData properties are specified in end drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Default values are used when no Drawer or DrawerThemeData properties are specified in end drawer', (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + final ThemeData theme = ThemeData(useMaterial3: false); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + key: scaffoldKey, + endDrawer: const Drawer(), + ), + ), + ); + scaffoldKey.currentState!.openEndDrawer(); + await tester.pumpAndSettle(); + + expect(_drawerMaterial(tester).color, null); + expect(_drawerMaterial(tester).elevation, 16.0); + expect(_drawerMaterial(tester).shadowColor, theme.shadowColor); + expect(_drawerMaterial(tester).surfaceTintColor, null); + expect(_drawerMaterial(tester).shape, null); + expect(_scrim(tester).color, Colors.black54); + expect(_drawerRenderBox(tester).size.width, 304.0); + }); + + testWidgetsWithLeakTracking('Material3 - Default values are used when no Drawer or DrawerThemeData properties are specified in end drawer', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); - final ThemeData theme = ThemeData(); - final bool useMaterial3 = theme.useMaterial3; + final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, @@ -104,21 +149,19 @@ void main() { scaffoldKey.currentState!.openEndDrawer(); await tester.pumpAndSettle(); - expect(_drawerMaterial(tester).color, useMaterial3 ? theme.colorScheme.surface : null); - expect(_drawerMaterial(tester).elevation, useMaterial3 ? 1.0 : 16.0); - expect(_drawerMaterial(tester).shadowColor, useMaterial3 ? Colors.transparent : ThemeData().shadowColor); - expect(_drawerMaterial(tester).surfaceTintColor, useMaterial3 ? ThemeData().colorScheme.surfaceTint : null); + expect(_drawerMaterial(tester).color, theme.colorScheme.surface); + expect(_drawerMaterial(tester).elevation, 1.0); + expect(_drawerMaterial(tester).shadowColor, Colors.transparent); + expect(_drawerMaterial(tester).surfaceTintColor, theme.colorScheme.surfaceTint); expect( _drawerMaterial(tester).shape, - useMaterial3 - ? const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0))) - : null, + const RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(16.0))) ); expect(_scrim(tester).color, Colors.black54); expect(_drawerRenderBox(tester).size.width, 304.0); }); - testWidgets('DrawerThemeData values are used when no Drawer properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerThemeData values are used when no Drawer properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const Color scrimColor = Color(0x00000002); const double elevation = 7.0; @@ -159,7 +202,7 @@ void main() { expect(_drawerRenderBox(tester).size.width, width); }); - testWidgets('Drawer values take priority over DrawerThemeData values when both properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer values take priority over DrawerThemeData values when both properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const Color scrimColor = Color(0x00000002); const double elevation = 7.0; @@ -206,7 +249,7 @@ void main() { expect(_drawerRenderBox(tester).size.width, width); }); - testWidgets('DrawerTheme values take priority over ThemeData.drawerTheme values when both properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrawerTheme values take priority over ThemeData.drawerTheme values when both properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const Color scrimColor = Color(0x00000002); const double elevation = 7.0; diff --git a/packages/flutter/test/material/dropdown_form_field_test.dart b/packages/flutter/test/material/dropdown_form_field_test.dart index c46c560e7e3ae..6d8faf8b24a0a 100644 --- a/packages/flutter/test/material/dropdown_form_field_test.dart +++ b/packages/flutter/test/material/dropdown_form_field_test.dart @@ -4,10 +4,10 @@ import 'dart:math' as math; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const List<String> menuItems = <String>['one', 'two', 'three', 'four']; void onChanged<T>(T _) { } @@ -149,7 +149,7 @@ void verifyPaintedShadow(Finder customPaint, int elevation) { void main() { // Regression test for https://github.com/flutter/flutter/issues/87102 - testWidgets('label position test - show hint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show hint', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -200,7 +200,7 @@ void main() { expect(hintEmptyLabel, oneValueLabel); }); - testWidgets('label position test - show disabledHint: disable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show disabledHint: disable', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -238,7 +238,7 @@ void main() { expect(hintEmptyLabel, const Offset(0.0, 12.0)); }); - testWidgets('label position test - show disabledHint: enable + null item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show disabledHint: enable + null item', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -263,7 +263,7 @@ void main() { expect(hintEmptyLabel, const Offset(0.0, 12.0)); }); - testWidgets('label position test - show disabledHint: enable + empty item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show disabledHint: enable + empty item', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -288,7 +288,7 @@ void main() { expect(hintEmptyLabel, const Offset(0.0, 12.0)); }); - testWidgets('label position test - show hint: enable + empty item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show hint: enable + empty item', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -313,7 +313,7 @@ void main() { expect(hintEmptyLabel, const Offset(0.0, 12.0)); }); - testWidgets('label position test - no hint shown: enable + no selected + disabledHint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - no hint shown: enable + no selected + disabledHint', (WidgetTester tester) async { int? value; await tester.pumpWidget( @@ -351,7 +351,7 @@ void main() { expect(hintEmptyLabel, const Offset(0.0, 24.0)); }); - testWidgets('label position test - show selected item: disabled + hint + disabledHint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('label position test - show selected item: disabled + hint + disabledHint', (WidgetTester tester) async { const int value = 1; await tester.pumpWidget( @@ -391,7 +391,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/82910 - testWidgets('null value test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null value test', (WidgetTester tester) async { int? value = 1; await tester.pumpWidget( @@ -443,7 +443,7 @@ void main() { expect(nonEmptyLabel, nullValueLabel); }); - testWidgets('DropdownButtonFormField with autovalidation test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField with autovalidation test', (WidgetTester tester) async { String? value = 'one'; int validateCalled = 0; @@ -492,7 +492,7 @@ void main() { expect(value, equals('three')); }); - testWidgets('DropdownButtonFormField arrow icon aligns with the edge of button when expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField arrow icon aligns with the edge of button when expanded', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); // There shouldn't be overflow when expanded although list contains longer items. @@ -527,7 +527,7 @@ void main() { ); }); - testWidgets('DropdownButtonFormField with isDense:true aligns selected menu item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField with isDense:true aligns selected menu item', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); await tester.pumpWidget( @@ -567,7 +567,7 @@ void main() { } }); - testWidgets('DropdownButtonFormField with isDense:true does not clip large scale text', + testWidgetsWithLeakTracking('DropdownButtonFormField with isDense:true does not clip large scale text', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); const String value = 'two'; @@ -606,7 +606,7 @@ void main() { expect(box.size.height, 72.0); }); - testWidgets('DropdownButtonFormField.isDense is true by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField.isDense is true by default', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/46844 final Key buttonKey = UniqueKey(); const String value = 'two'; @@ -637,7 +637,7 @@ void main() { expect(box.size.height, 48.0); }); - testWidgets('DropdownButtonFormField - custom text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - custom text style', (WidgetTester tester) async { const String value = 'foo'; final UniqueKey itemKey = UniqueKey(); @@ -675,7 +675,7 @@ void main() { expect(richText.text.style!.fontSize, 20.0); }); - testWidgets('DropdownButtonFormField - disabledHint displays when the items list is empty, when items is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - disabledHint displays when the items list is empty, when items is null', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); Widget build({ List<String>? items }) { @@ -722,7 +722,7 @@ void main() { }, ); - testWidgets('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); Widget build({ List<String>? items }) { @@ -742,7 +742,7 @@ void main() { expect(find.text('hint used when disabled'), findsOneWidget); }); - testWidgets('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - disabledHint is null by default', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); Widget build({ List<String>? items }) { @@ -762,7 +762,7 @@ void main() { expect(find.text('hint used when disabled'), findsOneWidget); }); - testWidgets('DropdownButtonFormField - disabledHint displays when onChanged is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - disabledHint displays when onChanged is null', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); Widget build({ List<String>? items, ValueChanged<String?>? onChanged }) { @@ -780,7 +780,7 @@ void main() { expect(find.text('disabled'), findsOneWidget); }); - testWidgets('DropdownButtonFormField - disabled hint should be of same size as enabled hint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - disabled hint should be of same size as enabled hint', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); Widget build({ List<String>? items}) { @@ -805,7 +805,7 @@ void main() { expect(enabledHintBox.size, equals(disabledHintBox.size)); }); - testWidgets('DropdownButtonFormField - Custom icon size and colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - Custom icon size and colors', (WidgetTester tester) async { final Key iconKey = UniqueKey(); final Icon customIcon = Icon(Icons.assessment, key: iconKey); @@ -838,7 +838,7 @@ void main() { expect(disabledRichText.text.style!.color, Colors.orange); }); - testWidgets('DropdownButtonFormField - default elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - default elevation', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); debugDisableShadows = false; await tester.pumpWidget(buildFormFrame( @@ -858,7 +858,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('DropdownButtonFormField - custom elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - custom elevation', (WidgetTester tester) async { debugDisableShadows = false; final Key buttonKeyOne = UniqueKey(); final Key buttonKeyTwo = UniqueKey(); @@ -895,7 +895,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('DropdownButtonFormField does not allow duplicate item values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField does not allow duplicate item values', (WidgetTester tester) async { final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'c'] .map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( @@ -924,7 +924,7 @@ void main() { ); }); - testWidgets('DropdownButtonFormField value should only appear in one menu item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField value should only appear in one menu item', (WidgetTester tester) async { final List<DropdownMenuItem<String>> itemsWithDuplicateValues = <String>['a', 'b', 'c', 'd'] .map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( @@ -953,7 +953,7 @@ void main() { ); }); - testWidgets('DropdownButtonFormField - selectedItemBuilder builds custom buttons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - selectedItemBuilder builds custom buttons', (WidgetTester tester) async { const List<String> items = <String>[ 'One', 'Two', @@ -997,7 +997,7 @@ void main() { expect(find.text('Two as an Arabic numeral: 2'), findsOneWidget); }); - testWidgets('DropdownButton onTap callback is called when defined', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButton onTap callback is called when defined', (WidgetTester tester) async { int dropdownButtonTapCounter = 0; String? value = 'one'; void onChanged(String? newValue) { @@ -1043,7 +1043,7 @@ void main() { expect(dropdownButtonTapCounter, 2); // Should not change. }); - testWidgets('DropdownButtonFormField should re-render if value param changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField should re-render if value param changes', (WidgetTester tester) async { String currentValue = 'two'; await tester.pumpWidget( @@ -1089,7 +1089,7 @@ void main() { expect(find.text(currentValue), findsOneWidget); }); - testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autovalidateMode is passed to super', (WidgetTester tester) async { int validateCalled = 0; await tester.pumpWidget( @@ -1118,7 +1118,7 @@ void main() { expect(validateCalled, 1); }); - testWidgets('DropdownButtonFormField - Custom button alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownButtonFormField - Custom button alignment', (WidgetTester tester) async { await tester.pumpWidget(buildFormFrame( buttonAlignment: AlignmentDirectional.center, items: <String>['one'], @@ -1134,4 +1134,149 @@ void main() { selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0)), ); }); + + testWidgetsWithLeakTracking('InputDecoration borders are used for clipping', (WidgetTester tester) async { + const BorderRadius errorBorderRadius = BorderRadius.all(Radius.circular(5.0)); + const BorderRadius focusedErrorBorderRadius = BorderRadius.all(Radius.circular(6.0)); + const BorderRadius focusedBorder = BorderRadius.all(Radius.circular(7.0)); + const BorderRadius enabledBorder = BorderRadius.all(Radius.circular(9.0)); + + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + const String errorText = 'This is an error'; + bool showError = false; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + inputDecorationTheme: const InputDecorationTheme( + errorBorder: OutlineInputBorder( + borderRadius: errorBorderRadius, + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: focusedErrorBorderRadius, + ), + focusedBorder: OutlineInputBorder( + borderRadius: focusedBorder, + ), + enabledBorder: OutlineInputBorder( + borderRadius: enabledBorder, + ), + ), + ), + home: Material( + child: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return DropdownButtonFormField<String>( + value: 'two', + onChanged:(String? value) { + setState(() { + if (value == 'three') { + showError = true; + } else { + showError = false; + } + }); + }, + decoration: InputDecoration( + errorText: showError ? errorText : null, + ), + focusNode: focusNode, + items: menuItems.map<DropdownMenuItem<String>>((String item) { + return DropdownMenuItem<String>( + key: ValueKey<String>(item), + value: item, + child: Text(item, key: ValueKey<String>('${item}Text')), + ); + }).toList(), + ); + } + ), + ), + ), + ), + ); + + // Test enabled border. + InkWell inkWell = tester.widget<InkWell>(find.byType(InkWell)); + expect(inkWell.borderRadius, enabledBorder); + + // Test focused border. + focusNode.requestFocus(); + await tester.pump(); + + inkWell = tester.widget<InkWell>(find.byType(InkWell)); + expect(inkWell.borderRadius, focusedBorder); + + // Test focused error border. + await tester.tap(find.text('two'), warnIfMissed: false); + await tester.pumpAndSettle(); + await tester.tap(find.text('three').last); + await tester.pumpAndSettle(); + + inkWell = tester.widget<InkWell>(find.byType(InkWell)); + expect(inkWell.borderRadius, focusedErrorBorderRadius); + + // Test error border with no focus. + focusNode.unfocus(); + await tester.pump(); + + // Hovering over the widget should show the error border. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(tester.getCenter(find.text('three').last)); + await tester.pumpAndSettle(); + + inkWell = tester.widget<InkWell>(find.byType(InkWell)); + expect(inkWell.borderRadius, errorBorderRadius); + }); + + testWidgets('DropdownButtonFormField onChanged is called when the form is reset', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/123009. + final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>(); + final GlobalKey<FormState> formKey = GlobalKey<FormState>(); + String? value; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Form( + key: formKey, + child: DropdownButtonFormField<String>( + key: stateKey, + value: 'One', + items: <String>['One', 'Two', 'Free', 'Four'] + .map<DropdownMenuItem<String>>((String value) { + return DropdownMenuItem<String>( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + value = newValue; + }, + ), + ), + ), + ), + ); + + // Initial value is 'One'. + expect(value, isNull); + expect(stateKey.currentState!.value, equals('One')); + + // Select 'Two'. + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Two').last); + await tester.pumpAndSettle(); + expect(value, equals('Two')); + expect(stateKey.currentState!.value, equals('Two')); + + // Should be back to 'One' when the form is reset. + formKey.currentState!.reset(); + expect(value, equals('One')); + expect(stateKey.currentState!.value, equals('One')); + }); } diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 71f05e87ae638..6b142e5c9099f 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:ui'; import 'package:flutter/material.dart'; @@ -11,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + const String longText = 'one two three four five six seven eight nine ten eleven twelve'; final List<DropdownMenuEntry<TestMenu>> menuChildren = <DropdownMenuEntry<TestMenu>>[]; for (final TestMenu value in TestMenu.values) { @@ -39,15 +39,19 @@ void main() { await tester.pumpWidget(buildTest(themeData, menuChildren)); final EditableText editableText = tester.widget(find.byType(EditableText)); - expect(editableText.style.color, themeData.textTheme.labelLarge!.color); - expect(editableText.style.background, themeData.textTheme.labelLarge!.background); - expect(editableText.style.shadows, themeData.textTheme.labelLarge!.shadows); - expect(editableText.style.decoration, themeData.textTheme.labelLarge!.decoration); - expect(editableText.style.locale, themeData.textTheme.labelLarge!.locale); - expect(editableText.style.wordSpacing, themeData.textTheme.labelLarge!.wordSpacing); + expect(editableText.style.color, themeData.textTheme.bodyLarge!.color); + expect(editableText.style.background, themeData.textTheme.bodyLarge!.background); + expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows); + expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration); + expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale); + expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing); + expect(editableText.style.fontSize, 16.0); + expect(editableText.style.height, 1.5); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.decoration?.border, const OutlineInputBorder()); + expect(textField.style?.fontSize, 16.0); + expect(textField.style?.height, 1.5); await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first); await tester.pump(); @@ -74,6 +78,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); }); testWidgets('DropdownMenu can be disabled', (WidgetTester tester) async { @@ -109,11 +115,10 @@ void main() { expect(updatedMenuMaterial, findsNothing); }); - testWidgets('The width of the text field should always be the same as the menu view', + testWidgets('Material2 - The width of the text field should always be the same as the menu view', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: false); - final bool useMaterial3 = themeData.useMaterial3; await tester.pumpWidget( MaterialApp( theme: themeData, @@ -129,7 +134,7 @@ void main() { final Finder textField = find.byType(TextField); final Size anchorSize = tester.getSize(textField); - expect(anchorSize, useMaterial3 ? const Size(195.0, 60.0) : const Size(180.0, 56.0)); + expect(anchorSize, const Size(180.0, 56.0)); await tester.tap(find.byType(DropdownMenu<TestMenu>)); await tester.pumpAndSettle(); @@ -139,15 +144,15 @@ void main() { matching: find.byType(Material), ); final Size menuSize = tester.getSize(menuMaterial); - expect(menuSize, useMaterial3 ? const Size(195.0, 304.0) : const Size(180.0, 304.0)); + expect(menuSize, const Size(180.0, 304.0)); // The text field should have same width as the menu // when the width property is not null. await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0)); final Finder anchor = find.byType(TextField); - final Size size = tester.getSize(anchor); - expect(size, useMaterial3 ? const Size(200.0, 60.0) : const Size(200.0, 56.0)); + final double width = tester.getSize(anchor).width; + expect(width, 200.0); await tester.tap(anchor); await tester.pumpAndSettle(); @@ -156,8 +161,57 @@ void main() { of: find.byType(SingleChildScrollView), matching: find.byType(Material), ); - final Size updatedMenuSize = tester.getSize(updatedMenu); - expect(updatedMenuSize, const Size(200.0, 304.0)); + final double updatedMenuWidth = tester.getSize(updatedMenu).width; + expect(updatedMenuWidth, 200.0); + }); + + testWidgets('Material3 - The width of the text field should always be the same as the menu view', + (WidgetTester tester) async { + final ThemeData themeData = ThemeData(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + body: SafeArea( + child: DropdownMenu<TestMenu>( + dropdownMenuEntries: menuChildren, + ), + ), + ), + ) + ); + + final Finder textField = find.byType(TextField); + final double anchorWidth = tester.getSize(textField).width; + expect(anchorWidth, closeTo(180.5, 0.1)); + + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + await tester.pumpAndSettle(); + + final Finder menuMaterial = find.ancestor( + of: find.byType(SingleChildScrollView), + matching: find.byType(Material), + ); + final double menuWidth = tester.getSize(menuMaterial).width; + expect(menuWidth, closeTo(180.5, 0.1)); + + // The text field should have same width as the menu + // when the width property is not null. + await tester.pumpWidget(buildTest(themeData, menuChildren, width: 200.0)); + + final Finder anchor = find.byType(TextField); + final double width = tester.getSize(anchor).width; + expect(width, 200.0); + + await tester.tap(anchor); + await tester.pumpAndSettle(); + + final Finder updatedMenu = find.ancestor( + of: find.byType(SingleChildScrollView), + matching: find.byType(Material), + ); + final double updatedMenuWidth = tester.getSize(updatedMenu).width; + expect(updatedMenuWidth, 200.0); }); testWidgets('The width property can customize the width of the dropdown menu', (WidgetTester tester) async { @@ -216,10 +270,73 @@ void main() { expect(box.size.width, customWidth); }); - testWidgets('The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list', + testWidgets('The width of MenuAnchor respects MenuAnchor.expandedInsets', (WidgetTester tester) async { + const double parentWidth = 500.0; + final List<DropdownMenuEntry<ShortMenu>> shortMenuItems = <DropdownMenuEntry<ShortMenu>>[]; + for (final ShortMenu value in ShortMenu.values) { + final DropdownMenuEntry<ShortMenu> entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label); + shortMenuItems.add(entry); + } + Widget buildMenuAnchor({EdgeInsets? expandedInsets}) { + return MaterialApp( + home: Scaffold( + body: SizedBox( + width: parentWidth, + height: parentWidth, + child: DropdownMenu<ShortMenu>( + expandedInsets: expandedInsets, + dropdownMenuEntries: shortMenuItems, + ), + ), + ), + ); + } + + // By default, the width of the text field is determined by the menu children. + await tester.pumpWidget(buildMenuAnchor()); + RenderBox box = tester.firstRenderObject(find.byType(TextField)); + expect(box.size.width, 136.0); + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + Size buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').hitTestable()); + expect(buttonSize.width, 136.0); + + // If expandedInsets is EdgeInsets.zero, the width should be the same as its parent. + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildMenuAnchor(expandedInsets: EdgeInsets.zero)); + box = tester.firstRenderObject(find.byType(TextField)); + expect(box.size.width, parentWidth); + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0')); + expect(buttonSize.width, parentWidth); + + // If expandedInsets is not zero, the width of the text field should be adjusted + // based on the EdgeInsets.left and EdgeInsets.right. The top and bottom values + // will be ignored. + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildMenuAnchor(expandedInsets: const EdgeInsets.only(left: 35.0, top: 50.0, right: 20.0))); + box = tester.firstRenderObject(find.byType(TextField)); + expect(box.size.width, parentWidth - 35.0 - 20.0); + final Rect containerRect = tester.getRect(find.byType(SizedBox).first); + final Rect dropdownMenuRect = tester.getRect(find.byType(TextField)); + expect(dropdownMenuRect.top, containerRect.top); + + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0')); + expect(buttonSize.width, parentWidth - 35.0 - 20.0); + }); + + testWidgets('Material2 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list', (WidgetTester tester) async { - final ThemeData themeData = ThemeData(); - final bool material3 = themeData.useMaterial3; + final ThemeData themeData = ThemeData(useMaterial3: false); await tester.pumpWidget(buildTest(themeData, menuChildren)); await tester.tap(find.byType(DropdownMenu<TestMenu>)); @@ -239,7 +356,7 @@ void main() { matching: find.byType(Padding), ).first; final Size menuViewSize = tester.getSize(menuView); - expect(menuViewSize, material3 ? const Size(195.0, 304.0) : const Size(180.0, 304.0)); // 304 = 288 + vertical padding(2 * 8) + expect(menuViewSize, const Size(180.0, 304.0)); // 304 = 288 + vertical padding(2 * 8) // Constrains the menu height. await tester.pumpWidget(Container()); @@ -255,9 +372,52 @@ void main() { ).first; final Size updatedMenuSize = tester.getSize(updatedMenu); - expect(updatedMenuSize, material3 ? const Size(195.0, 100.0) : const Size(180.0, 100.0)); + expect(updatedMenuSize, const Size(180.0, 100.0)); }); + testWidgets('Material3 - The menuHeight property can be used to show a shorter scrollable menu list instead of the complete list', + (WidgetTester tester) async { + final ThemeData themeData = ThemeData(useMaterial3: true); + await tester.pumpWidget(buildTest(themeData, menuChildren)); + + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + await tester.pumpAndSettle(); + + final Element firstItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 0').last); + final RenderBox firstBox = firstItem.renderObject! as RenderBox; + final Offset topLeft = firstBox.localToGlobal(firstBox.size.topLeft(Offset.zero)); + final Element lastItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 5').last); + final RenderBox lastBox = lastItem.renderObject! as RenderBox; + final Offset bottomRight = lastBox.localToGlobal(lastBox.size.bottomRight(Offset.zero)); + // height = height of MenuItemButton * 6 = 48 * 6 + expect(bottomRight.dy - topLeft.dy, 288.0); + + final Finder menuView = find.ancestor( + of: find.byType(SingleChildScrollView), + matching: find.byType(Padding), + ).first; + final Size menuViewSize = tester.getSize(menuView); + expect(menuViewSize.width, closeTo(180.6, 0.1)); + expect(menuViewSize.height, equals(304.0)); // 304 = 288 + vertical padding(2 * 8) + + // Constrains the menu height. + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildTest(themeData, menuChildren, menuHeight: 100)); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + await tester.pumpAndSettle(); + + final Finder updatedMenu = find.ancestor( + of: find.byType(SingleChildScrollView), + matching: find.byType(Padding), + ).first; + + final Size updatedMenuSize = tester.getSize(updatedMenu); + expect(updatedMenuSize.width, closeTo(180.6, 0.1)); + expect(updatedMenuSize.height, equals(100.0)); +}); + testWidgets('The text in the menu button should be aligned with the text of ' 'the text field - LTR', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); @@ -889,6 +1049,34 @@ void main() { expect(controller.text, 'New Item'); }); + testWidgets('The menu should be closed after text editing is complete', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(); + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget(MaterialApp( + theme: themeData, + home: Scaffold( + body: DropdownMenu<TestMenu>( + requestFocusOnTap: true, + enableFilter: true, + dropdownMenuEntries: menuChildren, + controller: controller, + ), + ), + )); + // Access the MenuAnchor + final MenuAnchor menuAnchor = tester.widget<MenuAnchor>(find.byType(MenuAnchor)); + + // Open the menu + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + await tester.pumpAndSettle(); + expect(menuAnchor.controller!.isOpen, true); + + // Simulate `TextInputAction.done` on textfield + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + expect(menuAnchor.controller!.isOpen, false); + }); + testWidgets('The onSelected gets called only when a selection is made', (WidgetTester tester) async { int selectionCount = 0; @@ -1338,6 +1526,194 @@ void main() { expect(find.text('Item 5').hitTestable(), findsOneWidget); }); + // This is a regression test for https://github.com/flutter/flutter/issues/131676. + testWidgets('Material3 - DropdownMenu uses correct text styles', (WidgetTester tester) async { + const TextStyle inputTextThemeStyle = TextStyle( + fontSize: 18.5, + fontStyle: FontStyle.italic, + wordSpacing: 1.2, + decoration: TextDecoration.lineThrough, + ); + const TextStyle menuItemTextThemeStyle = TextStyle( + fontSize: 20.5, + fontStyle: FontStyle.italic, + wordSpacing: 2.1, + decoration: TextDecoration.underline, + ); + final ThemeData themeData = ThemeData( + useMaterial3: true, + textTheme: const TextTheme( + bodyLarge: inputTextThemeStyle, + labelLarge: menuItemTextThemeStyle, + ), + ); + await tester.pumpWidget(buildTest(themeData, menuChildren)); + + // Test input text style uses the TextTheme.bodyLarge. + final EditableText editableText = tester.widget(find.byType(EditableText)); + expect(editableText.style.fontSize, inputTextThemeStyle.fontSize); + expect(editableText.style.fontStyle, inputTextThemeStyle.fontStyle); + expect(editableText.style.wordSpacing, inputTextThemeStyle.wordSpacing); + expect(editableText.style.decoration, inputTextThemeStyle.decoration); + + // Open the menu. + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first); + await tester.pump(); + + final Finder buttonMaterial = find.descendant( + of: find.byType(TextButton), + matching: find.byType(Material), + ).last; + + // Test menu item text style uses the TextTheme.labelLarge. + final Material material = tester.widget<Material>(buttonMaterial); + expect(material.textStyle?.fontSize, menuItemTextThemeStyle.fontSize); + expect(material.textStyle?.fontStyle, menuItemTextThemeStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuItemTextThemeStyle.wordSpacing); + expect(material.textStyle?.decoration, menuItemTextThemeStyle.decoration); + }); + + testWidgets('DropdownMenuEntries do not overflow when width is specified', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/126882 + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenu<TestMenu>( + controller: controller, + width: 100, + dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) { + return DropdownMenuEntry<TestMenu>( + value: item, + label: '${item.label} $longText', + ); + }).toList(), + ), + ), + ), + ); + + // Opening the width=100 menu should not crash. + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + expect(tester.takeException(), isNull); + await tester.pumpAndSettle(); + + Finder findMenuItemText(String label) { + final String labelText = '$label $longText'; + return find.descendant( + of: find.widgetWithText(MenuItemButton, labelText), + matching: find.byType(Text), + ).last; + } + + // Actual size varies a little on web platforms. + final Matcher closeTo300 = closeTo(300, 0.25); + expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo300); + expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo300); + expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo300); + expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo300); + + await tester.tap(findMenuItemText('Item 0')); + await tester.pumpAndSettle(); + expect(controller.text, 'Item 0 $longText'); + }); + + testWidgets('DropdownMenuEntry.labelWidget is Text that specifies maxLines 1 or 2', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/126882 + final TextEditingController controller = TextEditingController(); + + Widget buildFrame({ required int maxLines }) { + return MaterialApp( + home: Scaffold( + body: DropdownMenu<TestMenu>( + key: ValueKey<int>(maxLines), + controller: controller, + width: 100, + dropdownMenuEntries: TestMenu.values.map<DropdownMenuEntry<TestMenu>>((TestMenu item) { + return DropdownMenuEntry<TestMenu>( + value: item, + label: '${item.label} $longText', + labelWidget: Text('${item.label} $longText', maxLines: maxLines), + ); + }).toList(), + ), + ) + ); + } + + Finder findMenuItemText(String label) { + final String labelText = '$label $longText'; + return find.descendant( + of: find.widgetWithText(MenuItemButton, labelText), + matching: find.byType(Text), + ).last; + } + + await tester.pumpWidget(buildFrame(maxLines: 1)); + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + + // Actual size varies a little on web platforms. + final Matcher closeTo20 = closeTo(20, 0.05); + expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo20); + expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo20); + expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo20); + expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo20); + + // Close the menu + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + expect(controller.text, ''); // nothing selected + + await tester.pumpWidget(buildFrame(maxLines: 2)); + await tester.tap(find.byType(DropdownMenu<TestMenu>)); + + // Actual size varies a little on web platforms. + final Matcher closeTo40 = closeTo(40, 0.05); + expect(tester.getSize(findMenuItemText('Item 0')).height, closeTo40); + expect(tester.getSize(findMenuItemText('Menu 1')).height, closeTo40); + expect(tester.getSize(findMenuItemText('Item 2')).height, closeTo40); + expect(tester.getSize(findMenuItemText('Item 3')).height, closeTo40); + + // Close the menu + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + expect(controller.text, ''); // nothing selected + }); + + // Regression test for https://github.com/flutter/flutter/issues/131350. + testWidgets('DropdownMenuEntry.leadingIcon default layout', (WidgetTester tester) async { + // The DropdownMenu should not get extra padding in DropdownMenuEntry items + // when both text field and DropdownMenuEntry have leading icons. + await tester.pumpWidget(const MaterialApp( + home: Scaffold( + body: DropdownMenu<int>( + leadingIcon: Icon(Icons.search), + hintText: 'Hint', + dropdownMenuEntries: <DropdownMenuEntry<int>>[ + DropdownMenuEntry<int>( + value: 0, + label: 'Item 0', + leadingIcon: Icon(Icons.alarm) + ), + DropdownMenuEntry<int>(value: 1, label: 'Item 1'), + ], + ), + ) + )); + await tester.tap(find.byType(DropdownMenu<int>)); + await tester.pumpAndSettle(); + + // Check text location in text field. + expect(tester.getTopLeft(find.text('Hint')).dx, 48.0); + + // By default, the text of item 0 should be aligned with the text of the text field. + expect(tester.getTopLeft(find.text('Item 0').last).dx, 48.0); + + // By default, the text of item 1 should be aligned with the text of the text field, + // so there are some extra padding before "Item 1". + expect(tester.getTopLeft(find.text('Item 1').last).dx, 48.0); + }); } enum TestMenu { diff --git a/packages/flutter/test/material/dropdown_menu_theme_test.dart b/packages/flutter/test/material/dropdown_menu_theme_test.dart index e91f3a3959a9a..279f60256a288 100644 --- a/packages/flutter/test/material/dropdown_menu_theme_test.dart +++ b/packages/flutter/test/material/dropdown_menu_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('DropdownMenuThemeData copyWith, ==, hashCode basics', () { @@ -30,7 +31,7 @@ void main() { expect(identical(DropdownMenuThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Default DropdownMenuThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default DropdownMenuThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const DropdownMenuThemeData().debugFillProperties(builder); @@ -42,7 +43,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('With no other configuration, defaults are used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('With no other configuration, defaults are used', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); await tester.pumpWidget( MaterialApp( @@ -99,7 +100,7 @@ void main() { expect(material.textStyle?.color, themeData.colorScheme.onSurface); }); - testWidgets('ThemeData.dropdownMenuTheme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.dropdownMenuTheme overrides defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData( dropdownMenuTheme: DropdownMenuThemeData( textStyle: TextStyle( @@ -178,7 +179,7 @@ void main() { expect(material.textStyle?.color, theme.colorScheme.onSurface); }); - testWidgets('DropdownMenuTheme overrides ThemeData and defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownMenuTheme overrides ThemeData and defaults', (WidgetTester tester) async { final DropdownMenuThemeData global = DropdownMenuThemeData( textStyle: TextStyle( color: Colors.orange, @@ -281,7 +282,7 @@ void main() { expect(material.textStyle?.color, theme.colorScheme.onSurface); }); - testWidgets('Widget parameters overrides DropdownMenuTheme, ThemeData and defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget parameters overrides DropdownMenuTheme, ThemeData and defaults', (WidgetTester tester) async { final DropdownMenuThemeData global = DropdownMenuThemeData( textStyle: TextStyle( color: Colors.orange, diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index d2a3c9d14f86e..8db8c6fbdee10 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -23,7 +23,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -3347,7 +3346,7 @@ void main() { // Should be center-center aligned, the icon size is 24.0 pixels. expect( buttonBox.localToGlobal(Offset((buttonBox.size.width -24.0) / 2.0, buttonBox.size.height / 2.0)), - selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0)), + offsetMoreOrLessEquals(selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0))), ); // Open dropdown. @@ -3363,10 +3362,10 @@ void main() { Offset(selectedItemBoxInMenu.size.width / 2.0, selectedItemBoxInMenu.size.height / 2.0) ); - expect(center.dx, menuRect.topCenter.dx,); + expect(center.dx, moreOrLessEquals(menuRect.topCenter.dx)); expect( center.dy, - selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0)).dy, + moreOrLessEquals(selectedItemBox.localToGlobal(Offset(selectedItemBox.size.width / 2.0, selectedItemBox.size.height / 2.0)).dy), ); }); diff --git a/packages/flutter/test/material/elevated_button_test.dart b/packages/flutter/test/material/elevated_button_test.dart index 6c9cc205a54fa..0f3796c87f7e8 100644 --- a/packages/flutter/test/material/elevated_button_test.dart +++ b/packages/flutter/test/material/elevated_button_test.dart @@ -7,12 +7,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('ElevatedButton, ElevatedButton.icon defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton, ElevatedButton.icon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); final bool material3 = theme.useMaterial3; @@ -161,7 +160,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Default ElevatedButton meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ElevatedButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -196,11 +195,12 @@ void main() { await gesture.moveTo(center); await tester.pumpAndSettle(); await expectLater(tester, meetsGuideline(textContrastGuideline)); + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('ElevatedButton default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -259,9 +259,11 @@ void main() { await tester.pumpAndSettle(); expect(elevation(), 1.0); expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('ElevatedButton uses stateful color for text color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton uses stateful color for text color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -334,10 +336,12 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(textColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('ElevatedButton uses stateful color for icon color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton uses stateful color for icon color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final Key buttonKey = UniqueKey(); @@ -410,9 +414,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(iconColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('ElevatedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder elevatedButton; @@ -455,7 +461,7 @@ void main() { expect(tester.widget<ElevatedButton>(elevatedButton).enabled, false); }); - testWidgets('ElevatedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -486,7 +492,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets("ElevatedButton response doesn't hover when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ElevatedButton response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Focus'); final GlobalKey childKey = GlobalKey(); @@ -534,9 +540,11 @@ void main() { await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + + focusNode.dispose(); }); - testWidgets('disabled and hovered ElevatedButton responds to mouse-exit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled and hovered ElevatedButton responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; @@ -598,7 +606,7 @@ void main() { expect(hover, false); }); - testWidgets('Can set ElevatedButton focus and Can set unFocus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set ElevatedButton focus and Can set unFocus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -625,9 +633,11 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('When ElevatedButton disable, Can not set ElevatedButton focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When ElevatedButton disable, Can not set ElevatedButton focus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -648,9 +658,12 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + + node.dispose(); }); - testWidgets('Does ElevatedButton work with hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does ElevatedButton work with hover', (WidgetTester tester) async { const Color hoverColor = Color(0xff001122); await tester.pumpWidget( @@ -677,7 +690,7 @@ void main() { expect(inkFeatures, paints..rect(color: hoverColor)); }); - testWidgets('Does ElevatedButton work with focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does ElevatedButton work with focus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Node'); @@ -703,9 +716,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('Does ElevatedButton work with autofocus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does ElevatedButton work with autofocus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -733,9 +748,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('Does ElevatedButton contribute semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does ElevatedButton contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( @@ -783,7 +800,7 @@ void main() { semantics.dispose(); }); - testWidgets('ElevatedButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { const ButtonStyle style = ButtonStyle( // Specifying minimumSize to mimic the original minimumSize for // RaisedButton so that the corresponding button size matches @@ -817,7 +834,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); }); - testWidgets('ElevatedButton has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton has no clip by default', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -834,7 +851,7 @@ void main() { ); }); - testWidgets('ElevatedButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -903,7 +920,7 @@ void main() { expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); }); - testWidgets('ElevatedButton.icon responds to applied padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton.icon responds to applied padding', (WidgetTester tester) async { const Key buttonKey = Key('test'); const Key labelKey = Key('label'); await tester.pumpWidget( @@ -1030,7 +1047,7 @@ void main() { if (textDirection == TextDirection.rtl) 'RTL', ].join(', '); - testWidgets(testName, (WidgetTester tester) async { + testWidgetsWithLeakTracking(testName, (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1172,7 +1189,7 @@ void main() { } }); - testWidgets('Override ElevatedButton default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override ElevatedButton default padding', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light()), @@ -1206,7 +1223,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.all(22)); }); - testWidgets('M3 ElevatedButton has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 ElevatedButton has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1232,7 +1249,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 24)); }); - testWidgets('M3 ElevatedButton.icon has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 ElevatedButton.icon has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1259,7 +1276,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(16.0, 0.0, 24.0, 0.0)); }); - testWidgets('Elevated buttons animate elevation before color on disable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Elevated buttons animate elevation before color on disable', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/387 const ColorScheme colorScheme = ColorScheme.light(); @@ -1306,7 +1323,7 @@ void main() { expect(physicalShape().color, disabledBackgroundColor); }); - testWidgets('By default, ElevatedButton shape outline is defined by shape.side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('By default, ElevatedButton shape outline is defined by shape.side', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/69544 const Color borderColor = Color(0xff4caf50); @@ -1337,7 +1354,7 @@ void main() { ); }); - testWidgets('Fixed size ElevatedButtons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size ElevatedButtons', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1370,7 +1387,7 @@ void main() { expect(tester.getSize(find.widgetWithText(ElevatedButton, 'wx200')).height, 200); }); - testWidgets('ElevatedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) { return MaterialApp( home: Scaffold( @@ -1410,7 +1427,7 @@ void main() { } }); - testWidgets('ElevatedButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -1437,7 +1454,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('ElevatedButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( @@ -1459,7 +1476,7 @@ void main() { expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory)); }, variant: TargetPlatformVariant.all()); - testWidgets('ElevatedButton.icon does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton.icon does not overflow', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/77815 await tester.pumpWidget( MaterialApp( @@ -1480,7 +1497,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('ElevatedButton.icon icon,label layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton.icon icon,label layout', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); final Key iconKey = UniqueKey(); final Key labelKey = UniqueKey(); @@ -1517,7 +1534,7 @@ void main() { expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0)); }); - testWidgets('ElevatedButton maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton maximumSize', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); @@ -1559,7 +1576,7 @@ void main() { expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0)); }); - testWidgets('Fixed size ElevatedButton, same as minimumSize == maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size ElevatedButton, same as minimumSize == maximumSize', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1589,7 +1606,7 @@ void main() { expect(tester.getSize(find.widgetWithText(ElevatedButton, '200,200')), const Size(200, 200)); }); - testWidgets('ElevatedButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1667,7 +1684,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('ElevatedButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104595. await tester.pumpWidget(MaterialApp( home: SelectionArea( @@ -1690,7 +1707,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('Ink Response shape matches Material shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ink Response shape matches Material shape', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/91844 Widget buildFrame({BorderSide? side}) { @@ -1735,7 +1752,7 @@ void main() { ); }); - testWidgets('ElevatedButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ElevatedButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/packages/flutter/test/material/elevated_button_theme_test.dart b/packages/flutter/test/material/elevated_button_theme_test.dart index 1957b7463874e..aa713c23696a2 100644 --- a/packages/flutter/test/material/elevated_button_theme_test.dart +++ b/packages/flutter/test/material/elevated_button_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('ElevatedButtonThemeData lerp special cases', () { @@ -12,7 +13,7 @@ void main() { expect(identical(ElevatedButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Material3: Passing no ElevatedButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Passing no ElevatedButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -49,7 +50,7 @@ void main() { expect(align.alignment, Alignment.center); }); - testWidgets('Material2: Passing no ElevatedButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Passing no ElevatedButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -189,19 +190,19 @@ void main() { expect(align.alignment, alignment); } - testWidgets('Button style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(themeStyle: style)); await tester.pumpAndSettle(); checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallStyle: style)); await tester.pumpAndSettle(); checkButton(tester); @@ -209,26 +210,26 @@ void main() { // Same as the previous tests with empty ButtonStyle's instead of null. - testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); }); - testWidgets('Material 3: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material 3: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -299,7 +300,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material 2: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material 2: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); diff --git a/packages/flutter/test/material/expand_icon_test.dart b/packages/flutter/test/material/expand_icon_test.dart index 4d795da9adb66..b6998cc58f651 100644 --- a/packages/flutter/test/material/expand_icon_test.dart +++ b/packages/flutter/test/material/expand_icon_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Widget wrap({ required Widget child, ThemeData? theme }) { return MaterialApp( @@ -15,7 +16,7 @@ Widget wrap({ required Widget child, ThemeData? theme }) { } void main() { - testWidgets('ExpandIcon test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon test', (WidgetTester tester) async { bool expanded = false; IconTheme iconTheme; @@ -73,7 +74,7 @@ void main() { expect(iconTheme.data.color, equals(Colors.white60)); }); - testWidgets('ExpandIcon disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon disabled', (WidgetTester tester) async { IconTheme iconTheme; // Light mode test await tester.pumpWidget(wrap( @@ -96,7 +97,7 @@ void main() { expect(iconTheme.data.color, equals(Colors.white38)); }); - testWidgets('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async { bool expanded = false; await tester.pumpWidget(wrap( @@ -119,7 +120,7 @@ void main() { expect(expanded, isFalse); }); - testWidgets('ExpandIcon is rotated initially if isExpanded is true on first build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon is rotated initially if isExpanded is true on first build', (WidgetTester tester) async { bool expanded = true; await tester.pumpWidget(wrap( @@ -134,7 +135,7 @@ void main() { expect(rotation.turns.value, 0.5); }); - testWidgets('ExpandIcon default size is 24', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon default size is 24', (WidgetTester tester) async { final ExpandIcon expandIcon = ExpandIcon( onPressed: (bool isExpanded) {}, ); @@ -147,7 +148,7 @@ void main() { expect(icon.size, 24); }); - testWidgets('ExpandIcon has the correct given size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon has the correct given size', (WidgetTester tester) async { ExpandIcon expandIcon = ExpandIcon( size: 36, onPressed: (bool isExpanded) {}, @@ -173,7 +174,7 @@ void main() { expect(icon.size, 48); }); - testWidgets('ExpandIcon has correct semantic hints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon has correct semantic hints', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); await tester.pumpWidget(wrap( @@ -210,7 +211,7 @@ void main() { handle.dispose(); }); - testWidgets('ExpandIcon uses custom icon color and expanded icon color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon uses custom icon color and expanded icon color', (WidgetTester tester) async { bool expanded = false; IconTheme iconTheme; @@ -271,7 +272,7 @@ void main() { expect(iconTheme.data.color, equals(Colors.indigo)); }); - testWidgets('ExpandIcon uses custom disabled icon color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpandIcon uses custom disabled icon color', (WidgetTester tester) async { IconTheme iconTheme; await tester.pumpWidget(wrap( diff --git a/packages/flutter/test/material/expansion_panel_test.dart b/packages/flutter/test/material/expansion_panel_test.dart index 7c2fc32a49bd9..cfef2a5344fea 100644 --- a/packages/flutter/test/material/expansion_panel_test.dart +++ b/packages/flutter/test/material/expansion_panel_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class SimpleExpansionPanelListTestWidget extends StatefulWidget { const SimpleExpansionPanelListTestWidget({ @@ -112,7 +113,7 @@ class ExpansionPanelListSemanticsTestState extends State<ExpansionPanelListSeman } void main() { - testWidgets('ExpansionPanelList test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList test', (WidgetTester tester) async { late int capturedIndex; late bool capturedIsExpanded; @@ -178,7 +179,7 @@ void main() { expect(box.size.height - oldHeight, greaterThanOrEqualTo(100.0)); // 100 + some margin }); - testWidgets('ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final Key headerKey = UniqueKey(); await tester.pumpWidget( @@ -226,7 +227,7 @@ void main() { handle.dispose(); }); - testWidgets('Multiple Panel List test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiple Panel List test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: ListView( @@ -265,7 +266,7 @@ void main() { expect(find.text('D'), findsOneWidget); }); - testWidgets('Open/close animations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Open/close animations', (WidgetTester tester) async { const Duration kSizeAnimationDuration = Duration(milliseconds: 1000); // The MaterialGaps animate in using kThemeAnimationDuration (hardcoded), // which should be less than our test size animation length. So we can assume that they @@ -359,7 +360,7 @@ void main() { expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 48.0 + 1.0 + 48.0 + 16.0 + 16.0 + 48.0 + 16.0, 800.0, 100.0)); }); - testWidgets('Radio mode has max of one panel open at a time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio mode has max of one panel open at a time', (WidgetTester tester) async { final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[ ExpansionPanelRadio( headerBuilder: (BuildContext context, bool isExpanded) { @@ -498,7 +499,7 @@ void main() { expect(find.text('F'), findsNothing); }); - testWidgets('Radio mode calls expansionCallback once if other panels closed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio mode calls expansionCallback once if other panels closed', (WidgetTester tester) async { final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[ ExpansionPanelRadio( headerBuilder: (BuildContext context, bool isExpanded) { @@ -569,7 +570,7 @@ void main() { expect(callbackHistory.last['isExpanded'], equals(false)); }); - testWidgets('Radio mode calls expansionCallback twice if other panel open prior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio mode calls expansionCallback twice if other panel open prior', (WidgetTester tester) async { final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[ ExpansionPanelRadio( headerBuilder: (BuildContext context, bool isExpanded) { @@ -649,7 +650,7 @@ void main() { expect(callbackResults['isExpanded'], equals(true)); }); - testWidgets('ExpansionPanelList.radio callback displays true or false based on the visibility of a list item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList.radio callback displays true or false based on the visibility of a list item', (WidgetTester tester) async { late int lastExpanded; bool topElementExpanded = false; bool bottomElementExpanded = false; @@ -744,7 +745,7 @@ void main() { expect(find.text('D'), findsNothing); }); - testWidgets( + testWidgetsWithLeakTracking( 'didUpdateWidget accounts for toggling between ExpansionPanelList ' 'and ExpansionPaneList.radio', (WidgetTester tester) async { @@ -874,7 +875,7 @@ void main() { }, ); - testWidgets('No duplicate global keys at layout/build time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No duplicate global keys at layout/build time', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/13780 await tester.pumpWidget( StatefulBuilder( @@ -998,7 +999,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Panel header has semantics, canTapOnHeader = false ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Panel header has semantics, canTapOnHeader = false ', (WidgetTester tester) async { const Key expandedKey = Key('expanded'); const Key collapsedKey = Key('collapsed'); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); @@ -1083,7 +1084,7 @@ void main() { handle.dispose(); }); - testWidgets('Panel header has semantics, canTapOnHeader = true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Panel header has semantics, canTapOnHeader = true', (WidgetTester tester) async { const Key expandedKey = Key('expanded'); const Key collapsedKey = Key('collapsed'); final SemanticsHandle handle = tester.ensureSemantics(); @@ -1136,7 +1137,7 @@ void main() { handle.dispose(); }); - testWidgets('Ensure canTapOnHeader is false by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ensure canTapOnHeader is false by default', (WidgetTester tester) async { final ExpansionPanel expansionPanel = ExpansionPanel( headerBuilder: (BuildContext context, bool isExpanded) => const Text('Demo'), body: const SizedBox(height: 100.0), @@ -1145,7 +1146,7 @@ void main() { expect(expansionPanel.canTapOnHeader, isFalse); }); - testWidgets('Toggle ExpansionPanelRadio when tapping header and canTapOnHeader is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle ExpansionPanelRadio when tapping header and canTapOnHeader is true', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); const Key secondPanelKey = Key('secondPanelKey'); @@ -1205,7 +1206,7 @@ void main() { expect(find.text('D'), findsOneWidget); }); - testWidgets('Toggle ExpansionPanel when tapping header and canTapOnHeader is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle ExpansionPanel when tapping header and canTapOnHeader is true', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); const Key secondPanelKey = Key('secondPanelKey'); @@ -1264,7 +1265,7 @@ void main() { expect(find.text('D'), findsNothing); }); - testWidgets('Do not toggle ExpansionPanel when tapping header and canTapOnHeader is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not toggle ExpansionPanel when tapping header and canTapOnHeader is false', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); const Key secondPanelKey = Key('secondPanelKey'); @@ -1304,7 +1305,7 @@ void main() { expect(find.text('D'), findsNothing); }); - testWidgets('Do not toggle ExpansionPanelRadio when tapping header and canTapOnHeader is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not toggle ExpansionPanelRadio when tapping header and canTapOnHeader is false', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); const Key secondPanelKey = Key('secondPanelKey'); @@ -1362,7 +1363,7 @@ void main() { expect(find.text('D'), findsNothing); }); - testWidgets('Correct default header padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Correct default header padding', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); await tester.pumpWidget( @@ -1400,7 +1401,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/5848. - testWidgets('The AnimatedContainer and IconButton have the same height of 48px', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The AnimatedContainer and IconButton have the same height of 48px', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); await tester.pumpWidget( @@ -1425,7 +1426,7 @@ void main() { expect(boxOfContainer.size.height, equals(48.0)); // Header should have 48px height according to Material 2 Design spec. }); - testWidgets("The AnimatedContainer's height is at least kMinInteractiveDimension", (WidgetTester tester) async { + testWidgetsWithLeakTracking("The AnimatedContainer's height is at least kMinInteractiveDimension", (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); await tester.pumpWidget( @@ -1448,7 +1449,7 @@ void main() { expect(box.size.height, greaterThanOrEqualTo(kMinInteractiveDimension)); }); - testWidgets('Correct custom header padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Correct custom header padding', (WidgetTester tester) async { const Key firstPanelKey = Key('firstPanelKey'); await tester.pumpWidget( @@ -1486,7 +1487,7 @@ void main() { expect(box.size.width, equals(744.0)); }); - testWidgets('ExpansionPanelList respects dividerColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList respects dividerColor', (WidgetTester tester) async { const Color dividerColor = Colors.red; await tester.pumpWidget(const MaterialApp( home: SingleChildScrollView( @@ -1503,7 +1504,7 @@ void main() { expect(decoration.border!.top.color, dividerColor); }); - testWidgets('ExpansionPanelList.radio respects DividerColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList.radio respects DividerColor', (WidgetTester tester) async { const Color dividerColor = Colors.red; await tester.pumpWidget(MaterialApp( home: SingleChildScrollView( @@ -1536,7 +1537,7 @@ void main() { expect(boxDecoration.border!.top.color, dividerColor); }); - testWidgets('ExpansionPanelList respects expandIconColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList respects expandIconColor', (WidgetTester tester) async { const Color expandIconColor = Colors.blue; await tester.pumpWidget(MaterialApp( home: SingleChildScrollView( @@ -1560,7 +1561,7 @@ void main() { expect(expandIcon.color, expandIconColor); }); - testWidgets('ExpansionPanelList.radio respects expandIconColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList.radio respects expandIconColor', (WidgetTester tester) async { const Color expandIconColor = Colors.blue; await tester.pumpWidget(MaterialApp( home: SingleChildScrollView( @@ -1585,7 +1586,7 @@ void main() { expect(expandIcon.color, expandIconColor); }); - testWidgets('elevation is propagated properly to MergeableMaterial', (WidgetTester tester) async { + testWidgetsWithLeakTracking('elevation is propagated properly to MergeableMaterial', (WidgetTester tester) async { const double elevation = 8; // Test for ExpansionPanelList. @@ -1627,7 +1628,7 @@ void main() { expect(tester.widget<MergeableMaterial>(find.byType(MergeableMaterial)).elevation, elevation); }); - testWidgets('Using a value non defined value throws assertion error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Using a value non defined value throws assertion error', (WidgetTester tester) async { // It should throw an AssertionError since, 19 is not defined in kElevationToShadow. await tester.pumpWidget(const MaterialApp( @@ -1646,7 +1647,7 @@ void main() { )); }); - testWidgets('ExpansionPanel.panelColor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanel.panelColor test', (WidgetTester tester) async { const Color firstPanelColor = Colors.red; const Color secondPanelColor = Colors.brown; @@ -1682,7 +1683,7 @@ void main() { expect((mergeableMaterial.children.last as MaterialSlice).color, secondPanelColor); }); - testWidgets('ExpansionPanelRadio.backgroundColor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelRadio.backgroundColor test', (WidgetTester tester) async { const Color firstPanelColor = Colors.red; const Color secondPanelColor = Colors.brown; @@ -1717,7 +1718,7 @@ void main() { expect((mergeableMaterial.children.last as MaterialSlice).color, secondPanelColor); }); - testWidgets('ExpansionPanelList.materialGapSize defaults to 16.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList.materialGapSize defaults to 16.0', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: SingleChildScrollView( child: ExpansionPanelList( @@ -1738,7 +1739,7 @@ void main() { expect(expansionPanelList.materialGapSize, 16); }); - testWidgets('ExpansionPanelList respects materialGapSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionPanelList respects materialGapSize', (WidgetTester tester) async { Widget buildWidgetForTest({double materialGapSize = 16}) { return MaterialApp( home: SingleChildScrollView( diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index 9d6fba1a42058..9900a1101c9f5 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestIcon extends StatefulWidget { const TestIcon({super.key}); @@ -48,7 +49,7 @@ void main() { const Color unselectedWidgetColor = Colors.black54; const Color headerColor = Colors.black45; - testWidgets('ExpansionTile initial state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile initial state', (WidgetTester tester) async { final Key topKey = UniqueKey(); const Key expandedKey = PageStorageKey<String>('expanded'); const Key collapsedKey = PageStorageKey<String>('collapsed'); @@ -158,7 +159,7 @@ void main() { expect((collapsedContainerDecoration.shape as Border).bottom.color, dividerColor); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('ExpansionTile Theme dependencies', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile Theme dependencies', (WidgetTester tester) async { final Key expandedTitleKey = UniqueKey(); final Key collapsedTitleKey = UniqueKey(); final Key expandedIconKey = UniqueKey(); @@ -217,7 +218,7 @@ void main() { expect(iconColor(collapsedIconKey), foregroundColor); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('ExpansionTile subtitle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile subtitle', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -233,7 +234,7 @@ void main() { expect(find.text('Subtitle'), findsOneWidget); }); - testWidgets('ExpansionTile maintainState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile maintainState', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -271,7 +272,7 @@ void main() { expect(find.text('Discarding State'), findsNothing); }); - testWidgets('ExpansionTile padding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile padding test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -298,7 +299,7 @@ void main() { expect(listTileRect.bottom, tallerWidget.bottom + remainingHeight / 2 + 10); }); - testWidgets('ExpansionTile expandedAlignment test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile expandedAlignment test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -327,7 +328,7 @@ void main() { expect(columnRect.right, 100.0); }); - testWidgets('ExpansionTile expandedCrossAxisAlignment test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile expandedCrossAxisAlignment test', (WidgetTester tester) async { const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -371,7 +372,7 @@ void main() { expect(child1Rect.left, 700.0); }); - testWidgets('CrossAxisAlignment.baseline is not allowed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CrossAxisAlignment.baseline is not allowed', (WidgetTester tester) async { expect( () { MaterialApp( @@ -391,7 +392,7 @@ void main() { ); }); - testWidgets('expandedCrossAxisAlignment and expandedAlignment default values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('expandedCrossAxisAlignment and expandedAlignment default values', (WidgetTester tester) async { const Key child1Key = Key('child1'); await tester.pumpWidget(const MaterialApp( @@ -426,7 +427,7 @@ void main() { }); - testWidgets('childrenPadding default value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('childrenPadding default value', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -456,7 +457,7 @@ void main() { expect(columnRect.bottom, paddingRect.bottom); }); - testWidgets('ExpansionTile childrenPadding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile childrenPadding test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -487,7 +488,7 @@ void main() { expect(columnRect.bottom, paddingRect.bottom - 4); }); - testWidgets('ExpansionTile.collapsedBackgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile.collapsedBackgroundColor', (WidgetTester tester) async { const Key expansionTileKey = Key('expansionTileKey'); const Color backgroundColor = Colors.red; const Color collapsedBackgroundColor = Colors.brown; @@ -524,7 +525,7 @@ void main() { expect(shapeDecoration.color, backgroundColor); }); - testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( @@ -553,7 +554,7 @@ void main() { expect(getTextColor(), theme.colorScheme.onSurface); }); - testWidgets('ExpansionTile iconColor, textColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile iconColor, textColor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/78281 const Color iconColor = Color(0xff00ff00); @@ -590,7 +591,7 @@ void main() { expect(getTextColor(), textColor); }); - testWidgets('ExpansionTile Border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile Border', (WidgetTester tester) async { const Key expansionTileKey = PageStorageKey<String>('expansionTile'); const Border collapsedShape = Border( @@ -638,7 +639,7 @@ void main() { expect(expandedContainerDecoration.shape, shape); }); - testWidgets('ExpansionTile platform controlAffinity test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile platform controlAffinity test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: ExpansionTile( @@ -652,7 +653,7 @@ void main() { expect(listTile.trailing.runtimeType, RotationTransition); }); - testWidgets('ExpansionTile trailing controlAffinity test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile trailing controlAffinity test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: ExpansionTile( @@ -667,7 +668,7 @@ void main() { expect(listTile.trailing.runtimeType, RotationTransition); }); - testWidgets('ExpansionTile leading controlAffinity test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile leading controlAffinity test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: ExpansionTile( @@ -682,7 +683,7 @@ void main() { expect(listTile.trailing, isNull); }); - testWidgets('ExpansionTile override rotating icon test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile override rotating icon test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: ExpansionTile( @@ -698,7 +699,7 @@ void main() { expect(listTile.trailing, isNull); }); - testWidgets('Nested ListTile Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested ListTile Semantics', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final SemanticsHandle handle = tester.ensureSemantics(); @@ -753,7 +754,7 @@ void main() { handle.dispose(); }); - testWidgets('ExpansionTile Semantics announcement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile Semantics announcement', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); await tester.pumpWidget( @@ -792,7 +793,7 @@ void main() { handle.dispose(); }); - testWidgets('Semantics with the onTapHint is an ancestor of ListTile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics with the onTapHint is an ancestor of ListTile', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/pull/121624 final SemanticsHandle handle = tester.ensureSemantics(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); @@ -844,7 +845,7 @@ void main() { handle.dispose(); }); - testWidgets('Semantics hint for iOS and macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics hint for iOS and macOS', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); @@ -892,12 +893,168 @@ void main() { handle.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); + testWidgetsWithLeakTracking('Collapsed ExpansionTile properties can be updated with setState', (WidgetTester tester) async { + const Key expansionTileKey = Key('expansionTileKey'); + ShapeBorder collapsedShape = const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ); + Color collapsedTextColor = const Color(0xffffffff); + Color collapsedBackgroundColor = const Color(0xffff0000); + Color collapsedIconColor = const Color(0xffffffff); + + await tester.pumpWidget(MaterialApp( + home: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + children: <Widget>[ + ExpansionTile( + key: expansionTileKey, + collapsedShape: collapsedShape, + collapsedTextColor: collapsedTextColor, + collapsedBackgroundColor: collapsedBackgroundColor, + collapsedIconColor: collapsedIconColor, + title: const TestText('title'), + trailing: const TestIcon(), + children: const <Widget>[ + SizedBox(height: 100, width: 100), + ], + ), + // This button is used to update the ExpansionTile properties. + FilledButton( + onPressed: () { + setState(() { + collapsedShape = const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ); + collapsedTextColor = const Color(0xff000000); + collapsedBackgroundColor = const Color(0xffffff00); + collapsedIconColor = const Color(0xff000000); + }); + }, + child: const Text('Update collapsed properties'), + ), + ], + ); + } + ), + ), + )); + + ShapeDecoration shapeDecoration = tester.firstWidget<Container>(find.descendant( + of: find.byKey(expansionTileKey), + matching: find.byType(Container), + )).decoration! as ShapeDecoration; + + // Test initial ExpansionTile properties. + expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4)))); + expect(shapeDecoration.color, const Color(0xffff0000)); + expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xffffffff)); + expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xffffffff)); + + // Tap the button to update the ExpansionTile properties. + await tester.tap(find.text('Update collapsed properties')); + await tester.pumpAndSettle(); + + shapeDecoration = tester.firstWidget<Container>(find.descendant( + of: find.byKey(expansionTileKey), + matching: find.byType(Container), + )).decoration! as ShapeDecoration; + + // Test updated ExpansionTile properties. + expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16)))); + expect(shapeDecoration.color, const Color(0xffffff00)); + expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xff000000)); + expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xff000000)); + }); + + testWidgetsWithLeakTracking('Expanded ExpansionTile properties can be updated with setState', (WidgetTester tester) async { + const Key expansionTileKey = Key('expansionTileKey'); + ShapeBorder shape = const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ); + Color textColor = const Color(0xff00ffff); + Color backgroundColor = const Color(0xff0000ff); + Color iconColor = const Color(0xff00ffff); + + await tester.pumpWidget(MaterialApp( + home: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + children: <Widget>[ + ExpansionTile( + key: expansionTileKey, + shape: shape, + textColor: textColor, + backgroundColor: backgroundColor, + iconColor: iconColor, + title: const TestText('title'), + trailing: const TestIcon(), + children: const <Widget>[ + SizedBox(height: 100, width: 100), + ], + ), + // This button is used to update the ExpansionTile properties. + FilledButton( + onPressed: () { + setState(() { + shape = const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6)), + ); + textColor = const Color(0xffffffff); + backgroundColor = const Color(0xff123456); + iconColor = const Color(0xffffffff); + }); + }, + child: const Text('Update collapsed properties'), + ), + ], + ); + } + ), + ), + )); + + // Tap to expand the ExpansionTile. + await tester.tap(find.text('title')); + await tester.pumpAndSettle(); + + ShapeDecoration shapeDecoration = tester.firstWidget<Container>(find.descendant( + of: find.byKey(expansionTileKey), + matching: find.byType(Container), + )).decoration! as ShapeDecoration; + + // Test initial ExpansionTile properties. + expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12)))); + expect(shapeDecoration.color, const Color(0xff0000ff)); + expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xff00ffff)); + expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xff00ffff)); + + // Tap the button to update the ExpansionTile properties. + await tester.tap(find.text('Update collapsed properties')); + await tester.pumpAndSettle(); + + shapeDecoration = tester.firstWidget<Container>(find.descendant( + of: find.byKey(expansionTileKey), + matching: find.byType(Container), + )).decoration! as ShapeDecoration; + iconColor = tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!; + textColor = tester.state<TestTextState>(find.byType(TestText)).textStyle.color!; + + // Test updated ExpansionTile properties. + expect(shapeDecoration.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6)))); + expect(shapeDecoration.color, const Color(0xff123456)); + expect(tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, const Color(0xffffffff)); + expect(tester.state<TestTextState>(find.byType(TestText)).textStyle.color, const Color(0xffffffff)); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget(MaterialApp( @@ -927,7 +1084,7 @@ void main() { }); }); - testWidgets('ExpansionTileController isExpanded, expand() and collapse()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTileController isExpanded, expand() and collapse()', (WidgetTester tester) async { final ExpansionTileController controller = ExpansionTileController(); await tester.pumpWidget(MaterialApp( @@ -955,7 +1112,7 @@ void main() { expect(find.text('Child 0'), findsNothing); }); - testWidgets('Calling ExpansionTileController.expand/collapsed has no effect if it is already expanded/collapsed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Calling ExpansionTileController.expand/collapsed has no effect if it is already expanded/collapsed', (WidgetTester tester) async { final ExpansionTileController controller = ExpansionTileController(); await tester.pumpWidget(MaterialApp( @@ -991,7 +1148,7 @@ void main() { expect(tester.hasRunningAnimations, isFalse); }); - testWidgets('Call to ExpansionTileController.of()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Call to ExpansionTileController.of()', (WidgetTester tester) async { final GlobalKey titleKey = GlobalKey(); final GlobalKey childKey = GlobalKey(); await tester.pumpWidget(MaterialApp( @@ -1015,7 +1172,7 @@ void main() { expect(controller1, controller2); }); - testWidgets('Call to ExpansionTile.maybeOf()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Call to ExpansionTile.maybeOf()', (WidgetTester tester) async { final GlobalKey titleKey = GlobalKey(); final GlobalKey nonDescendantKey = GlobalKey(); await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/material/expansion_tile_theme_test.dart b/packages/flutter/test/material/expansion_tile_theme_test.dart index 323d3eb2540c3..f21b095156443 100644 --- a/packages/flutter/test/material/expansion_tile_theme_test.dart +++ b/packages/flutter/test/material/expansion_tile_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestIcon extends StatefulWidget { const TestIcon({super.key}); @@ -69,7 +70,7 @@ void main() { expect(theme.clipBehavior, null); }); - testWidgets('Default ExpansionTileThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ExpansionTileThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TooltipThemeData().debugFillProperties(builder); @@ -81,7 +82,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ExpansionTileThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTileThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ExpansionTileThemeData( backgroundColor: Color(0xff000000), @@ -119,7 +120,7 @@ void main() { ]); }); - testWidgets('ExpansionTileTheme - collapsed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTileTheme - collapsed', (WidgetTester tester) async { final Key tileKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key iconKey = UniqueKey(); @@ -211,7 +212,7 @@ void main() { expect(shapeDecoration.shape, collapsedShape); }); - testWidgets('ExpansionTileTheme - expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ExpansionTileTheme - expanded', (WidgetTester tester) async { final Key tileKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key iconKey = UniqueKey(); @@ -290,7 +291,7 @@ void main() { // Check the expanded text color when textColor is applied. expect(getTextColor(), textColor); // Check the expanded ShapeBorder when shape is applied. - expect(shapeDecoration.shape, collapsedShape); + expect(shapeDecoration.shape, shape); // Check the child position when expandedAlignment is applied. final Rect childRect = tester.getRect(find.text('Tile 1')); diff --git a/packages/flutter/test/material/feedback_test.dart b/packages/flutter/test/material/feedback_test.dart index f4507df0052c8..95cc70b7cb4a6 100644 --- a/packages/flutter/test/material/feedback_test.dart +++ b/packages/flutter/test/material/feedback_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -37,7 +37,7 @@ void main () { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('forTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forTap', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(TestWidget( @@ -66,7 +66,7 @@ void main () { semanticsTester.dispose(); }); - testWidgets('forTap Wrapper', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forTap Wrapper', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); int callbackCount = 0; @@ -101,7 +101,7 @@ void main () { semanticsTester.dispose(); }); - testWidgets('forLongPress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forLongPress', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(TestWidget( @@ -129,7 +129,7 @@ void main () { semanticsTester.dispose(); }); - testWidgets('forLongPress Wrapper', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forLongPress Wrapper', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); int callbackCount = 0; void callback() { @@ -166,7 +166,7 @@ void main () { }); group('Feedback on iOS', () { - testWidgets('forTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forTap', (WidgetTester tester) async { await tester.pumpWidget(Theme( data: ThemeData(platform: TargetPlatform.iOS), child: TestWidget( @@ -182,7 +182,7 @@ void main () { expect(feedback.clickSoundCount, 0); }); - testWidgets('forLongPress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forLongPress', (WidgetTester tester) async { await tester.pumpWidget(Theme( data: ThemeData(platform: TargetPlatform.iOS), child: TestWidget( diff --git a/packages/flutter/test/material/filled_button_test.dart b/packages/flutter/test/material/filled_button_test.dart index 5469933ba0358..58cfd9c7ae129 100644 --- a/packages/flutter/test/material/filled_button_test.dart +++ b/packages/flutter/test/material/filled_button_test.dart @@ -7,12 +7,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('FilledButton, FilledButton.icon defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton, FilledButton.icon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(useMaterial3: false, colorScheme: colorScheme); @@ -125,7 +124,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -238,8 +237,9 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Default FilledButton meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default FilledButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -277,7 +277,7 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('FilledButton default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -340,9 +340,10 @@ void main() { await tester.pumpAndSettle(); expect(elevation(), 0.0); expect(overlayColor(), paints..rect(color: theme.colorScheme.onPrimary.withOpacity(0.12))); + focusNode.dispose(); }); - testWidgets('FilledButton.tonal default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.tonal default overlayColor and elevation resolve pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -405,9 +406,10 @@ void main() { await tester.pumpAndSettle(); expect(elevation(), 0.0); expect(overlayColor(), paints..rect(color: theme.colorScheme.onSecondaryContainer.withOpacity(0.12))); + focusNode.dispose(); }); - testWidgets('FilledButton uses stateful color for text color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton uses stateful color for text color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -480,10 +482,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(textColor(), pressedColor); + focusNode.dispose(); }); - testWidgets('FilledButton uses stateful color for icon color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton uses stateful color for icon color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final Key buttonKey = UniqueKey(); @@ -556,9 +559,10 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(iconColor(), pressedColor); + focusNode.dispose(); }); - testWidgets('FilledButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder filledButton; @@ -601,7 +605,7 @@ void main() { expect(tester.widget<FilledButton>(filledButton).enabled, false); }); - testWidgets('FilledButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -632,7 +636,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets("FilledButton response doesn't hover when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("FilledButton response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'FilledButton Focus'); final GlobalKey childKey = GlobalKey(); @@ -680,9 +684,10 @@ void main() { await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + focusNode.dispose(); }); - testWidgets('disabled and hovered FilledButton responds to mouse-exit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled and hovered FilledButton responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; @@ -744,7 +749,7 @@ void main() { expect(hover, false); }); - testWidgets('Can set FilledButton focus and Can set unFocus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set FilledButton focus and Can set unFocus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'FilledButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -771,9 +776,10 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + node.dispose(); }); - testWidgets('When FilledButton disable, Can not set FilledButton focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When FilledButton disable, Can not set FilledButton focus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'FilledButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -794,9 +800,10 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + node.dispose(); }); - testWidgets('Does FilledButton work with hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does FilledButton work with hover', (WidgetTester tester) async { const Color hoverColor = Color(0xff001122); await tester.pumpWidget( @@ -823,7 +830,7 @@ void main() { expect(inkFeatures, paints..rect(color: hoverColor)); }); - testWidgets('Does FilledButton work with focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does FilledButton work with focus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); final FocusNode focusNode = FocusNode(debugLabel: 'FilledButton Node'); @@ -849,9 +856,10 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + focusNode.dispose(); }); - testWidgets('Does FilledButton work with autofocus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does FilledButton work with autofocus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -879,9 +887,10 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + focusNode.dispose(); }); - testWidgets('Does FilledButton contribute semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does FilledButton contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( @@ -929,7 +938,7 @@ void main() { semantics.dispose(); }); - testWidgets('FilledButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { const ButtonStyle style = ButtonStyle( // Specifying minimumSize to mimic the original minimumSize for // RaisedButton so that the corresponding button size matches @@ -963,7 +972,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); }); - testWidgets('FilledButton has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton has no clip by default', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -980,7 +989,7 @@ void main() { ); }); - testWidgets('FilledButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -1049,7 +1058,7 @@ void main() { expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); }); - testWidgets('FilledButton.icon responds to applied padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.icon responds to applied padding', (WidgetTester tester) async { const Key buttonKey = Key('test'); const Key labelKey = Key('label'); await tester.pumpWidget( @@ -1176,7 +1185,7 @@ void main() { if (textDirection == TextDirection.rtl) 'RTL', ].join(', '); - testWidgets(testName, (WidgetTester tester) async { + testWidgetsWithLeakTracking(testName, (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1317,7 +1326,7 @@ void main() { } }); - testWidgets('Override FilledButton default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override FilledButton default padding', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light()), @@ -1351,7 +1360,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.all(22)); }); - testWidgets('M3 FilledButton has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 FilledButton has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1377,7 +1386,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 24)); }); - testWidgets('M3 FilledButton.icon has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 FilledButton.icon has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1404,7 +1413,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(16.0, 0.0, 24.0, 0.0)); }); - testWidgets('By default, FilledButton shape outline is defined by shape.side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('By default, FilledButton shape outline is defined by shape.side', (WidgetTester tester) async { const Color borderColor = Color(0xff4caf50); await tester.pumpWidget( MaterialApp( @@ -1433,7 +1442,7 @@ void main() { ); }); - testWidgets('Fixed size FilledButtons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size FilledButtons', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1466,7 +1475,7 @@ void main() { expect(tester.getSize(find.widgetWithText(FilledButton, 'wx200')).height, 200); }); - testWidgets('FilledButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) { return MaterialApp( home: Scaffold( @@ -1506,7 +1515,7 @@ void main() { } }); - testWidgets('FilledButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -1533,7 +1542,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('FilledButton.icon does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.icon does not overflow', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/77815 await tester.pumpWidget( MaterialApp( @@ -1554,7 +1563,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('FilledButton.icon icon,label layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.icon icon,label layout', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); final Key iconKey = UniqueKey(); final Key labelKey = UniqueKey(); @@ -1591,7 +1600,7 @@ void main() { expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0)); }); - testWidgets('FilledButton maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton maximumSize', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); @@ -1633,7 +1642,7 @@ void main() { expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0)); }); - testWidgets('Fixed size FilledButton, same as minimumSize == maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size FilledButton, same as minimumSize == maximumSize', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1663,7 +1672,7 @@ void main() { expect(tester.getSize(find.widgetWithText(FilledButton, '200,200')), const Size(200, 200)); }); - testWidgets('FilledButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1741,7 +1750,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('FilledButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104595. await tester.pumpWidget(MaterialApp( home: SelectionArea( @@ -1764,7 +1773,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('Ink Response shape matches Material shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ink Response shape matches Material shape', (WidgetTester tester) async { Widget buildFrame({BorderSide? side}) { return MaterialApp( home: Scaffold( @@ -1807,7 +1816,7 @@ void main() { ); }); - testWidgets('FilledButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1837,6 +1846,7 @@ void main() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( @@ -1937,20 +1947,21 @@ void main() { await gesture.removePointer(); } - testWidgets('FilledButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton statesController', (WidgetTester tester) async { testStatesController(null, tester); }); - testWidgets('FilledButton.icon statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilledButton.icon statesController', (WidgetTester tester) async { testStatesController(const Icon(Icons.add), tester); }); - testWidgets('Disabled FilledButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled FilledButton statesController', (WidgetTester tester) async { int count = 0; void valueChanged() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/filled_button_theme_test.dart b/packages/flutter/test/material/filled_button_theme_test.dart index 5b0d7c4dbe52f..2b0ff185dd5a2 100644 --- a/packages/flutter/test/material/filled_button_theme_test.dart +++ b/packages/flutter/test/material/filled_button_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('FilledButtonThemeData lerp special cases', () { @@ -12,7 +13,7 @@ void main() { expect(identical(FilledButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Passing no FilledButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no FilledButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -153,19 +154,19 @@ void main() { expect(align.alignment, alignment); } - testWidgets('Button style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(themeStyle: style)); await tester.pumpAndSettle(); checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallStyle: style)); await tester.pumpAndSettle(); checkButton(tester); @@ -173,26 +174,26 @@ void main() { // Same as the previous tests with empty ButtonStyle's instead of null. - testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); }); - testWidgets('Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); diff --git a/packages/flutter/test/material/filter_chip_test.dart b/packages/flutter/test/material/filter_chip_test.dart index c4aaa40a06f28..3d46afa83fd1b 100644 --- a/packages/flutter/test/material/filter_chip_test.dart +++ b/packages/flutter/test/material/filter_chip_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; /// Adds the basic requirements for a Chip. Widget wrapForChip({ @@ -115,7 +114,7 @@ DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) { } void main() { - testWidgets('FilterChip defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'filter chip'; @@ -135,7 +134,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(FilterChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(FilterChip)), + within(distance: 0.001, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, label).style.color!.value, @@ -247,7 +249,7 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); - testWidgets('FilterChip.elevated defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip.elevated defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const String label = 'filter chip'; @@ -267,7 +269,10 @@ void main() { ); // Test default chip size. - expect(tester.getSize(find.byType(FilterChip)), const Size(190.0, 48.0)); + expect( + tester.getSize(find.byType(FilterChip)), + within(distance: 0.001, from: const Size(189.1, 48.0)), + ); // Test default label style. expect( getLabelStyle(tester, 'filter chip').style.color!.value, @@ -379,7 +384,7 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); - testWidgets('FilterChip.color resolves material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip.color resolves material states', (WidgetTester tester) async { const Color disabledSelectedColor = Color(0xffffff00); const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); @@ -479,7 +484,7 @@ void main() { ); }); - testWidgets('FilterChip uses provided state color properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip uses provided state color properties', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); const Color selectedColor = Color(0xffff0000); @@ -554,7 +559,7 @@ void main() { ); }); - testWidgets('FilterChip can be tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -570,7 +575,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Filter chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Filter chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { await pumpCheckmarkChip( theme: ThemeData(useMaterial3: false), tester, @@ -583,7 +588,7 @@ void main() { ); }); - testWidgets('Filter chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Filter chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedFilterChip(), @@ -597,7 +602,7 @@ void main() { ); }); - testWidgets('Filter chip check mark color can be set by the chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Filter chip check mark color can be set by the chip theme', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedFilterChip(), @@ -610,7 +615,7 @@ void main() { ); }); - testWidgets('Filter chip check mark color can be set by the chip constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Filter chip check mark color can be set by the chip constructor', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedFilterChip(checkmarkColor: const Color(0xff00ff00)), @@ -622,7 +627,7 @@ void main() { ); }); - testWidgets('Filter chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Filter chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedFilterChip(checkmarkColor: const Color(0xffff0000)), @@ -635,7 +640,7 @@ void main() { ); }); - testWidgets('FilterChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FilterChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { const Text label = Text('label'); await tester.pumpWidget(wrapForChip(child: FilterChip(label: label, onSelected: (bool b) { }))); checkChipMaterialClipBehavior(tester, Clip.none); @@ -644,7 +649,7 @@ void main() { checkChipMaterialClipBehavior(tester, Clip.antiAlias); }); - testWidgets('M3 width should not change with selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 width should not change with selection', (WidgetTester tester) async { // Regression tests for: https://github.com/flutter/flutter/issues/110645 // For the text "FilterChip" the chip should default to 175 regardless of selection. diff --git a/packages/flutter/test/material/flexible_space_bar_collapse_mode_test.dart b/packages/flutter/test/material/flexible_space_bar_collapse_mode_test.dart index ebd7a21896391..c5ce2842a6643 100644 --- a/packages/flutter/test/material/flexible_space_bar_collapse_mode_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_collapse_mode_test.dart @@ -5,13 +5,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final Key blockKey = UniqueKey(); const double expandedAppbarHeight = 250.0; final Key appbarContainerKey = UniqueKey(); void main() { - testWidgets('FlexibleSpaceBar collapse mode none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar collapse mode none', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: debugDefaultTargetPlatformOverride), @@ -49,7 +50,7 @@ void main() { expect(topAfterScroll.dy, equals(0.0)); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.fuchsia })); - testWidgets('FlexibleSpaceBar collapse mode pin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar collapse mode pin', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: debugDefaultTargetPlatformOverride), @@ -87,7 +88,7 @@ void main() { expect(topAfterScroll.dy, equals(-100.0)); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.fuchsia })); - testWidgets('FlexibleSpaceBar collapse mode parallax', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar collapse mode parallax', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: debugDefaultTargetPlatformOverride), diff --git a/packages/flutter/test/material/flexible_space_bar_stretch_mode_test.dart b/packages/flutter/test/material/flexible_space_bar_stretch_mode_test.dart index 86efe7056f7bc..1b047096e7aac 100644 --- a/packages/flutter/test/material/flexible_space_bar_stretch_mode_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_stretch_mode_test.dart @@ -9,13 +9,14 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final Key blockKey = UniqueKey(); const double expandedAppbarHeight = 250.0; final Key finderKey = UniqueKey(); void main() { - testWidgets('FlexibleSpaceBar stretch mode default zoomBackground', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar stretch mode default zoomBackground', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -50,7 +51,7 @@ void main() { expect(sizeBeforeScroll.height, lessThan(sizeAfterScroll.height)); }); - testWidgets('FlexibleSpaceBar stretch mode blurBackground', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar stretch mode blurBackground', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -90,7 +91,7 @@ void main() { ); }); - testWidgets('FlexibleSpaceBar stretch mode fadeTitle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar stretch mode fadeTitle', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -134,7 +135,7 @@ void main() { expect(opacityWidget.opacity, equals(0.0)); }); - testWidgets('FlexibleSpaceBar stretch mode ignored for non-overscroll physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar stretch mode ignored for non-overscroll physics', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart index 7477480f9f969..283197333b918 100644 --- a/packages/flutter/test/material/flexible_space_bar_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_test.dart @@ -10,11 +10,11 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('FlexibleSpaceBar centers title on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar centers title on iOS', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), @@ -57,7 +57,7 @@ void main() { } }); - testWidgets('FlexibleSpaceBarSettings provides settings to a FlexibleSpaceBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBarSettings provides settings to a FlexibleSpaceBar', (WidgetTester tester) async { const double minExtent = 100.0; const double initExtent = 200.0; const double maxExtent = 300.0; @@ -133,7 +133,7 @@ void main() { expect(clipRect.size.height, minExtent); }); - testWidgets('FlexibleSpaceBar.background is visible when using height other than kToolbarHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar.background is visible when using height other than kToolbarHeight', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80451 await tester.pumpWidget( MaterialApp( @@ -168,7 +168,7 @@ void main() { expect(backgroundOpacity.opacity, 1.0); }); - testWidgets('Collapsed FlexibleSpaceBar has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Collapsed FlexibleSpaceBar has correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const double expandedHeight = 200; await tester.pumpWidget( @@ -434,7 +434,7 @@ void main() { }); // This is a regression test for https://github.com/flutter/flutter/issues/14227 - testWidgets('FlexibleSpaceBar sets width constraints for the title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar sets width constraints for the title', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; late double width; @@ -470,9 +470,7 @@ void main() { ), ); - final double textWidth = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? width - : (width / 1.5).floorToDouble() * 1.5; + final double textWidth = width; // The title is scaled and transformed to be 1.5 times bigger, when the // FlexibleSpaceBar is fully expanded, thus we expect the width to be // 1.5 times smaller than the full width. The height of the text is the same @@ -483,7 +481,7 @@ void main() { ); }); - testWidgets('FlexibleSpaceBar sets constraints for the title - override expandedTitleScale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar sets constraints for the title - override expandedTitleScale', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; const double expandedTitleScale = 3.0; @@ -541,9 +539,7 @@ void main() { // bottom edge. const double bottomMargin = titleFontSize * (expandedTitleScale - 1); - final double textWidth = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? collapsedWidth - : (collapsedWidth / 3).floorToDouble() * 3; + final double textWidth = collapsedWidth; // The title is scaled and transformed to be 3 times bigger, when the // FlexibleSpaceBar is fully expanded, thus we expect the width to be // 3 times smaller than the full width. The height of the text is the same @@ -554,7 +550,7 @@ void main() { ); }); - testWidgets('FlexibleSpaceBar scaled title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar scaled title', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; await tester.pumpWidget( @@ -614,7 +610,7 @@ void main() { ); }); - testWidgets('FlexibleSpaceBar scaled title - override expandedTitleScale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar scaled title - override expandedTitleScale', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; const double expandedTitleScale = 3.0; @@ -677,7 +673,7 @@ void main() { ); }); - testWidgets('FlexibleSpaceBar test titlePadding defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar test titlePadding defaults', (WidgetTester tester) async { Widget buildFrame(TargetPlatform platform, bool? centerTitle) { return MaterialApp( theme: ThemeData(platform: platform, useMaterial3: false), @@ -727,7 +723,7 @@ void main() { }); - testWidgets('FlexibleSpaceBar test titlePadding override', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar test titlePadding override', (WidgetTester tester) async { Widget buildFrame(TargetPlatform platform, bool? centerTitle) { return MaterialApp( theme: ThemeData(platform: platform, useMaterial3: false), @@ -795,7 +791,7 @@ void main() { expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); }); - testWidgets('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: SubCategoryScreenView(), )); @@ -825,6 +821,94 @@ void main() { expect(RenderRebuildTracker.count, greaterThan(1)); expect(tester.layers.whereType<OpacityLayer>(), isEmpty); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/132030. + testWidgetsWithLeakTracking('FlexibleSpaceBarSettings.hasLeading provides a gap between leading and title', (WidgetTester tester) async { + final FlexibleSpaceBarSettings customSettings = FlexibleSpaceBar.createSettings( + currentExtent: 200.0, + hasLeading: true, + child: AppBar( + leading: const Icon(Icons.menu), + flexibleSpace: FlexibleSpaceBar( + title: Text('title ' * 10), + centerTitle: true, + ), + ), + ) as FlexibleSpaceBarSettings; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + slivers: <Widget>[ + SliverPersistentHeader( + floating: true, + pinned: true, + delegate: TestDelegate(settings: customSettings), + ), + SliverToBoxAdapter( + child: Container( + height: 1200.0, + color: Colors.orange[400], + ), + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.byType(Text)).dx, closeTo(72.0, 0.01)); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/132030. + testWidgetsWithLeakTracking('Long centered FlexibleSpaceBar.title respects leading widget', (WidgetTester tester) async { + // Test start position of a long title when the leading widget is + // shown by default and the long title is centered. + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + drawer: const Drawer(), + body: CustomScrollView( + slivers: <Widget>[ + SliverAppBar( + flexibleSpace: FlexibleSpaceBar( + title: Text('Title ' * 10), + centerTitle: true, + ), + ), + ], + ), + ), + ), + ); + + expect(tester.getTopLeft(find.byType(Text)).dx, 72.0); + + // Test start position of a long title when the leading widget is provided + // and the long title is centered. + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + slivers: <Widget>[ + SliverAppBar( + leading: const Icon(Icons.menu), + flexibleSpace: FlexibleSpaceBar( + title: Text('Title ' * 10), + centerTitle: true, + ), + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.byType(Text)).dx, 72.0); + }); } class TestDelegate extends SliverPersistentHeaderDelegate { @@ -907,21 +991,17 @@ class _SubCategoryScreenViewState extends State<SubCategoryScreenView> ), ), const SliverToBoxAdapter(child: SizedBox(height: 12)), - SliverToBoxAdapter( - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: 300, - itemBuilder: (BuildContext context, int index) { - return Card( - color: Colors.amber, - child: Center(child: Text('$index')), - ); - }, + SliverGrid.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, ), + itemCount: 300, + itemBuilder: (BuildContext context, int index) { + return Card( + color: Colors.amber, + child: Center(child: Text('$index')), + ); + }, ), const SliverToBoxAdapter(child: SizedBox(height: 12)), ], diff --git a/packages/flutter/test/material/floating_action_button_location_test.dart b/packages/flutter/test/material/floating_action_button_location_test.dart index faefd63c90cfa..a94253aa2e99a 100644 --- a/packages/flutter/test/material/floating_action_button_location_test.dart +++ b/packages/flutter/test/material/floating_action_button_location_test.dart @@ -7,10 +7,11 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('Basic floating action button locations', () { - testWidgets('still animates motion when the floating action button is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('still animates motion when the floating action button is null', (WidgetTester tester) async { await tester.pumpWidget(_buildFrame(fab: null)); expect(find.byType(FloatingActionButton), findsNothing); @@ -27,7 +28,7 @@ void main() { expect(tester.binding.transientCallbackCount, greaterThan(0)); }); - testWidgets('moves fab from center to end and back', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moves fab from center to end and back', (WidgetTester tester) async { await tester.pumpWidget(_buildFrame(location: FloatingActionButtonLocation.endFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0)); @@ -52,7 +53,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('moves to and from custom-defined positions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moves to and from custom-defined positions', (WidgetTester tester) async { await tester.pumpWidget(_buildFrame(location: const _StartTopFloatingActionButtonLocation())); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0)); @@ -173,7 +174,7 @@ void main() { previousRotations = null; }); - testWidgets('moving the fab to centerFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moving the fab to centerFloat', (WidgetTester tester) async { // Create a scaffold with the fab at endFloat await tester.pumpWidget(_buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener)); setupListener(tester); @@ -183,7 +184,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('interrupting motion towards the StartTop location.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('interrupting motion towards the StartTop location.', (WidgetTester tester) async { await tester.pumpWidget(_buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener)); setupListener(tester); @@ -196,7 +197,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('interrupting entrance to remove the fab.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('interrupting entrance to remove the fab.', (WidgetTester tester) async { await tester.pumpWidget(_buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat, listener: geometryListener)); setupListener(tester); @@ -215,7 +216,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('interrupting entrance of a new fab.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('interrupting entrance of a new fab.', (WidgetTester tester) async { await tester.pumpWidget( _buildFrame( fab: null, @@ -241,7 +242,7 @@ void main() { }); }); - testWidgets('Docked floating action button locations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Docked floating action button locations', (WidgetTester tester) async { await tester.pumpWidget( _buildFrame( location: FloatingActionButtonLocation.endDocked, @@ -276,7 +277,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0)); }); - testWidgets('Docked floating action button locations: no BAB, small BAB', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Docked floating action button locations: no BAB, small BAB', (WidgetTester tester) async { await tester.pumpWidget( _buildFrame( location: FloatingActionButtonLocation.endDocked, @@ -295,7 +296,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0)); }); - testWidgets('Contained floating action button locations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Contained floating action button locations', (WidgetTester tester) async { await tester.pumpWidget( _buildFrame( location: FloatingActionButtonLocation.endContained, @@ -310,7 +311,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 550.0)); }); - testWidgets('Mini-start-top floating action button location', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mini-start-top floating action button location', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -331,7 +332,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)).dy, kToolbarHeight); }); - testWidgets('Start-top floating action button location LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Start-top floating action button location LTR', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -344,7 +345,7 @@ void main() { expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(16.0, 28.0, 56.0, 56.0))); }); - testWidgets('End-top floating action button location RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('End-top floating action button location RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -360,7 +361,7 @@ void main() { expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(16.0, 28.0, 56.0, 56.0))); }); - testWidgets('Start-top floating action button location RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Start-top floating action button location RTL', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -376,7 +377,7 @@ void main() { expect(tester.getRect(find.byType(FloatingActionButton)), rectMoreOrLessEquals(const Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0))); }); - testWidgets('End-top floating action button location LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('End-top floating action button location LTR', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -390,103 +391,103 @@ void main() { }); group('New Floating Action Button Locations', () { - testWidgets('startTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _topOffsetY)); }); - testWidgets('centerTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); }); - testWidgets('endTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _topOffsetY)); }); - testWidgets('startFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _floatOffsetY)); }); - testWidgets('centerFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _floatOffsetY)); }); - testWidgets('endFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _floatOffsetY)); }); - testWidgets('startDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.startDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _dockedOffsetY)); }); - testWidgets('centerDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _dockedOffsetY)); }); - testWidgets('endDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _dockedOffsetY)); }); - testWidgets('endContained', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endContained', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endContained)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _containedOffsetY)); }); - testWidgets('miniStartTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _topOffsetY)); }); - testWidgets('miniEndTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _topOffsetY)); }); - testWidgets('miniStartFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _miniFloatOffsetY)); }); - testWidgets('miniCenterFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniCenterFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniCenterFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _miniFloatOffsetY)); }); - testWidgets('miniEndFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndFloat', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndFloat)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _miniFloatOffsetY)); }); - testWidgets('miniStartDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniLeftOffsetX, _dockedOffsetY)); }); - testWidgets('miniEndDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndDocked', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniEndDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _dockedOffsetY)); @@ -494,13 +495,13 @@ void main() { // Test a few RTL cases. - testWidgets('endTop, RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endTop, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endTop, textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _topOffsetY)); }); - testWidgets('miniStartFloat, RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartFloat, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartFloat, textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_miniRightOffsetX, _miniFloatOffsetY)); @@ -508,25 +509,25 @@ void main() { }); group('Custom Floating Action Button Locations', () { - testWidgets('Almost end float', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Almost end float', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_AlmostEndFloatFabLocation())); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX - 50, _floatOffsetY)); }); - testWidgets('Almost end float, RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Almost end float, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_AlmostEndFloatFabLocation(), textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX + 50, _floatOffsetY)); }); - testWidgets('Quarter end top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Quarter end top', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_QuarterEndTopFabLocation())); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX * 0.75 + _leftOffsetX * 0.25, _topOffsetY)); }); - testWidgets('Quarter end top, RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Quarter end top, RTL', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(_QuarterEndTopFabLocation(), textDirection: TextDirection.rtl)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX * 0.75 + _rightOffsetX * 0.25, _topOffsetY)); @@ -534,7 +535,7 @@ void main() { }); group('Moves involving new locations', () { - testWidgets('Moves between new locations and new locations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moves between new locations and new locations', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.centerTop)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); @@ -556,7 +557,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_leftOffsetX, _dockedOffsetY)); }); - testWidgets('Moves between new locations and old locations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moves between new locations and old locations', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endDocked)); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _dockedOffsetY)); @@ -586,7 +587,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_centerOffsetX, _topOffsetY)); }); - testWidgets('Moves between new locations and old locations with custom animator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moves between new locations and old locations with custom animator', (WidgetTester tester) async { final FloatingActionButtonAnimator animator = _LinearMovementFabAnimator(); const Offset begin = Offset(_centerOffsetX, _topOffsetY); const Offset end = Offset(_rightOffsetX - 50, _floatOffsetY); @@ -628,7 +629,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('Animator can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animator can be updated', (WidgetTester tester) async { FloatingActionButtonAnimator fabAnimator = FloatingActionButtonAnimator.scaling; FloatingActionButtonLocation fabLocation = FloatingActionButtonLocation.startFloat; @@ -999,7 +1000,7 @@ void main() { ); } - testWidgets('startFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(16.0, 478.0, 72.0, 534.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(16.0, 422.0, 72.0, 478.0); @@ -1017,7 +1018,7 @@ void main() { ); }); - testWidgets('miniStartFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(12.0, 490.0, 60.0, 538.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(12.0, 434.0, 60.0, 482.0); @@ -1036,7 +1037,7 @@ void main() { ); }); - testWidgets('centerFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(372.0, 478.0, 428.0, 534.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(372.0, 422.0, 428.0, 478.0); @@ -1054,7 +1055,7 @@ void main() { ); }); - testWidgets('miniCenterFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniCenterFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(376.0, 490.0, 424.0, 538.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(376.0, 434.0, 424.0, 482.0); @@ -1073,7 +1074,7 @@ void main() { ); }); - testWidgets('endFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(728.0, 478.0, 784.0, 534.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(728.0, 422.0, 784.0, 478.0); @@ -1091,7 +1092,7 @@ void main() { ); }); - testWidgets('miniEndFloat', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndFloat', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(740.0, 490.0, 788.0, 538.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(740.0, 434.0, 788.0, 482.0); @@ -1362,7 +1363,7 @@ void main() { ); } - testWidgets('startDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(16.0, 494.0, 72.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(16.0, 466.0, 72.0, 522.0); @@ -1380,7 +1381,7 @@ void main() { ); }); - testWidgets('miniStartDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(12.0, 502.0, 60.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(12.0, 470.0, 60.0, 518.0); @@ -1399,7 +1400,7 @@ void main() { ); }); - testWidgets('centerDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(372.0, 494.0, 428.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(372.0, 466.0, 428.0, 522.0); @@ -1417,7 +1418,7 @@ void main() { ); }); - testWidgets('miniCenterDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniCenterDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(376.0, 502.0, 424.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(376.0, 470.0, 424.0, 518.0); @@ -1436,7 +1437,7 @@ void main() { ); }); - testWidgets('endDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(728.0, 494.0, 784.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(728.0, 466.0, 784.0, 522.0); @@ -1454,7 +1455,7 @@ void main() { ); }); - testWidgets('miniEndDocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndDocked', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(740.0, 502.0, 788.0, 550.0); // Positioned relative to BottomNavigationBar const Rect bottomNavigationBarRect = Rect.fromLTRB(740.0, 470.0, 788.0, 518.0); @@ -1511,7 +1512,7 @@ void main() { ); } - testWidgets('startTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('startTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(16.0, 50.0, 72.0, 106.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(16.0, 28.0, 72.0, 84.0); @@ -1523,7 +1524,7 @@ void main() { ); }); - testWidgets('miniStartTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniStartTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(12.0, 50.0, 60.0, 98.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(12.0, 32.0, 60.0, 80.0); @@ -1536,7 +1537,7 @@ void main() { ); }); - testWidgets('centerTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('centerTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(372.0, 50.0, 428.0, 106.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(372.0, 28.0, 428.0, 84.0); @@ -1548,7 +1549,7 @@ void main() { ); }); - testWidgets('miniCenterTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniCenterTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(376.0, 50.0, 424.0, 98.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(376.0, 32.0, 424.0, 80.0); @@ -1561,7 +1562,7 @@ void main() { ); }); - testWidgets('endTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('endTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(728.0, 50.0, 784.0, 106.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(728.0, 28.0, 784.0, 84.0); @@ -1573,7 +1574,7 @@ void main() { ); }); - testWidgets('miniEndTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('miniEndTop', (WidgetTester tester) async { const Rect defaultRect = Rect.fromLTRB(740.0, 50.0, 788.0, 98.0); // Positioned relative to AppBar const Rect appBarRect = Rect.fromLTRB(740.0, 32.0, 788.0, 80.0); diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 2511fade5f177..7c286f442396c 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -13,8 +13,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -22,7 +21,7 @@ void main() { final ThemeData material3Theme = ThemeData(useMaterial3: true); final ThemeData material2Theme = ThemeData(useMaterial3: false); - testWidgets('Floating Action Button control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button control test', (WidgetTester tester) async { bool didPressButton = false; await tester.pumpWidget( Directionality( @@ -43,7 +42,7 @@ void main() { expect(didPressButton, isTrue); }); - testWidgets('Floating Action Button tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button tooltip', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -61,7 +60,7 @@ void main() { }); // Regression test for: https://github.com/flutter/flutter/pull/21084 - testWidgets('Floating Action Button tooltip (long press button edge)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button tooltip (long press button edge)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -82,7 +81,7 @@ void main() { }); // Regression test for: https://github.com/flutter/flutter/pull/21084 - testWidgets('Floating Action Button tooltip (long press button edge - no child)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button tooltip (long press button edge - no child)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -101,7 +100,7 @@ void main() { expect(find.text('Add'), findsOneWidget); }); - testWidgets('Floating Action Button tooltip (no child)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button tooltip (no child)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -136,7 +135,7 @@ void main() { expect(find.text('Add'), findsOneWidget); }); - testWidgets('Floating Action Button tooltip reacts when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button tooltip reacts when disabled', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -172,7 +171,7 @@ void main() { expect(find.text('Add'), findsOneWidget); }); - testWidgets('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material3Theme, @@ -210,7 +209,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button elevation when disabled - defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when disabled - defaults', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -225,7 +224,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button elevation when disabled - override', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when disabled - override', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -240,7 +239,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 0.0); }); - testWidgets('Floating Action Button elevation when disabled - effect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when disabled - effect', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -279,7 +278,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material3Theme, @@ -326,7 +325,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button states elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button states elevation', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -368,9 +367,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(getFABWidget(fabFinder).elevation, 6); + + focusNode.dispose(); }); - testWidgets('FlatActionButton mini size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FlatActionButton mini size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -408,7 +409,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); }); - testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.isExtended', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material3Theme, @@ -480,7 +481,7 @@ void main() { expect(tester.getSize(fabFinder).width, 168); }); - testWidgets('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async { final Finder fabFinder = find.byType(FloatingActionButton); FloatingActionButton getFabWidget() { @@ -533,7 +534,7 @@ void main() { expect(tester.getSize(fabFinder).width, 140); }); - testWidgets('Floating Action Button heroTag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button heroTag', (WidgetTester tester) async { late BuildContext theContext; await tester.pumpWidget( MaterialApp( @@ -556,7 +557,7 @@ void main() { await tester.pump(); // this would fail if heroTag was the same on both FloatingActionButtons (see below). }); - testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async { late BuildContext theContext; await tester.pumpWidget( MaterialApp( @@ -580,7 +581,7 @@ void main() { expect(tester.takeException().toString(), contains('FloatingActionButton')); }); - testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async { late BuildContext theContext; await tester.pumpWidget( MaterialApp( @@ -604,7 +605,7 @@ void main() { expect(tester.takeException().toString(), contains('xyzzy')); }); - testWidgets('Floating Action Button semantics (enabled)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button semantics (enabled)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -639,7 +640,7 @@ void main() { semantics.dispose(); }); - testWidgets('Floating Action Button semantics (disabled)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button semantics (disabled)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -669,7 +670,7 @@ void main() { semantics.dispose(); }); - testWidgets('Tooltip is used as semantics tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip is used as semantics tooltip', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -719,7 +720,7 @@ void main() { semantics.dispose(); }); - testWidgets('extended FAB hero transitions succeed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extended FAB hero transitions succeed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/18782 await tester.pumpWidget( @@ -785,7 +786,7 @@ void main() { }); // This test prevents https://github.com/flutter/flutter/issues/20483 - testWidgets('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -813,7 +814,7 @@ void main() { ); }); - testWidgets('Floating Action Button changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -890,7 +891,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button has no clip by default', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( Directionality( @@ -909,9 +910,11 @@ void main() { tester.renderObject(find.byType(FloatingActionButton)), paintsExactlyCountTimes(#clipPath, 0), ); + + focusNode.dispose(); }); - testWidgets('Can find FloatingActionButton semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can find FloatingActionButton semantics', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: FloatingActionButton(onPressed: () {}), )); @@ -928,7 +931,7 @@ void main() { ); }); - testWidgets('Foreground color applies to icon on fab', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Foreground color applies to icon on fab', (WidgetTester tester) async { const Color foregroundColor = Color(0xcafefeed); await tester.pumpWidget(MaterialApp( @@ -945,7 +948,7 @@ void main() { expect(iconRichText.text.style!.color, foregroundColor); }); - testWidgets('FloatingActionButton uses custom splash color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton uses custom splash color', (WidgetTester tester) async { const Color splashColor = Color(0xcafefeed); await tester.pumpWidget(MaterialApp( @@ -966,7 +969,7 @@ void main() { ); }); - testWidgets('extended FAB does not show label when isExtended is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extended FAB does not show label when isExtended is false', (WidgetTester tester) async { const Key iconKey = Key('icon'); const Key labelKey = Key('label'); @@ -987,7 +990,7 @@ void main() { expect(find.byKey(labelKey), findsNothing); }); - testWidgets('FloatingActionButton.small configures correct size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.small configures correct size', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1004,7 +1007,7 @@ void main() { expect(tester.getSize(find.byKey(key)), const Size(40.0, 40.0)); }); - testWidgets('FloatingActionButton.large configures correct size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.large configures correct size', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1020,7 +1023,7 @@ void main() { expect(tester.getSize(find.byKey(key)), const Size(96.0, 96.0)); }); - testWidgets('FloatingActionButton.extended can customize spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.extended can customize spacing', (WidgetTester tester) async { const Key iconKey = Key('icon'); const Key labelKey = Key('label'); const double spacing = 33.0; @@ -1045,7 +1048,7 @@ void main() { expect(tester.getTopRight(find.byType(FloatingActionButton)).dx - tester.getTopRight(find.byKey(labelKey)).dx, padding.end); }); - testWidgets('FloatingActionButton.extended can customize text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.extended can customize text style', (WidgetTester tester) async { const Key labelKey = Key('label'); const TextStyle style = TextStyle(letterSpacing: 2.0); @@ -1078,7 +1081,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when highlighted - effect', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material2Theme, @@ -1117,7 +1120,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button elevation when disabled while highlighted - effect', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material2Theme, @@ -1164,7 +1167,7 @@ void main() { expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0); }); - testWidgets('Floating Action Button states elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button states elevation', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -1206,9 +1209,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(getFABWidget(fabFinder).elevation, 12); + + focusNode.dispose(); }); - testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.isExtended', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: material2Theme, @@ -1275,7 +1280,7 @@ void main() { expect(tester.getSize(fabFinder).width, 168); }); - testWidgets('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async { final Finder fabFinder = find.byType(FloatingActionButton); FloatingActionButton getFabWidget() { @@ -1327,7 +1332,7 @@ void main() { // This test prevents https://github.com/flutter/flutter/issues/20483 - testWidgets('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1367,7 +1372,7 @@ void main() { feedback.dispose(); }); - testWidgets('FloatingActionButton with enabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton with enabled feedback', (WidgetTester tester) async { const bool enableFeedback = true; await tester.pumpWidget(MaterialApp( @@ -1384,7 +1389,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('FloatingActionButton with disabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton with disabled feedback', (WidgetTester tester) async { const bool enableFeedback = false; await tester.pumpWidget(MaterialApp( @@ -1401,7 +1406,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('FloatingActionButton with enabled feedback by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton with enabled feedback by default', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: FloatingActionButton( onPressed: () {}, @@ -1415,7 +1420,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('FloatingActionButton with disabled feedback using FloatingActionButtonTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton with disabled feedback using FloatingActionButtonTheme', (WidgetTester tester) async { const bool enableFeedbackTheme = false; final ThemeData theme = ThemeData( floatingActionButtonTheme: const FloatingActionButtonThemeData( @@ -1439,7 +1444,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('FloatingActionButton.enableFeedback is overridden by FloatingActionButtonThemeData.enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.enableFeedback is overridden by FloatingActionButtonThemeData.enableFeedback', (WidgetTester tester) async { const bool enableFeedbackTheme = false; const bool enableFeedback = true; final ThemeData theme = ThemeData( diff --git a/packages/flutter/test/material/floating_action_button_theme_test.dart b/packages/flutter/test/material/floating_action_button_theme_test.dart index f3d54f69957ea..1c282d714fa32 100644 --- a/packages/flutter/test/material/floating_action_button_theme_test.dart +++ b/packages/flutter/test/material/floating_action_button_theme_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('FloatingActionButtonThemeData copyWith, ==, hashCode basics', () { @@ -19,7 +20,7 @@ void main() { expect(identical(FloatingActionButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Material3: Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget(MaterialApp( theme: ThemeData.from(useMaterial3: true, colorScheme: colorScheme), @@ -44,7 +45,7 @@ void main() { expect(_getIconSize(tester).height, 24.0); }); - testWidgets('Material2: Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Default values are used when no FloatingActionButton or FloatingActionButtonThemeData properties are specified', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget(MaterialApp( theme: ThemeData.from(useMaterial3: false, colorScheme: colorScheme), @@ -69,7 +70,7 @@ void main() { expect(_getIconSize(tester).height, 24.0); }); - testWidgets('FloatingActionButtonThemeData values are used when no FloatingActionButton properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButtonThemeData values are used when no FloatingActionButton properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0xBEEFBEEF); const Color foregroundColor = Color(0xFACEFACE); const Color splashColor = Color(0xCAFEFEED); @@ -110,7 +111,7 @@ void main() { expect(_getRawMaterialButton(tester).constraints, constraints); }); - testWidgets('FloatingActionButton values take priority over FloatingActionButtonThemeData values when both properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton values take priority over FloatingActionButtonThemeData values when both properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const Color foregroundColor = Color(0x00000002); const Color splashColor = Color(0x00000003); @@ -155,7 +156,7 @@ void main() { expect(_getRawMaterialButton(tester).splashColor, splashColor); }); - testWidgets('FloatingActionButton uses a custom shape when specified in the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton uses a custom shape when specified in the theme', (WidgetTester tester) async { const ShapeBorder customShape = BeveledRectangleBorder(); await tester.pumpWidget(MaterialApp( @@ -170,7 +171,7 @@ void main() { expect(_getRawMaterialButton(tester).shape, customShape); }); - testWidgets('FloatingActionButton.small uses custom constraints when specified in the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.small uses custom constraints when specified in the theme', (WidgetTester tester) async { const BoxConstraints constraints = BoxConstraints.tightFor(width: 100.0, height: 100.0); const double iconSize = 24.0; @@ -193,7 +194,7 @@ void main() { expect(_getIconSize(tester).height, iconSize); }); - testWidgets('FloatingActionButton.large uses custom constraints when specified in the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.large uses custom constraints when specified in the theme', (WidgetTester tester) async { const BoxConstraints constraints = BoxConstraints.tightFor(width: 100.0, height: 100.0); const double iconSize = 36.0; @@ -216,7 +217,7 @@ void main() { expect(_getIconSize(tester).height, iconSize); }); - testWidgets('Material3: FloatingActionButton.extended uses custom properties when specified in the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: FloatingActionButton.extended uses custom properties when specified in the theme', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Key iconKey = Key('icon'); const Key labelKey = Key('label'); @@ -253,7 +254,7 @@ void main() { expect(_getRawMaterialButton(tester).textStyle, textStyle.copyWith(color: colorScheme.onPrimaryContainer)); }); - testWidgets('Material2: FloatingActionButton.extended uses custom properties when specified in the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: FloatingActionButton.extended uses custom properties when specified in the theme', (WidgetTester tester) async { const Key iconKey = Key('icon'); const Key labelKey = Key('label'); const BoxConstraints constraints = BoxConstraints.tightFor(height: 100.0); @@ -287,7 +288,7 @@ void main() { expect(_getRawMaterialButton(tester).textStyle, textStyle.copyWith(color: const Color(0xffffffff))); }); - testWidgets('Material3: FloatingActionButton.extended custom properties takes priority over FloatingActionButtonThemeData spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: FloatingActionButton.extended custom properties takes priority over FloatingActionButtonThemeData spacing', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Key iconKey = Key('icon'); const Key labelKey = Key('label'); @@ -324,7 +325,7 @@ void main() { expect(_getRawMaterialButton(tester).textStyle, textStyle.copyWith(color: colorScheme.onPrimaryContainer)); }); - testWidgets('Material2: FloatingActionButton.extended custom properties takes priority over FloatingActionButtonThemeData spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: FloatingActionButton.extended custom properties takes priority over FloatingActionButtonThemeData spacing', (WidgetTester tester) async { const Key iconKey = Key('icon'); const Key labelKey = Key('label'); const double iconLabelSpacing = 33.0; @@ -358,7 +359,7 @@ void main() { expect(_getRawMaterialButton(tester).textStyle, textStyle.copyWith(color: const Color(0xffffffff))); }); - testWidgets('default FloatingActionButton debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default FloatingActionButton debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const FloatingActionButtonThemeData ().debugFillProperties(builder); @@ -370,7 +371,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('Material implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const FloatingActionButtonThemeData( foregroundColor: Color(0xFEEDFEED), @@ -426,7 +427,7 @@ void main() { ]); }); - testWidgets('FloatingActionButton.mouseCursor uses FloatingActionButtonThemeData.mouseCursor when specified.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton.mouseCursor uses FloatingActionButtonThemeData.mouseCursor when specified.', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData().copyWith( floatingActionButtonTheme: FloatingActionButtonThemeData( diff --git a/packages/flutter/test/material/flutter_logo_test.dart b/packages/flutter/test/material/flutter_logo_test.dart index 284a965c039f2..66d9d2484ebe5 100644 --- a/packages/flutter/test/material/flutter_logo_test.dart +++ b/packages/flutter/test/material/flutter_logo_test.dart @@ -9,8 +9,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('Flutter Logo golden test', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/grid_title_test.dart b/packages/flutter/test/material/grid_title_test.dart index b3f1f1c85696b..ff5bd5bd5f7ad 100644 --- a/packages/flutter/test/material/grid_title_test.dart +++ b/packages/flutter/test/material/grid_title_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('GridTile control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridTile control test', (WidgetTester tester) async { final Key headerKey = UniqueKey(); final Key footerKey = UniqueKey(); diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 949a467b1be26..44da15a62e6ec 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -7,8 +7,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -28,7 +27,7 @@ void main() { mockOnPressedFunction = MockOnPressedFunction(); }); - testWidgets('test icon is findable by key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test icon is findable by key', (WidgetTester tester) async { const ValueKey<String> key = ValueKey<String>('icon-button'); await tester.pumpWidget( wrap( @@ -44,7 +43,7 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - testWidgets('test default icon buttons are sized up to 48', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test default icon buttons are sized up to 48', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -63,7 +62,7 @@ void main() { expect(mockOnPressedFunction.called, 1); }); - testWidgets('test small icons are sized up to 48dp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test small icons are sized up to 48dp', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -80,7 +79,7 @@ void main() { expect(iconButton.size, const Size(48.0, 48.0)); }); - testWidgets('test icons can be small when total size is >48dp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test icons can be small when total size is >48dp', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -98,15 +97,16 @@ void main() { expect(iconButton.size, const Size(70.0, 70.0)); }); - testWidgets('when both iconSize and IconTheme.of(context).size are null, size falls back to 24.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when both iconSize and IconTheme.of(context).size are null, size falls back to 24.0', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; + final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( wrap( useMaterial3: material3, child: IconTheme( data: const IconThemeData(), child: IconButton( - focusNode: FocusNode(debugLabel: 'Ink Focus'), + focusNode: focusNode, onPressed: mockOnPressedFunction.handler, icon: const Icon(Icons.link), ), @@ -116,9 +116,11 @@ void main() { final RenderBox icon = tester.renderObject(find.byType(Icon)); expect(icon.size, const Size(24.0, 24.0)); + + focusNode.dispose(); }); - testWidgets('when null, iconSize is overridden by closest IconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when null, iconSize is overridden by closest IconTheme', (WidgetTester tester) async { RenderBox icon; final bool material3 = theme.useMaterial3; @@ -202,7 +204,7 @@ void main() { expect(icon.size, const Size(10.0, 10.0)); }); - testWidgets('when non-null, iconSize precedes IconTheme.of(context).size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when non-null, iconSize precedes IconTheme.of(context).size', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -222,7 +224,7 @@ void main() { expect(icon.size, const Size(10.0, 10.0)); }); - testWidgets('Small icons with non-null constraints can be <48dp for M2, but =48dp for M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small icons with non-null constraints can be <48dp for M2, but =48dp for M3', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -247,7 +249,7 @@ void main() { expect(icon.size, const Size(10.0, 10.0)); }); - testWidgets('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( @@ -273,7 +275,7 @@ void main() { expect(icon.size, const Size(10.0, 10.0)); }); - testWidgets('Small icons comply with VisualDensity requirements', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small icons comply with VisualDensity requirements', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; final ThemeData themeDataM2 = ThemeData( useMaterial3: material3, @@ -311,7 +313,7 @@ void main() { expect(iconButton.size, material3 ? const Size(52.0, 44.0) : const Size(36.0, 28.0)); }); - testWidgets('test default icon buttons are constrained', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test default icon buttons are constrained', (WidgetTester tester) async { await tester.pumpWidget( wrap( useMaterial3: theme.useMaterial3, @@ -328,7 +330,7 @@ void main() { expect(box.size, const Size(80.0, 80.0)); }); - testWidgets('test default icon buttons can be stretched if specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test default icon buttons can be stretched if specified', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -372,7 +374,7 @@ void main() { expect(boxM3.size, const Size(48.0, 600.0)); }); - testWidgets('test default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test default padding', (WidgetTester tester) async { await tester.pumpWidget( wrap( useMaterial3: theme.useMaterial3, @@ -388,7 +390,7 @@ void main() { expect(box.size, const Size(96.0, 96.0)); }); - testWidgets('test default alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test default alignment', (WidgetTester tester) async { await tester.pumpWidget( wrap( useMaterial3: theme.useMaterial3, @@ -404,7 +406,7 @@ void main() { expect(align.alignment, Alignment.center); }); - testWidgets('test tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test tooltip', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme, @@ -446,7 +448,7 @@ void main() { expect(mockOnPressedFunction.called, 1); }); - testWidgets('IconButton AppBar size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton AppBar size', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( MaterialApp( @@ -473,7 +475,7 @@ void main() { // This test is very similar to the '...explicit splashColor and highlightColor' test // in buttons_test.dart. If you change this one, you may want to also change that one. - testWidgets('IconButton with explicit splashColor and highlightColor - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton with explicit splashColor and highlightColor - M2', (WidgetTester tester) async { const Color directSplashColor = Color(0xFF00000F); const Color directHighlightColor = Color(0xFF0000F0); @@ -559,7 +561,7 @@ void main() { await gesture.up(); }); - testWidgets('IconButton with explicit splash radius - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton with explicit splash radius - M2', (WidgetTester tester) async { const double splashRadius = 30.0; await tester.pumpWidget( MaterialApp( @@ -590,7 +592,7 @@ void main() { await gesture.up(); }); - testWidgets('IconButton Semantics (enabled) - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton Semantics (enabled) - M2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -624,7 +626,7 @@ void main() { semantics.dispose(); }); - testWidgets('IconButton Semantics (disabled) - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton Semantics (disabled) - M2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -653,7 +655,7 @@ void main() { semantics.dispose(); }); - testWidgets('IconButton Semantics (selected) - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton Semantics (selected) - M3', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -709,7 +711,7 @@ void main() { semantics.dispose(); }); - testWidgets('IconButton loses focus when disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton loses focus when disabled.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); await tester.pumpWidget( wrap( @@ -739,9 +741,11 @@ void main() { ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isFalse); + + focusNode.dispose(); }); - testWidgets('IconButton keeps focus when disabled in directional navigation mode.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton keeps focus when disabled in directional navigation mode.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); await tester.pumpWidget( wrap( @@ -781,15 +785,17 @@ void main() { ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); + + focusNode.dispose(); }); - testWidgets("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async { final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1'); final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2'); await tester.pumpWidget( wrap( - useMaterial3: false, + useMaterial3: theme.useMaterial3, child: Column( children: <Widget>[ IconButton( @@ -812,11 +818,14 @@ void main() { expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); - expect(focusNode1.nextFocus(), isTrue); + expect(focusNode1.nextFocus(), isFalse); await tester.pump(); expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); + + focusNode1.dispose(); + focusNode2.dispose(); }); group('feedback', () { @@ -830,7 +839,7 @@ void main() { feedback.dispose(); }); - testWidgets('IconButton with disabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton with disabled feedback', (WidgetTester tester) async { final Widget button = Directionality( textDirection: TextDirection.ltr, child: Center( @@ -853,7 +862,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('IconButton with enabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton with enabled feedback', (WidgetTester tester) async { final Widget button = Directionality( textDirection: TextDirection.ltr, child: Center( @@ -875,7 +884,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('IconButton with enabled feedback by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton with enabled feedback by default', (WidgetTester tester) async { final Widget button = Directionality( textDirection: TextDirection.ltr, child: Center( @@ -898,7 +907,7 @@ void main() { }); }); - testWidgets('IconButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); final bool material3 = theme.useMaterial3; Future<void> buildTest(VisualDensity visualDensity) async { @@ -942,7 +951,7 @@ void main() { expect(box.size, equals(material3 ? const Size(64, 36) : const Size(60, 40))); }); - testWidgets('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async { // Test argument works await tester.pumpWidget( MaterialApp( @@ -990,7 +999,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('disabled IconButton has basic mouse cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled IconButton has basic mouse cursor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme, @@ -1016,7 +1025,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('IconButton.mouseCursor overrides implicit setting of mouse cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.mouseCursor overrides implicit setting of mouse cursor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme, @@ -1063,7 +1072,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none); }); - testWidgets('IconTheme opacity test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme opacity test', (WidgetTester tester) async { final ThemeData theme = ThemeData.from(colorScheme: colorScheme, useMaterial3: false); await tester.pumpWidget( @@ -1106,7 +1115,7 @@ void main() { expect(iconColorWithOpacity(), Colors.purple.withOpacity(0.5)); }); - testWidgets('IconButton defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled IconButton @@ -1189,7 +1198,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('IconButton default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -1240,9 +1249,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurfaceVariant.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('IconButton.fill defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.fill defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled IconButton @@ -1328,7 +1339,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('IconButton.fill default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.fill default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -1379,9 +1390,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.onPrimary.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('Toggleable IconButton.fill defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggleable IconButton.fill defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled selected IconButton @@ -1496,7 +1509,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('IconButton.filledTonal defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.filledTonal defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled IconButton.tonal @@ -1582,7 +1595,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('IconButton.filledTonal default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.filledTonal default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -1633,9 +1646,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.onSecondaryContainer.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('Toggleable IconButton.filledTonal defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggleable IconButton.filledTonal defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled selected IconButton @@ -1750,7 +1765,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('IconButton.outlined defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.outlined defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled IconButton.tonal @@ -1836,7 +1851,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('IconButton.outlined default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton.outlined default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -1887,9 +1902,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurfaceVariant.withOpacity(0.08))); + + focusNode.dispose(); }); - testWidgets('Toggleable IconButton.outlined defaults - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggleable IconButton.outlined defaults - M3', (WidgetTester tester) async { final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true); // Enabled selected IconButton @@ -2004,7 +2021,7 @@ void main() { expect(iconColor(), colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('Default IconButton meets a11y contrast guidelines - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default IconButton meets a11y contrast guidelines - M3', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -2047,11 +2064,13 @@ void main() { await expectLater(tester, meetsGuideline(textContrastGuideline)); await gesture.removePointer(); + + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('IconButton uses stateful color for icon color in different states - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton uses stateful color for icon color in different states - M3', (WidgetTester tester) async { bool isSelected = false; final FocusNode focusNode = FocusNode(); @@ -2135,9 +2154,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(iconColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('Does IconButton contribute semantics - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does IconButton contribute semantics - M3', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -2184,7 +2205,7 @@ void main() { semantics.dispose(); }); - testWidgets('IconButton size is configurable by ThemeData.materialTapTargetSize - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconButton size is configurable by ThemeData.materialTapTargetSize - M3', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize) { return Theme( data: ThemeData(materialTapTargetSize: tapTargetSize, useMaterial3: true), @@ -2208,7 +2229,7 @@ void main() { expect(tester.getSize(find.byType(IconButton)), const Size(40.0, 40.0)); }); - testWidgets('Override IconButton default padding - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override IconButton default padding - M3', (WidgetTester tester) async { // Use [IconButton]'s padding property to override default value. await tester.pumpWidget( MaterialApp( @@ -2284,7 +2305,7 @@ void main() { expect(paddingWidget3.padding, const EdgeInsets.all(22)); }); - testWidgets('Default IconButton is not selectable - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default IconButton is not selectable - M3', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true), @@ -2318,7 +2339,7 @@ void main() { expect(buttonMaterial().color, Colors.transparent); }); - testWidgets('Icon button is selectable when isSelected is not null - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon button is selectable when isSelected is not null - M3', (WidgetTester tester) async { bool isSelected = false; await tester.pumpWidget( MaterialApp( @@ -2371,7 +2392,7 @@ void main() { expect(buttonMaterial().color, Colors.transparent); }); - testWidgets('The IconButton is in selected status if isSelected is true by default - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The IconButton is in selected status if isSelected is true by default - M3', (WidgetTester tester) async { bool isSelected = true; await tester.pumpWidget( MaterialApp( @@ -2417,7 +2438,7 @@ void main() { expect(buttonMaterial().color, Colors.transparent); }); - testWidgets("The selectedIcon is used if it's not null and the button is clicked" , (WidgetTester tester) async { + testWidgetsWithLeakTracking("The selectedIcon is used if it's not null and the button is clicked" , (WidgetTester tester) async { bool isSelected = false; await tester.pumpWidget( MaterialApp( @@ -2457,7 +2478,7 @@ void main() { expect(find.byIcon(Icons.account_box), findsNothing); }); - testWidgets('The original icon is used for selected and unselected status when selectedIcon is null' , (WidgetTester tester) async { + testWidgetsWithLeakTracking('The original icon is used for selected and unselected status when selectedIcon is null' , (WidgetTester tester) async { bool isSelected = false; await tester.pumpWidget( MaterialApp( @@ -2493,7 +2514,7 @@ void main() { expect(find.byIcon(Icons.account_box), findsOneWidget); }); - testWidgets('The selectedIcon is used for disabled button if isSelected is true - M3' , (WidgetTester tester) async { + testWidgetsWithLeakTracking('The selectedIcon is used for disabled button if isSelected is true - M3' , (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true), @@ -2514,7 +2535,7 @@ void main() { expect(find.byIcon(Icons.ac_unit), findsOneWidget); }); - testWidgets('The visualDensity of M3 IconButton can be configured by IconButtonTheme, ' + testWidgetsWithLeakTracking('The visualDensity of M3 IconButton can be configured by IconButtonTheme, ' 'but cannot be configured by ThemeData - M3' , (WidgetTester tester) async { Future<void> buildTest({VisualDensity? iconButtonThemeVisualDensity, VisualDensity? themeVisualDensity}) async { return tester.pumpWidget( @@ -2565,7 +2586,7 @@ void main() { }); group('IconTheme tests in Material 3', () { - testWidgets('IconTheme overrides default values in M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconTheme overrides default values in M3', (WidgetTester tester) async { // Theme's IconTheme await tester.pumpWidget( MaterialApp( @@ -2608,7 +2629,7 @@ void main() { expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(35, 35)),); }); - testWidgets('Theme IconButtonTheme overrides IconTheme in Material3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme IconButtonTheme overrides IconTheme in Material3', (WidgetTester tester) async { // When IconButtonTheme and IconTheme both exist in ThemeData, the IconButtonTheme can override IconTheme. await tester.pumpWidget( MaterialApp( @@ -2631,7 +2652,7 @@ void main() { expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(27, 27)),); }); - testWidgets('Button IconButtonTheme always overrides IconTheme in Material3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button IconButtonTheme always overrides IconTheme in Material3', (WidgetTester tester) async { // When IconButtonTheme is closer to IconButton, IconButtonTheme overrides IconTheme await tester.pumpWidget( MaterialApp( @@ -2681,7 +2702,7 @@ void main() { expect(tester.getSize(find.byIcon(Icons.account_box)), equals(const Size(36, 36)),); }); - testWidgets('White icon color defined by users shows correctly in Material3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('White icon color defined by users shows correctly in Material3', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from( @@ -2701,7 +2722,7 @@ void main() { expect(iconColor1(), Colors.white); }); - testWidgets('In light mode, icon color is M3 default color instead of IconTheme.of(context).color, ' + testWidgetsWithLeakTracking('In light mode, icon color is M3 default color instead of IconTheme.of(context).color, ' 'if only setting color in IconTheme', (WidgetTester tester) async { final ColorScheme darkScheme = const ColorScheme.dark().copyWith(onSurfaceVariant: const Color(0xffe91e60)); // Brightness.dark @@ -2724,34 +2745,34 @@ void main() { expect(iconColor0(), darkScheme.onSurfaceVariant); // onSurfaceVariant }); - testWidgets('In dark mode, icon color is M3 default color instead of IconTheme.of(context).color, ' + testWidgetsWithLeakTracking('In dark mode, icon color is M3 default color instead of IconTheme.of(context).color, ' 'if only setting color in IconTheme', (WidgetTester tester) async { final ColorScheme lightScheme = const ColorScheme.light().copyWith(onSurfaceVariant: const Color(0xffe91e60)); // Brightness.dark await tester.pumpWidget( - MaterialApp( - theme: ThemeData(colorScheme: lightScheme, useMaterial3: true,), - home: Scaffold( - body: IconTheme.merge( - data: const IconThemeData(size: 26), - child: IconButton( - icon: const Icon(Icons.account_box), - onPressed: () {}, - ), - ), - ) + MaterialApp( + theme: ThemeData(colorScheme: lightScheme, useMaterial3: true,), + home: Scaffold( + body: IconTheme.merge( + data: const IconThemeData(size: 26), + child: IconButton( + icon: const Icon(Icons.account_box), + onPressed: () {}, + ), + ), ) + ) ); Color? iconColor0() => _iconStyle(tester, Icons.account_box)?.color; expect(iconColor0(), lightScheme.onSurfaceVariant); // onSurfaceVariant }); - testWidgets('black87 icon color defined by users shows correctly in Material3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('black87 icon color defined by users shows correctly in Material3', (WidgetTester tester) async { }); - testWidgets("IconButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("IconButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/118071. await tester.pumpWidget( Directionality( @@ -2770,6 +2791,25 @@ void main() { expect(tester.takeException(), isNull); }); + + testWidgetsWithLeakTracking('Material3 - IconButton memory leak', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/130708. + Widget buildWidget(bool showIconButton) { + return showIconButton + ? MaterialApp( + theme: ThemeData(useMaterial3: true), + home: IconButton( + onPressed: () { }, + icon: const Icon(Icons.search), + ), + ) + : const SizedBox(); + } + await tester.pumpWidget(buildWidget(true)); + await tester.pumpWidget(buildWidget(false)); + + // No exception is thrown. + }); }); } diff --git a/packages/flutter/test/material/icon_button_theme_test.dart b/packages/flutter/test/material/icon_button_theme_test.dart index 7cc64cf8744ff..f0f3c8cd89fb5 100644 --- a/packages/flutter/test/material/icon_button_theme_test.dart +++ b/packages/flutter/test/material/icon_button_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('IconButtonThemeData lerp special cases', () { @@ -12,7 +13,7 @@ void main() { expect(identical(IconButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no IconButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -146,19 +147,19 @@ void main() { expect(align.alignment, alignment); } - testWidgets('Button style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(themeStyle: style)); await tester.pumpAndSettle(); checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallStyle: style)); await tester.pumpAndSettle(); checkButton(tester); @@ -166,26 +167,26 @@ void main() { // Same as the previous tests with empty ButtonStyle's instead of null. - testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); }); - testWidgets('Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); diff --git a/packages/flutter/test/material/icons_test.dart b/packages/flutter/test/material/icons_test.dart index 01106758c97f0..adc51f4d32fd3 100644 --- a/packages/flutter/test/material/icons_test.dart +++ b/packages/flutter/test/material/icons_test.dart @@ -12,21 +12,22 @@ import 'package:file/local.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:platform/platform.dart'; void main() { - testWidgets('IconData object test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconData object test', (WidgetTester tester) async { expect(Icons.account_balance, isNot(equals(Icons.account_box))); expect(Icons.account_balance.hashCode, isNot(equals(Icons.account_box.hashCode))); expect(Icons.account_balance, hasOneLineDescription); }); - testWidgets('Icons specify the material font', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icons specify the material font', (WidgetTester tester) async { expect(Icons.clear.fontFamily, 'MaterialIcons'); expect(Icons.search.fontFamily, 'MaterialIcons'); }); - testWidgets('Certain icons (and their variants) match text direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Certain icons (and their variants) match text direction', (WidgetTester tester) async { expect(Icons.arrow_back.matchTextDirection, true); expect(Icons.arrow_back_rounded.matchTextDirection, true); expect(Icons.arrow_back_outlined.matchTextDirection, true); @@ -38,7 +39,7 @@ void main() { expect(Icons.access_time_sharp.matchTextDirection, false); }); - testWidgets('Adaptive icons are correct on cupertino platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adaptive icons are correct on cupertino platforms', (WidgetTester tester) async { expect(Icons.adaptive.arrow_back, Icons.arrow_back_ios); expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_ios_outlined); }, @@ -48,7 +49,7 @@ void main() { }), ); - testWidgets('Adaptive icons are correct on non-cupertino platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adaptive icons are correct on non-cupertino platforms', (WidgetTester tester) async { expect(Icons.adaptive.arrow_back, Icons.arrow_back); expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_outlined); }, @@ -60,7 +61,7 @@ void main() { }), ); - testWidgets('A sample of icons look as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A sample of icons look as expected', (WidgetTester tester) async { await _loadIconFont(); await tester.pumpWidget(const MaterialApp( @@ -84,7 +85,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/39998 // Regression test for https://github.com/flutter/flutter/issues/95886 - testWidgets('Another sample of icons look as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Another sample of icons look as expected', (WidgetTester tester) async { await _loadIconFont(); await tester.pumpWidget(const MaterialApp( @@ -104,7 +105,7 @@ void main() { await expectLater(find.byType(Wrap), matchesGoldenFile('test.icons.sample2.png')); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/39998 - testWidgets('Another sample of icons look as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Another sample of icons look as expected', (WidgetTester tester) async { await _loadIconFont(); await tester.pumpWidget(const MaterialApp( @@ -125,7 +126,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/39998 // Regression test for https://github.com/flutter/flutter/issues/103202. - testWidgets('Another sample of icons look as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Another sample of icons look as expected', (WidgetTester tester) async { await _loadIconFont(); await tester.pumpWidget(const MaterialApp( diff --git a/packages/flutter/test/material/inherited_theme_test.dart b/packages/flutter/test/material/inherited_theme_test.dart index 9cffa4706e76d..40ace7382aa50 100644 --- a/packages/flutter/test/material/inherited_theme_test.dart +++ b/packages/flutter/test/material/inherited_theme_test.dart @@ -4,11 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Theme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme.wrap()', (WidgetTester tester) async { const Color primaryColor = Color(0xFF00FF00); final Key primaryContainerKey = UniqueKey(); @@ -93,7 +92,7 @@ void main() { expect(containerColor(), isNot(primaryColor)); }); - testWidgets('PopupMenuTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuTheme.wrap()', (WidgetTester tester) async { const double menuFontSize = 24; const Color menuTextColor = Color(0xFF0000FF); @@ -147,7 +146,7 @@ void main() { await tester.pumpAndSettle(); // menu route animation }); - testWidgets('BannerTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BannerTheme.wrap()', (WidgetTester tester) async { const Color bannerBackgroundColor = Color(0xFF0000FF); const double bannerFontSize = 48; const Color bannerTextColor = Color(0xFF00FF00); @@ -245,7 +244,7 @@ void main() { expect(getTextStyle('hello').color, isNot(bannerTextColor)); }); - testWidgets('DividerTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DividerTheme.wrap()', (WidgetTester tester) async { const Color dividerColor = Color(0xFF0000FF); const double dividerSpace = 13; const double dividerThickness = 7; @@ -327,7 +326,7 @@ void main() { expect(dividerBorder().width, isNot(dividerThickness)); }); - testWidgets('ListTileTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme.wrap()', (WidgetTester tester) async { const Color tileSelectedColor = Color(0xFF00FF00); const Color tileIconColor = Color(0xFF0000FF); const Color tileTextColor = Color(0xFFFF0000); @@ -438,7 +437,7 @@ void main() { expect(getIconStyle(unselectedIconKey).color, isNot(tileIconColor)); }); - testWidgets('SliderTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderTheme.wrap()', (WidgetTester tester) async { const Color activeTrackColor = Color(0xFF00FF00); const Color inactiveTrackColor = Color(0xFF0000FF); const Color thumbColor = Color(0xFFFF0000); @@ -521,7 +520,7 @@ void main() { expect(sliderBox, isNot(paints..circle(color: thumbColor))); }); - testWidgets('ToggleButtonsTheme.wrap()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtonsTheme.wrap()', (WidgetTester tester) async { const Color buttonColor = Color(0xFF00FF00); const Color selectedButtonColor = Color(0xFFFF0000); diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart index e62cc7b7107c7..14b0b500fe9f6 100644 --- a/packages/flutter/test/material/ink_paint_test.dart +++ b/packages/flutter/test/material/ink_paint_test.dart @@ -5,11 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('The Ink widget expands when no dimensions are set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The Ink widget expands when no dimensions are set', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Ink(), @@ -19,7 +18,7 @@ void main() { expect(tester.getSize(find.byType(Ink)), const Size(800.0, 600.0)); }); - testWidgets('The Ink widget fits the specified size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The Ink widget fits the specified size', (WidgetTester tester) async { const double height = 150.0; const double width = 200.0; await tester.pumpWidget( @@ -37,7 +36,7 @@ void main() { expect(tester.getSize(find.byType(Ink)), const Size(width, height)); }); - testWidgets('The Ink widget expands on a unspecified dimension', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The Ink widget expands on a unspecified dimension', (WidgetTester tester) async { const double height = 150.0; await tester.pumpWidget( Material( @@ -53,7 +52,7 @@ void main() { expect(tester.getSize(find.byType(Ink)), const Size(800, height)); }); - testWidgets('The InkWell widget renders an ink splash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The InkWell widget renders an ink splash', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xAA0000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); @@ -102,7 +101,7 @@ void main() { await gesture.up(); }); - testWidgets('The InkWell widget renders an ink ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The InkWell widget renders an ink ripple', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); @@ -188,7 +187,7 @@ void main() { expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 0)); }); - testWidgets('Does the Ink widget render anything', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does the Ink widget render anything', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -271,12 +270,13 @@ void main() { await gesture.up(); }); - testWidgets('The InkWell widget renders an SelectAction or ActivateAction-induced ink ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The InkWell widget renders an SelectAction or ActivateAction-induced ink ripple', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); Future<void> buildTest(Intent intent) async { return tester.pumpWidget( Shortcuts( @@ -371,7 +371,7 @@ void main() { expect(box, ripplePattern(105.0, 0)); }); - testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/14391 await tester.pumpWidget( Directionality( @@ -404,7 +404,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); @@ -454,9 +454,13 @@ void main() { })); }); - testWidgets('The InkWell widget on OverlayPortal does not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The InkWell widget on OverlayPortal does not throw', (WidgetTester tester) async { final OverlayPortalController controller = OverlayPortalController(); controller.show(); + + late OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Center( child: RepaintBoundary( @@ -466,7 +470,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return Center( child: SizedBox.square( @@ -514,7 +518,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async { const Color splashColor = Color(0xff00ff00); Widget buildWidget({InteractiveInkFeatureFactory? splashFactory}) { @@ -569,7 +573,7 @@ void main() { ); }); - testWidgets('Ink with isVisible=false does not paint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ink with isVisible=false does not paint', (WidgetTester tester) async { const Color testColor = Color(0xffff1234); Widget inkWidget({required bool isVisible}) { return Material( diff --git a/packages/flutter/test/material/ink_sparkle_test.dart b/packages/flutter/test/material/ink_sparkle_test.dart index 6fea3242e3d3d..40b47d7625173 100644 --- a/packages/flutter/test/material/ink_sparkle_test.dart +++ b/packages/flutter/test/material/ink_sparkle_test.dart @@ -10,11 +10,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/src/foundation/constants.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('InkSparkle in a Button compiles and does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkSparkle in a Button compiles and does not crash', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Center( @@ -34,7 +33,7 @@ void main() { skip: kIsWeb, // [intended] shaders are not yet supported for web. ); - testWidgets('InkSparkle default splashFactory paints with drawRect when bounded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkSparkle default splashFactory paints with drawRect when bounded', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Center( @@ -65,7 +64,7 @@ void main() { skip: kIsWeb, // [intended] shaders are not yet supported for web. ); - testWidgets('InkSparkle default splashFactory paints with drawPaint when unbounded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkSparkle default splashFactory paints with drawPaint when unbounded', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Center( @@ -92,23 +91,41 @@ void main() { // Goldens // ///////////// - testWidgets('InkSparkle renders with sparkles when top left of button is tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - InkSparkle renders with sparkles when top left of button is tapped', (WidgetTester tester) async { await _runTest(tester, 'top_left', 0.2); }, skip: kIsWeb, // [intended] shaders are not yet supported for web. ); - testWidgets('InkSparkle renders with sparkles when center of button is tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - InkSparkle renders with sparkles when top left of button is tapped', (WidgetTester tester) async { + await _runM3Test(tester, 'top_left', 0.2); + }, + skip: kIsWeb, // [intended] shaders are not yet supported for web. + ); + + testWidgetsWithLeakTracking('Material2 - InkSparkle renders with sparkles when center of button is tapped', (WidgetTester tester) async { await _runTest(tester, 'center', 0.5); }, skip: kIsWeb, // [intended] shaders are not yet supported for web. ); - testWidgets('InkSparkle renders with sparkles when bottom right of button is tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - InkSparkle renders with sparkles when center of button is tapped', (WidgetTester tester) async { + await _runM3Test(tester, 'center', 0.5); + }, + skip: kIsWeb, // [intended] shaders are not yet supported for web. + ); + + testWidgetsWithLeakTracking('Material2 - InkSparkle renders with sparkles when bottom right of button is tapped', (WidgetTester tester) async { await _runTest(tester, 'bottom_right', 0.8); }, skip: kIsWeb, // [intended] shaders are not yet supported for web. ); + + testWidgetsWithLeakTracking('Material3 - InkSparkle renders with sparkles when bottom right of button is tapped', (WidgetTester tester) async { + await _runM3Test(tester, 'bottom_right', 0.8); + }, + skip: kIsWeb, // [intended] shaders are not yet supported for web. + ); } Future<void> _runTest(WidgetTester tester, String positionName, double distanceFromTopLeft) async { @@ -145,7 +162,46 @@ Future<void> _runTest(WidgetTester tester, String positionName, double distanceF await tester.pump(const Duration(milliseconds: 50)); await expectLater( repaintFinder, - matchesGoldenFile('ink_sparkle.$positionName.$i.png'), + matchesGoldenFile('m2_ink_sparkle.$positionName.$i.png'), + ); + } +} + +Future<void> _runM3Test(WidgetTester tester, String positionName, double distanceFromTopLeft) async { + final Key repaintKey = UniqueKey(); + final Key buttonKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: Center( + child: RepaintBoundary( + key: repaintKey, + child: ElevatedButton( + key: buttonKey, + style: ElevatedButton.styleFrom(splashFactory: InkSparkle.constantTurbulenceSeedSplashFactory), + child: const Text('Sparkle!'), + onPressed: () { }, + ), + ), + ), + ), + )); + + final Finder buttonFinder = find.byKey(buttonKey); + final Finder repaintFinder = find.byKey(repaintKey); + final Offset topLeft = tester.getTopLeft(buttonFinder); + final Offset bottomRight = tester.getBottomRight(buttonFinder); + + await _warmUpShader(tester, buttonFinder); + + final Offset target = topLeft + (bottomRight - topLeft) * distanceFromTopLeft; + await tester.tapAt(target); + for (int i = 0; i <= 5; i++) { + await tester.pump(const Duration(milliseconds: 50)); + await expectLater( + repaintFinder, + matchesGoldenFile('m3_ink_sparkle.$positionName.$i.png'), ); } } diff --git a/packages/flutter/test/material/ink_splash_test.dart b/packages/flutter/test/material/ink_splash_test.dart index f178004ebceea..ceb1c4b114f49 100644 --- a/packages/flutter/test/material/ink_splash_test.dart +++ b/packages/flutter/test/material/ink_splash_test.dart @@ -4,12 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/21506. - testWidgets('InkSplash receives textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkSplash receives textDirection', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Button Border Test')), @@ -27,7 +26,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('InkWell with NoSplash splashFactory paints nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell with NoSplash splashFactory paints nothing', (WidgetTester tester) async { Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) { return MaterialApp( theme: ThemeData(useMaterial3: false), diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index 60d64ed420d8e..e2ffd3f116b76 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -7,13 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; void main() { - testWidgets('InkWell gestures control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell gestures control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -80,7 +79,7 @@ void main() { expect(log, equals(<String>['tap-down', 'tap-cancel'])); }); - testWidgets('InkWell only onTapDown enables gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell only onTapDown enables gestures', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/96030 bool downTapped = false; await tester.pumpWidget(Directionality( @@ -100,7 +99,7 @@ void main() { expect(downTapped, true); }); - testWidgets('InkWell invokes activation actions when expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell invokes activation actions when expected', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -132,7 +131,7 @@ void main() { expect(log, equals(<String>['tap'])); }); - testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press and tap on disabled should not throw', (WidgetTester tester) async { await tester.pumpWidget(const Material( child: Directionality( textDirection: TextDirection.ltr, @@ -147,7 +146,7 @@ void main() { await tester.pump(const Duration(seconds: 1)); }); - testWidgets('ink well changes color on hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink well changes color on hover', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, @@ -176,7 +175,7 @@ void main() { expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00))); }); - testWidgets('ink well changes color on hover with overlayColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink well changes color on hover with overlayColor', (WidgetTester tester) async { // Same test as 'ink well changes color on hover' except that the // hover color is specified with the overlayColor parameter. await tester.pumpWidget(Material( @@ -215,7 +214,7 @@ void main() { expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00))); }); - testWidgets('ink response changes color on focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response changes color on focus', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( @@ -250,9 +249,10 @@ void main() { inkFeatures, paints ..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)), ); + focusNode.dispose(); }); - testWidgets('ink response changes color on focus with overlayColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response changes color on focus with overlayColor', (WidgetTester tester) async { // Same test as 'ink well changes color on focus' except that the // hover color is specified with the overlayColor parameter. FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -298,9 +298,10 @@ void main() { inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)), ); + focusNode.dispose(); }); - testWidgets('ink well changes color on pressed with overlayColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink well changes color on pressed with overlayColor', (WidgetTester tester) async { const Color pressedColor = Color(0xffdd00ff); await tester.pumpWidget(Material( @@ -334,7 +335,7 @@ void main() { await gesture.up(); }); - testWidgets('ink response splashColor matches splashColor parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response splashColor matches splashColor parameter', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); const Color splashColor = Color(0xffff0000); @@ -370,9 +371,10 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..circle(x: 50, y: 50, color: splashColor)); await gesture.up(); + focusNode.dispose(); }); - testWidgets('ink response splashColor matches resolved overlayColor for MaterialState.pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response splashColor matches resolved overlayColor for MaterialState.pressed', (WidgetTester tester) async { // Same test as 'ink response splashColor matches splashColor // parameter' except that the splash color is specified with the // overlayColor parameter. @@ -419,9 +421,10 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..circle(x: 50, y: 50, color: splashColor)); await gesture.up(); + focusNode.dispose(); }); - testWidgets('ink response uses radius for focus highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response uses radius for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( @@ -449,9 +452,10 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paints..circle(radius: 20, color: const Color(0xff0000ff))); + focusNode.dispose(); }); - testWidgets('InkWell uses borderRadius for focus highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell uses borderRadius for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( @@ -485,9 +489,10 @@ void main() { rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(10)), color: const Color(0xff0000ff), )); + focusNode.dispose(); }); - testWidgets('InkWell uses borderRadius for hover highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell uses borderRadius for hover highlight', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Directionality( @@ -524,7 +529,7 @@ void main() { )); }); - testWidgets('InkWell customBorder clips for focus highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell customBorder clips for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( @@ -576,9 +581,10 @@ void main() { sampleSize: 100, )), ); + focusNode.dispose(); }); - testWidgets('InkWell customBorder clips for hover highlight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell customBorder clips for hover highlight', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Directionality( @@ -631,7 +637,7 @@ void main() { ); }); -testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { +testWidgetsWithLeakTracking('InkResponse radius can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(double radius) { @@ -667,9 +673,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 1)); expect(inkFeatures, paints..circle(radius: 20, color: const Color(0xff0000ff))); + focusNode.dispose(); }); - testWidgets('InkResponse highlightShape can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkResponse highlightShape can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BoxShape shape) { @@ -708,9 +715,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); + focusNode.dispose(); }); - testWidgets('InkWell borderRadius can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell borderRadius can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BorderRadius borderRadius) { @@ -753,9 +761,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(30)), color: const Color(0xff0000ff), )); + focusNode.dispose(); }); - testWidgets('InkWell customBorder can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell customBorder can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BorderRadius borderRadius) { @@ -820,9 +829,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { sampleSize: 100, )), ); + focusNode.dispose(); }); - testWidgets('InkWell splash customBorder can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell splash customBorder can be updated', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/121626. final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BorderRadius borderRadius) { @@ -908,9 +918,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { ); await gesture.up(); + focusNode.dispose(); }); - testWidgets("ink response doesn't change color on focus when on touch device", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ink response doesn't change color on focus when on touch device", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget(Material( @@ -940,9 +951,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); + focusNode.dispose(); }); - testWidgets('InkWell.mouseCursor changes cursor on hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell.mouseCursor changes cursor on hover', (WidgetTester tester) async { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: const Offset(1, 1)); @@ -1029,7 +1041,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('InkResponse containing selectable text changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkResponse containing selectable text changes mouse cursor when hovered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104595. await tester.pumpWidget(MaterialApp( home: SelectionArea( @@ -1061,7 +1073,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { feedback.dispose(); }); - testWidgets('enabled (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('enabled (default)', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, @@ -1089,7 +1101,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(feedback.hapticCount, 1); }); - testWidgets('disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, @@ -1114,7 +1126,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { }); }); - testWidgets('splashing survives scrolling when keep-alive is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('splashing survives scrolling when keep-alive is enabled', (WidgetTester tester) async { Future<void> runTest(bool keepAlive) async { await tester.pumpWidget( MaterialApp( @@ -1155,7 +1167,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await runTest(false); }); - testWidgets('excludeFromSemantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('excludeFromSemantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( @@ -1184,7 +1196,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { semantics.dispose(); }); - testWidgets("ink response doesn't focus when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ink response doesn't focus when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); @@ -1218,9 +1230,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + focusNode.dispose(); }); - testWidgets('ink response accepts focus when disabled in directional navigation mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink response accepts focus when disabled in directional navigation mode', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); @@ -1264,9 +1277,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); + focusNode.dispose(); }); - testWidgets("ink response doesn't hover when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ink response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); @@ -1317,9 +1331,10 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + focusNode.dispose(); }); - testWidgets('When ink wells are nested, only the inner one is triggered by tap splash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When ink wells are nested, only the inner one is triggered by tap splash', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { @@ -1388,7 +1403,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await gesture2.up(); }); - testWidgets('Reparenting parent should allow both inkwells to show splash afterwards', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reparenting parent should allow both inkwells to show splash afterwards', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { @@ -1482,7 +1497,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(material, paintsExactlyCountTimes(#drawCircle, 2)); }); - testWidgets('Parent inkwell does not block child inkwells from splashes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Parent inkwell does not block child inkwells from splashes', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { @@ -1530,7 +1545,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(material, paintsExactlyCountTimes(#drawCircle, 2)); }); - testWidgets('Parent inkwell can count the number of pressed children to prevent splash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Parent inkwell can count the number of pressed children to prevent splash', (WidgetTester tester) async { final GlobalKey parentKey = GlobalKey(); final GlobalKey leftKey = GlobalKey(); final GlobalKey rightKey = GlobalKey(); @@ -1623,7 +1638,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await gesture3.up(); }); - testWidgets('When ink wells are reparented, the old parent can display splash while the new parent can not', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When ink wells are reparented, the old parent can display splash while the new parent can not', (WidgetTester tester) async { final GlobalKey innerKey = GlobalKey(); final GlobalKey leftKey = GlobalKey(); final GlobalKey rightKey = GlobalKey(); @@ -1738,7 +1753,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await gesture2.up(); }); - testWidgets("Ink wells's splash starts before tap is confirmed and disappear after tap is canceled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Ink wells's splash starts before tap is confirmed and disappear after tap is canceled", (WidgetTester tester) async { final GlobalKey innerKey = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1795,7 +1810,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(material, paintsExactlyCountTimes(#drawCircle, 1)); }); - testWidgets('disabled and hovered inkwell responds to mouse-exit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled and hovered inkwell responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; @@ -1858,7 +1873,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(hover, false); }); - testWidgets('hovered ink well draws a transparent highlight when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hovered ink well draws a transparent highlight when disabled', (WidgetTester tester) async { Widget buildFrame({ required bool enabled }) { return Material( child: Directionality( @@ -1908,7 +1923,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { }); - testWidgets('Changing InkWell.enabled should not trigger TextButton setState()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing InkWell.enabled should not trigger TextButton setState()', (WidgetTester tester) async { Widget buildFrame({ required bool enabled }) { return Material( child: Directionality( @@ -1942,7 +1957,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await tester.pumpAndSettle(); }); - testWidgets('InkWell does not attach semantics handler for onTap if it was not provided an onTap handler', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell does not attach semantics handler for onTap if it was not provided an onTap handler', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( @@ -1985,7 +2000,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { )); }); - testWidgets('InkWell highlight should not survive after [onTapDown, onDoubleTap] sequence', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell highlight should not survive after [onTapDown, onDoubleTap] sequence', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -2026,7 +2041,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); }); - testWidgets('InkWell splash should not survive after [onTapDown, onTapDown, onTapCancel, onDoubleTap] sequence', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell splash should not survive after [onTapDown, onTapDown, onTapCancel, onDoubleTap] sequence', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -2071,7 +2086,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); }); - testWidgets('InkWell dispose statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell disposes statesController', (WidgetTester tester) async { int tapCount = 0; Widget buildFrame(MaterialStatesController? statesController) { return MaterialApp( @@ -2088,6 +2103,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); int pressedCount = 0; controller.addListener(() { if (controller.value.contains(MaterialState.pressed)) { @@ -2114,7 +2130,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(pressedCount, 2); }); - testWidgets('ink well overlayColor opacity fades from 0xff when hover ends', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ink well overlayColor opacity fades from 0xff when hover ends', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110266 await tester.pumpWidget(Material( child: Directionality( @@ -2152,7 +2168,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0x8000ff00))); }); - testWidgets('InkWell secondary tap test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell secondary tap test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -2190,7 +2206,7 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { }); // Regression test for https://github.com/flutter/flutter/issues/124328. - testWidgets('InkWell secondary tap should not draw a splash when no secondary callbacks are defined', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkWell secondary tap should not draw a splash when no secondary callbacks are defined', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( @@ -2214,4 +2230,63 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { await gesture.up(); }); + + testWidgetsWithLeakTracking('try out hoverDuration property', (WidgetTester tester) async { + final List<String> log = <String>[]; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: InkWell( + hoverDuration: const Duration(milliseconds: 1000), + onTap: () { + log.add('tap'); + }, + ), + ), + ), + )); + + await tester.tap(find.byType(InkWell), pointer: 1); + await tester.pump(const Duration(seconds: 1)); + + expect(log, equals(<String>['tap'])); + log.clear(); + }); + + testWidgetsWithLeakTracking('InkWell activation action does not end immediately', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132377. + final MaterialStatesController controller = MaterialStatesController(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Shortcuts( + shortcuts: const <ShortcutActivator, Intent>{ + SingleActivator(LogicalKeyboardKey.enter): ButtonActivateIntent(), + }, + child: Material( + child: Center( + child: InkWell( + autofocus: true, + onTap: () {}, + statesController: controller, + ), + ), + ), + ), + )); + + // Invoke the InkWell activation action. + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + + // The InkWell is in pressed state. + await tester.pump(const Duration(milliseconds: 99)); + expect(controller.value.contains(MaterialState.pressed), isTrue); + + await tester.pumpAndSettle(); + expect(controller.value.contains(MaterialState.pressed), isFalse); + + controller.dispose(); + }); } diff --git a/packages/flutter/test/material/input_chip_test.dart b/packages/flutter/test/material/input_chip_test.dart index 827ded808496e..bb43b64cf4661 100644 --- a/packages/flutter/test/material/input_chip_test.dart +++ b/packages/flutter/test/material/input_chip_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; /// Adds the basic requirements for a Chip. Widget wrapForChip({ @@ -101,7 +100,7 @@ void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { } void main() { - testWidgets('InputChip.color resolves material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputChip.color resolves material states', (WidgetTester tester) async { const Color disabledSelectedColor = Color(0xffffff00); const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); @@ -157,7 +156,7 @@ void main() { expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); }); - testWidgets('InputChip uses provided state color properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputChip uses provided state color properties', (WidgetTester tester) async { const Color disabledColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff0000ff); const Color selectedColor = Color(0xffff0000); @@ -196,7 +195,7 @@ void main() { expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); }); - testWidgets('InputChip can be tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -211,7 +210,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('loses focus when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('loses focus when disabled', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'InputChip'); await tester.pumpWidget( wrapForChip( @@ -241,9 +240,11 @@ void main() { ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isFalse); + + focusNode.dispose(); }); - testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cannot be traversed to when disabled', (WidgetTester tester) async { final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1'); final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2'); await tester.pumpWidget( @@ -271,14 +272,17 @@ void main() { expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); - expect(focusNode1.nextFocus(), isTrue); + expect(focusNode1.nextFocus(), isFalse); await tester.pump(); expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); + + focusNode1.dispose(); + focusNode2.dispose(); }); - testWidgets('Input chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedInputChip(), @@ -290,7 +294,7 @@ void main() { ); }); - testWidgets('Input chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedInputChip(), @@ -303,7 +307,7 @@ void main() { ); }); - testWidgets('Input chip check mark color can be set by the chip theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip check mark color can be set by the chip theme', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedInputChip(), @@ -316,7 +320,7 @@ void main() { ); }); - testWidgets('Input chip check mark color can be set by the chip constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip check mark color can be set by the chip constructor', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedInputChip(checkmarkColor: const Color(0xff00ff00)), @@ -328,7 +332,7 @@ void main() { ); }); - testWidgets('Input chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { await pumpCheckmarkChip( tester, chip: selectedInputChip(checkmarkColor: const Color(0xffff0000)), @@ -341,7 +345,7 @@ void main() { ); }); - testWidgets('InputChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { const Text label = Text('label'); await tester.pumpWidget(wrapForChip(child: const InputChip(label: label))); checkChipMaterialClipBehavior(tester, Clip.none); @@ -350,7 +354,7 @@ void main() { checkChipMaterialClipBehavior(tester, Clip.antiAlias); }); - testWidgets('Input chip has correct selected color when enabled - M3 defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip has correct selected color when enabled - M3 defaults', (WidgetTester tester) async { final ChipThemeData material3ChipDefaults = ThemeData(useMaterial3: true).chipTheme; await pumpCheckmarkChip( tester, @@ -362,7 +366,7 @@ void main() { expect(materialBox, paints..rrect(color: material3ChipDefaults.backgroundColor)); }); - testWidgets('Input chip has correct selected color when disabled - M3 defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Input chip has correct selected color when disabled - M3 defaults', (WidgetTester tester) async { final ChipThemeData material3ChipDefaults = ThemeData(useMaterial3: true).chipTheme; await pumpCheckmarkChip( tester, diff --git a/packages/flutter/test/material/input_date_picker_form_field_test.dart b/packages/flutter/test/material/input_date_picker_form_field_test.dart index 076b3978f41a0..4e09c8f7da0f2 100644 --- a/packages/flutter/test/material/input_date_picker_form_field_test.dart +++ b/packages/flutter/test/material/input_date_picker_form_field_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; @@ -97,7 +98,7 @@ void main() { group('InputDatePickerFormField', () { - testWidgets('Initial date is the default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial date is the default', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final DateTime initialDate = DateTime(2016, DateTime.february, 21); DateTime? inputDate; @@ -111,7 +112,7 @@ void main() { expect(inputDate, equals(initialDate)); }); - testWidgets('Changing initial date is reflected in text value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing initial date is reflected in text value', (WidgetTester tester) async { final DateTime initialDate = DateTime(2016, DateTime.february, 21); final DateTime updatedInitialDate = DateTime(2016, DateTime.february, 23); await tester.pumpWidget(inputDatePickerField( @@ -126,7 +127,7 @@ void main() { expect(textFieldController(tester).value.text, equals('02/23/2016')); }); - testWidgets('Valid date entry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Valid date entry', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); DateTime? inputDate; await tester.pumpWidget(inputDatePickerField( @@ -139,7 +140,7 @@ void main() { expect(inputDate, equals(DateTime(2016, DateTime.february, 21))); }); - testWidgets('Invalid text entry shows errorFormat text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Invalid text entry shows errorFormat text', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); DateTime? inputDate; await tester.pumpWidget(inputDatePickerField( @@ -166,7 +167,7 @@ void main() { expect(find.text('That is not a date.'), findsOneWidget); }); - testWidgets('Valid text entry, but date outside first or last date shows bounds shows errorInvalid text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Valid text entry, but date outside first or last date shows bounds shows errorInvalid text', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); DateTime? inputDate; await tester.pumpWidget(inputDatePickerField( @@ -201,7 +202,7 @@ void main() { expect(find.text('Not in given range.'), findsOneWidget); }); - testWidgets('selectableDatePredicate will be used to show errorInvalid if date is not selectable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selectableDatePredicate will be used to show errorInvalid if date is not selectable', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); DateTime? inputDate; await tester.pumpWidget(inputDatePickerField( @@ -227,7 +228,7 @@ void main() { expect(find.text('Out of range.'), findsNothing); }); - testWidgets('Empty field shows hint text when focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty field shows hint text when focused', (WidgetTester tester) async { await tester.pumpWidget(inputDatePickerField()); // Focus on it await tester.tap(find.byType(TextField)); @@ -250,7 +251,7 @@ void main() { expect(textOpacity(tester, 'Enter some date'), equals(0.0)); }); - testWidgets('Label text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Label text', (WidgetTester tester) async { await tester.pumpWidget(inputDatePickerField()); // Default label expect(find.text('Enter Date'), findsOneWidget); @@ -262,7 +263,7 @@ void main() { expect(find.text('Give me a date!'), findsOneWidget); }); - testWidgets('Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); // Fill the clipboard so that the Paste option is available in the text @@ -291,7 +292,7 @@ void main() { semantics.dispose(); }); - testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme is honored', (WidgetTester tester) async { const InputBorder border = InputBorder.none; await tester.pumpWidget(inputDatePickerField( theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith( @@ -325,7 +326,7 @@ void main() { expect(containerColor, equals(Colors.transparent)); }); - testWidgets('Date text localization', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Date text localization', (WidgetTester tester) async { final Iterable<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[ TestMaterialLocalizationsDelegate(), DefaultWidgetsLocalizations.delegate, @@ -348,7 +349,7 @@ void main() { ); }); - testWidgets('when an empty date is entered and acceptEmptyDate is true, then errorFormatText is not shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when an empty date is entered and acceptEmptyDate is true, then errorFormatText is not shown', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); const String errorFormatText = 'That is not a date.'; await tester.pumpWidget(inputDatePickerField( @@ -363,7 +364,7 @@ void main() { expect(find.text(errorFormatText), findsNothing); }); - testWidgets('when an empty date is entered and acceptEmptyDate is false, then errorFormatText is shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when an empty date is entered and acceptEmptyDate is false, then errorFormatText is shown', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); const String errorFormatText = 'That is not a date.'; await tester.pumpWidget(inputDatePickerField( diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 775505396767e..eed10b5bc06d4 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -14,8 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Widget buildInputDecorator({ InputDecoration decoration = const InputDecoration(), @@ -158,8 +157,12 @@ TextStyle? getIconStyle(WidgetTester tester, IconData icon) { } void main() { - for (final bool useMaterial3 in <bool>[true, false]){ - testWidgets('InputDecorator input/label text layout', (WidgetTester tester) async { + runAllTests(useMaterial3: true); + runAllTests(useMaterial3: false); +} + +void runAllTests({ required bool useMaterial3 }) { + testWidgetsWithLeakTracking('InputDecorator input/label text layout', (WidgetTester tester) async { // The label appears above the input text await tester.pumpWidget( buildInputDecorator( @@ -387,7 +390,7 @@ void main() { } }); - testWidgets('InputDecorator input/label widget layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator input/label widget layout', (WidgetTester tester) async { const Key key = Key('l'); // The label appears above the input text. @@ -731,7 +734,7 @@ void main() { }); - testWidgets('InputDecorator floating label animation duration and curve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator floating label animation duration and curve', (WidgetTester tester) async { Future<void> pumpInputDecorator({ required bool isFocused, }) async { @@ -782,10 +785,12 @@ void main() { group('alignLabelWithHint', () { group('expands false', () { - testWidgets('multiline TextField no-strut', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline TextField no-strut', (WidgetTester tester) async { const String text = 'text'; final FocusNode focusNode = FocusNode(); final TextEditingController controller = TextEditingController(); + addTearDown(() { focusNode.dispose(); controller.dispose();}); + Widget buildFrame(bool alignLabelWithHint) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -833,10 +838,11 @@ void main() { focusNode.unfocus(); }); - testWidgets('multiline TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline TextField', (WidgetTester tester) async { const String text = 'text'; final FocusNode focusNode = FocusNode(); final TextEditingController controller = TextEditingController(); + addTearDown(() { focusNode.dispose(); controller.dispose();}); Widget buildFrame(bool alignLabelWithHint) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -885,10 +891,13 @@ void main() { }); group('expands true', () { - testWidgets('multiline TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline TextField', (WidgetTester tester) async { const String text = 'text'; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + Widget buildFrame(bool alignLabelWithHint) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -937,10 +946,13 @@ void main() { focusNode.unfocus(); }); - testWidgets('multiline TextField with outline border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline TextField with outline border', (WidgetTester tester) async { const String text = 'text'; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + Widget buildFrame(bool alignLabelWithHint) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -999,7 +1011,7 @@ void main() { // 12 - top padding // 16 - input text (font size 16dps) // 12 - bottom padding - testWidgets('InputDecorator input/hint layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator input/hint layout', (WidgetTester tester) async { // The hint aligns with the input text await tester.pumpWidget( buildInputDecorator( @@ -1023,7 +1035,7 @@ void main() { expect(tester.getSize(find.text('hint')).width, tester.getSize(find.text('text')).width); }); - testWidgets('InputDecorator input/label/hint layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator input/label/hint layout', (WidgetTester tester) async { // Label is visible, hint is not (opacity 0.0). await tester.pumpWidget( buildInputDecorator( @@ -1077,14 +1089,14 @@ void main() { ); // The hint's opacity animates from 0.0 to 1.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(hintOpacity9ms, 1.0)); } await tester.pumpAndSettle(); @@ -1111,14 +1123,14 @@ void main() { ); // The hint's opacity animates from 1.0 to 0.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(0.0, hintOpacity9ms)); } await tester.pumpAndSettle(); @@ -1134,7 +1146,7 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('InputDecorator input/label/hint dense layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator input/label/hint dense layout', (WidgetTester tester) async { // Label is visible, hint is not (opacity 0.0). await tester.pumpWidget( buildInputDecorator( @@ -1198,7 +1210,220 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('InputDecorator with no input border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator default hint animation duration', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint is not visible (opacity 0.0). + expect(getOpacity(tester, 'hint'), 0.0); + + // Focus to show the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + isFocused: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint's opacity animates from 0.0 to 1.0. + // The animation's default duration is 20ms. + { + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(hintOpacity9ms, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + expect(getOpacity(tester, 'hint'), 1.0); + } + + // Unfocus to hide the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint's opacity animates from 1.0 to 0.0. + // The animation's default duration is 20ms. + { + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(0.0, hintOpacity9ms)); + await tester.pump(const Duration(milliseconds: 9)); + expect(getOpacity(tester, 'hint'), 0.0); + } + }); + + testWidgetsWithLeakTracking('InputDecorator custom hint animation duration', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + hintFadeDuration: Duration(milliseconds: 120), + ), + ), + ); + + // The hint is not visible (opacity 0.0). + expect(getOpacity(tester, 'hint'), 0.0); + + // Focus to show the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + isFocused: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + hintFadeDuration: Duration(milliseconds: 120), + ), + ), + ); + + // The hint's opacity animates from 0.0 to 1.0. + // The animation's duration is set to 120ms. + { + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity50ms = getOpacity(tester, 'hint'); + expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity100ms = getOpacity(tester, 'hint'); + expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + expect(getOpacity(tester, 'hint'), 1.0); + } + + // Unfocus to hide the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + hintFadeDuration: Duration(milliseconds: 120), + ), + ), + ); + + // The hint's opacity animates from 1.0 to 0.0. + // The animation's default duration is 20ms. + { + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity50ms = getOpacity(tester, 'hint'); + expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity100ms = getOpacity(tester, 'hint'); + expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); + await tester.pump(const Duration(milliseconds: 50)); + expect(getOpacity(tester, 'hint'), 0.0); + } + }); + + testWidgetsWithLeakTracking('InputDecorator custom hint animation duration from theme', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + inputDecorationTheme: const InputDecorationTheme( + hintFadeDuration: Duration(milliseconds: 120), + ), + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint is not visible (opacity 0.0). + expect(getOpacity(tester, 'hint'), 0.0); + + // Focus to show the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + inputDecorationTheme: const InputDecorationTheme( + hintFadeDuration: Duration(milliseconds: 120), + ), + isEmpty: true, + isFocused: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint's opacity animates from 0.0 to 1.0. + // The animation's duration is set to 120ms. + { + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity50ms = getOpacity(tester, 'hint'); + expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity100ms = getOpacity(tester, 'hint'); + expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + expect(getOpacity(tester, 'hint'), 1.0); + } + + // Unfocus to hide the hint. + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + inputDecorationTheme: const InputDecorationTheme( + hintFadeDuration: Duration(milliseconds: 120), + ), + isEmpty: true, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + ), + ), + ); + + // The hint's opacity animates from 1.0 to 0.0. + // The animation's duration is set to 160ms. + { + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity50ms = getOpacity(tester, 'hint'); + expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 50)); + final double hintOpacity100ms = getOpacity(tester, 'hint'); + expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); + await tester.pump(const Duration(milliseconds: 50)); + expect(getOpacity(tester, 'hint'), 0.0); + } + }); + + testWidgetsWithLeakTracking('InputDecorator with no input border', (WidgetTester tester) async { // Label is visible, hint is not (opacity 0.0). await tester.pumpWidget( buildInputDecorator( @@ -1213,7 +1438,7 @@ void main() { expect(getBorderWeight(tester), 0.0); }); - testWidgets('InputDecorator error/helper/counter layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator error/helper/counter layout', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -1365,7 +1590,7 @@ void main() { expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 56.0)); }); - testWidgets('InputDecorator counter text, widget, and null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator counter text, widget, and null', (WidgetTester tester) async { Widget buildFrame({ InputCounterWidgetBuilder? buildCounter, String? counterText, @@ -1467,7 +1692,7 @@ void main() { expect(find.byKey(buildCounterKey), findsOneWidget); }); - testWidgets('InputDecoration errorMaxLines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration errorMaxLines', (WidgetTester tester) async { const String kError1 = 'e0'; const String kError2 = 'e0\ne1'; const String kError3 = 'e0\ne1\ne2'; @@ -1546,7 +1771,7 @@ void main() { expect(tester.getBottomLeft(find.text(kError1)), const Offset(12.0, 76.0)); }); - testWidgets('InputDecoration helperMaxLines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration helperMaxLines', (WidgetTester tester) async { const String kHelper1 = 'e0'; const String kHelper2 = 'e0\ne1'; const String kHelper3 = 'e0\ne1\ne2'; @@ -1643,7 +1868,7 @@ void main() { expect(tester.getBottomLeft(find.text(kHelper1)), const Offset(12.0, 76.0)); }); - testWidgets('InputDecorator shows error text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator shows error text', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -1656,7 +1881,106 @@ void main() { expect(find.text('errorText'), findsOneWidget); }); - testWidgets('InputDecorator shows error widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration shows error border for errorText and error widget', (WidgetTester tester) async { + const InputBorder errorBorder = OutlineInputBorder( + borderSide: BorderSide(color: Colors.red, width: 1.5), + ); + const InputBorder focusedErrorBorder = OutlineInputBorder( + borderSide: BorderSide(color: Colors.teal, width: 5.0), + ); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isFocused: true, + decoration: const InputDecoration( + errorText: 'error', + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), focusedErrorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + errorText: 'error', + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + errorText: 'error', + enabled: false, + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + isFocused: true, + decoration: const InputDecoration( + error: Text('error'), + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), focusedErrorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + error: Text('error'), + // enabled: true (default) + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + // isFocused: false (default) + decoration: const InputDecoration( + error: Text('error'), + enabled: false, + errorBorder: errorBorder, + focusedErrorBorder: focusedErrorBorder, + ), + ), + ); + await tester.pumpAndSettle(); // Border changes are animated. + expect(getBorder(tester), errorBorder); + }); + + testWidgetsWithLeakTracking('InputDecorator shows error widget', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -1669,7 +1993,7 @@ void main() { expect(find.text('error'), findsOneWidget); }); - testWidgets('InputDecorator throws when error text and error widget are provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator throws when error text and error widget are provided', (WidgetTester tester) async { expect( () { buildInputDecorator( @@ -1684,7 +2008,7 @@ void main() { ); }); - testWidgets('InputDecorator prefix/suffix texts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefix/suffix texts', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -1722,7 +2046,7 @@ void main() { expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('s')).dx)); }); - testWidgets('InputDecorator icon/prefix/suffix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator icon/prefix/suffix', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -1762,7 +2086,7 @@ void main() { expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('s')).dx)); }); - testWidgets('InputDecorator iconColor/prefixIconColor/suffixIconColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator iconColor/prefixIconColor/suffixIconColor', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -1786,7 +2110,7 @@ void main() { expect(tester.widget<IconTheme>(find.widgetWithIcon(IconTheme,Icons.close).first).data.color, Colors.red); }); - testWidgets('InputDecorator suffixIconColor in M3 error state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator suffixIconColor in M3 error state', (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: true, iconButtonTheme: const IconButtonThemeData( @@ -1796,7 +2120,7 @@ void main() { ), ); await tester.pumpWidget( - MaterialApp( + MaterialApp( theme: theme, home: Material( child: TextField( @@ -1813,8 +2137,9 @@ void main() { expect(getIconStyle(tester, Icons.close)?.color, theme.colorScheme.error); }); - testWidgets('InputDecoration default floatingLabelStyle resolves hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration default floatingLabelStyle resolves hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -1847,7 +2172,7 @@ void main() { expect(getLabelStyle(tester).color, theme.colorScheme.onSurfaceVariant); }); - testWidgets('InputDecorator prefix/suffix widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefix/suffix widgets', (WidgetTester tester) async { const Key pKey = Key('p'); const Key sKey = Key('s'); await tester.pumpWidget( @@ -1899,7 +2224,7 @@ void main() { expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopRight(find.byKey(sKey)).dx)); }); - testWidgets('InputDecorator tall prefix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix', (WidgetTester tester) async { const Key pKey = Key('p'); await tester.pumpWidget( buildInputDecorator( @@ -1944,7 +2269,7 @@ void main() { expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx); }); - testWidgets('InputDecorator tall prefix with border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix with border', (WidgetTester tester) async { const Key pKey = Key('p'); await tester.pumpWidget( buildInputDecorator( @@ -1995,7 +2320,7 @@ void main() { expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx); }); - testWidgets('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2011,9 +2336,9 @@ void main() { // Overall height for this InputDecorator is 48dps because the prefix icon's minimum size // is 48x48 and the rest of the elements only require 40dps: - // 12 - top padding - // 16 - input text (font size 16dps) - // 12 - bottom padding + // 12 - top padding + // 16 - input text (font size 16dps) + // 12 - bottom padding expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0)); expect(tester.getSize(find.text('text')).height, 16.0); @@ -2031,7 +2356,7 @@ void main() { expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.byIcon(Icons.satellite)).dx)); }); - testWidgets('InputDecorator prefixIconConstraints/suffixIconConstraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefixIconConstraints/suffixIconConstraints', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2069,7 +2394,7 @@ void main() { expect(tester.getTopRight(find.byIcon(Icons.satellite)).dx, 800.0); }); - testWidgets('prefix/suffix icons are centered when smaller than 48 by 48', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prefix/suffix icons are centered when smaller than 48 by 48', (WidgetTester tester) async { const Key prefixKey = Key('prefix'); await tester.pumpWidget( buildInputDecorator( @@ -2085,17 +2410,17 @@ void main() { ); // Overall height for this InputDecorator is 48dps because the prefix icon's minimum size - // is 48x48 and the rest of the elements only require 40dps: - // 12 - top padding - // 16 - input text (font size 16dps) - // 12 - bottom padding + // is 48x48 and the rest of the elements only require 40dps: + // 12 - top padding + // 16 - input text (font size 16dps) + // 12 - bottom padding expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 48.0)); expect(tester.getSize(find.byKey(prefixKey)).height, 16.0); expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 16.0); }); - testWidgets('InputDecorator respects reduced theme visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator respects reduced theme visualDensity', (WidgetTester tester) async { // Label is visible, hint is not (opacity 0.0). await tester.pumpWidget( buildInputDecorator( @@ -2134,14 +2459,14 @@ void main() { ); // The hint's opacity animates from 0.0 to 1.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(hintOpacity9ms, 1.0)); } await tester.pumpAndSettle(); @@ -2169,14 +2494,14 @@ void main() { ); // The hint's opacity animates from 1.0 to 0.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(0.0, hintOpacity9ms)); } await tester.pumpAndSettle(); @@ -2192,7 +2517,7 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('InputDecorator respects increased theme visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator respects increased theme visualDensity', (WidgetTester tester) async { // Label is visible, hint is not (opacity 0.0). await tester.pumpWidget( buildInputDecorator( @@ -2231,14 +2556,14 @@ void main() { ); // The hint's opacity animates from 0.0 to 1.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(hintOpacity50ms, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(hintOpacity9ms, 1.0)); } await tester.pumpAndSettle(); @@ -2266,14 +2591,14 @@ void main() { ); // The hint's opacity animates from 1.0 to 0.0. - // The animation's duration is 167ms. + // The animation's default duration is 20ms. { - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity50ms = getOpacity(tester, 'hint'); - expect(hintOpacity50ms, inExclusiveRange(0.0, 1.0)); - await tester.pump(const Duration(milliseconds: 50)); - final double hintOpacity100ms = getOpacity(tester, 'hint'); - expect(hintOpacity100ms, inExclusiveRange(0.0, hintOpacity50ms)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity9ms = getOpacity(tester, 'hint'); + expect(hintOpacity9ms, inExclusiveRange(0.0, 1.0)); + await tester.pump(const Duration(milliseconds: 9)); + final double hintOpacity18ms = getOpacity(tester, 'hint'); + expect(hintOpacity18ms, inExclusiveRange(0.0, hintOpacity9ms)); } await tester.pumpAndSettle(); @@ -2289,7 +2614,7 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('prefix/suffix icons increase height of decoration when larger than 48 by 48', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prefix/suffix icons increase height of decoration when larger than 48 by 48', (WidgetTester tester) async { const Key prefixKey = Key('prefix'); await tester.pumpWidget( buildInputDecorator( @@ -2303,9 +2628,9 @@ void main() { // Overall height for this InputDecorator is 100dps because the prefix icon's size // is 100x100 and the rest of the elements only require 40dps: - // 12 - top padding - // 16 - input text (font size 16dps) - // 12 - bottom padding + // 12 - top padding + // 16 - input text (font size 16dps) + // 12 - bottom padding expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 100.0)); expect(tester.getSize(find.byKey(prefixKey)).height, 100.0); @@ -2313,7 +2638,7 @@ void main() { }); group('constraints', () { - testWidgets('No InputDecorator constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No InputDecorator constraints', (WidgetTester tester) async { await tester.pumpWidget(buildInputDecorator( useMaterial3: useMaterial3, )); @@ -2322,7 +2647,7 @@ void main() { expect(tester.getSize(find.byType(InputDecorator)), const Size(800, 48)); }); - testWidgets('InputDecoratorThemeData constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoratorThemeData constraints', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2338,7 +2663,7 @@ void main() { expect(tester.getSize(find.byType(InputDecorator)), const Size(300, 40)); }); - testWidgets('InputDecorator constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator constraints', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2361,7 +2686,7 @@ void main() { group('textAlignVertical position', () { group('simple case', () { - testWidgets('align top (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align top (default)', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2385,7 +2710,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 12.0); }); - testWidgets('align center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align center', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2409,7 +2734,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 290.0); }); - testWidgets('align bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align bottom', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2433,7 +2758,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 568.0); }); - testWidgets('align as a double', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align as a double', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2457,7 +2782,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 498.5); }); - testWidgets('works with density and content padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with density and content padding', (WidgetTester tester) async { const Key key = Key('child'); const Key containerKey = Key('container'); const double totalHeight = 100.0; @@ -2507,7 +2832,7 @@ void main() { }); group('outline border', () { - testWidgets('align top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align top', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2533,7 +2858,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 24.0); }); - testWidgets('align center (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align center (default)', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2558,7 +2883,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 289.0); }); - testWidgets('align bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align bottom', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2585,7 +2910,7 @@ void main() { }); group('prefix', () { - testWidgets('InputDecorator tall prefix align top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix align top', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2615,7 +2940,7 @@ void main() { expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0); }); - testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix align center', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2645,7 +2970,7 @@ void main() { expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0); }); - testWidgets('InputDecorator tall prefix align bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix align bottom', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2677,7 +3002,7 @@ void main() { }); group('outline border and prefix', () { - testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix align center', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2709,7 +3034,7 @@ void main() { expect(tester.getTopLeft(find.byKey(pKey)).dy, 246.5); }); - testWidgets('InputDecorator tall prefix with border align top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix with border align top', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2743,7 +3068,7 @@ void main() { expect(tester.getTopLeft(find.byKey(pKey)).dy, 24.0); }); - testWidgets('InputDecorator tall prefix with border align bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix with border align bottom', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2775,7 +3100,7 @@ void main() { expect(tester.getTopLeft(find.byKey(pKey)).dy, 479.0); }); - testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator tall prefix with border align double', (WidgetTester tester) async { const Key pKey = Key('p'); const String text = 'text'; await tester.pumpWidget( @@ -2809,7 +3134,7 @@ void main() { }); group('label', () { - testWidgets('align top (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align top (default)', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2835,7 +3160,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 28.0); }); - testWidgets('align center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align center', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2861,7 +3186,7 @@ void main() { expect(tester.getTopLeft(find.text(text)).dy, 298.0); }); - testWidgets('align bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('align bottom', (WidgetTester tester) async { const String text = 'text'; await tester.pumpWidget( buildInputDecorator( @@ -2891,7 +3216,7 @@ void main() { group('OutlineInputBorder', () { group('default alignment', () { - testWidgets('Centers when border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centers when border', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2908,7 +3233,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('Centers when border and label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centers when border and label', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2926,7 +3251,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('Centers when border and contentPadding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centers when border and contentPadding', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2947,7 +3272,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('Centers when border and contentPadding and label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centers when border and contentPadding and label', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2968,7 +3293,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('Centers when border and lopsided contentPadding and label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centers when border and lopsided contentPadding and label', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -2990,7 +3315,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('Floating label is aligned with prefixIcon by default in M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating label is aligned with prefixIcon by default in M3', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3009,7 +3334,7 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('Floating label for filled input decoration is aligned with text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating label for filled input decoration is aligned with text', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3030,7 +3355,7 @@ void main() { }); group('3 point interpolation alignment', () { - testWidgets('top align includes padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('top align includes padding', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3054,7 +3379,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('center align ignores padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center align ignores padding', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3078,7 +3403,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('bottom align includes padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bottom align includes padding', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3102,7 +3427,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('padding exceeds middle keeps top at middle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('padding exceeds middle keeps top at middle', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3128,7 +3453,7 @@ void main() { }); }); - testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async { + testWidgetsWithLeakTracking('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3148,7 +3473,7 @@ void main() { expect(tester.getRect(find.text('test')).right, dx - 12.0); }); - testWidgets('counter text has correct right margin - RTL, not dense', (WidgetTester tester) async { + testWidgetsWithLeakTracking('counter text has correct right margin - RTL, not dense', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3168,7 +3493,7 @@ void main() { expect(tester.getRect(find.text('test')).left, 12.0); }); - testWidgets('counter text has correct right margin - LTR, dense', (WidgetTester tester) async { + testWidgetsWithLeakTracking('counter text has correct right margin - LTR, dense', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3189,7 +3514,7 @@ void main() { expect(tester.getRect(find.text('test')).right, dx - 12.0); }); - testWidgets('counter text has correct right margin - RTL, dense', (WidgetTester tester) async { + testWidgetsWithLeakTracking('counter text has correct right margin - RTL, dense', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3210,7 +3535,7 @@ void main() { expect(tester.getRect(find.text('test')).left, 12.0); }); - testWidgets('InputDecorator error/helper/counter RTL layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator error/helper/counter RTL layout', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3267,7 +3592,7 @@ void main() { expect(find.text('helper'), findsNothing); }); - testWidgets('InputDecorator prefix/suffix RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefix/suffix RTL', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3301,7 +3626,7 @@ void main() { expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopLeft(find.text('p')).dx)); }); - testWidgets('InputDecorator contentPadding RTL layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator contentPadding RTL layout', (WidgetTester tester) async { // LTR: content left edge is contentPadding.start: 40.0 await tester.pumpWidget( buildInputDecorator( @@ -3342,13 +3667,13 @@ void main() { expect(tester.getTopRight(find.text('hint')).dx, 760.0); }); - testWidgets('FloatingLabelAlignment.toString()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingLabelAlignment.toString()', (WidgetTester tester) async { expect(FloatingLabelAlignment.start.toString(), 'FloatingLabelAlignment.start'); expect(FloatingLabelAlignment.center.toString(), 'FloatingLabelAlignment.center'); }); group('inputText width', () { - testWidgets('outline textField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outline textField', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3361,7 +3686,7 @@ void main() { expect(tester.getTopLeft(find.text('text')).dx, 12.0); expect(tester.getTopRight(find.text('text')).dx, 788.0); }); - testWidgets('outline textField with prefix and suffix icons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outline textField with prefix and suffix icons', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3376,7 +3701,7 @@ void main() { expect(tester.getTopLeft(find.text('text')).dx, 48.0); expect(tester.getTopRight(find.text('text')).dx, 752.0); }); - testWidgets('filled textField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('filled textField', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3389,7 +3714,7 @@ void main() { expect(tester.getTopLeft(find.text('text')).dx, 12.0); expect(tester.getTopRight(find.text('text')).dx, 788.0); }); - testWidgets('filled textField with prefix and suffix icons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('filled textField with prefix and suffix icons', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3428,7 +3753,7 @@ void main() { ); group('LTR with icon aligned', () { - testWidgets('start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('start', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.ltr, @@ -3452,7 +3777,7 @@ void main() { expect(tester.getTopLeft(find.text('label')).dx, 80.0); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.ltr, @@ -3478,7 +3803,7 @@ void main() { }); group('LTR without icon aligned', () { - testWidgets('start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('start', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.ltr, @@ -3502,7 +3827,7 @@ void main() { expect(tester.getTopLeft(find.text('label')).dx, 40.0); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.ltr, @@ -3528,7 +3853,7 @@ void main() { }); group('RTL with icon aligned', () { - testWidgets('start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('start', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.rtl, @@ -3552,7 +3877,7 @@ void main() { expect(tester.getTopRight(find.text('label')).dx, 720.0); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.rtl, @@ -3578,7 +3903,7 @@ void main() { }); group('RTL without icon aligned', () { - testWidgets('start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('start', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.rtl, @@ -3602,7 +3927,7 @@ void main() { expect(tester.getTopRight(find.text('label')).dx, 760.0); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecoratorWithFloatingLabel( textDirection: TextDirection.rtl, @@ -3628,7 +3953,7 @@ void main() { }); }); - testWidgets('InputDecorator prefix/suffix dense layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator prefix/suffix dense layout', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3670,7 +3995,7 @@ void main() { expect(getBorderWeight(tester), 2.0); }); - testWidgets('InputDecorator with empty InputDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator with empty InputDecoration', (WidgetTester tester) async { await tester.pumpWidget(buildInputDecorator( useMaterial3: useMaterial3, )); @@ -3687,7 +4012,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('contentPadding smaller than kMinInteractiveDimension', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contentPadding smaller than kMinInteractiveDimension', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/42449 const double verticalPadding = 1.0; await tester.pumpWidget( @@ -3763,7 +4088,7 @@ void main() { expect(getBorderWeight(tester), 0.0); }); - testWidgets('InputDecorator.collapsed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator.collapsed', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3806,7 +4131,7 @@ void main() { expect(getBorderWeight(tester), 0.0); }); - testWidgets('InputDecorator with baseStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator with baseStyle', (WidgetTester tester) async { // Setting the baseStyle of the InputDecoration and the style of the input // text child to a smaller font reduces the InputDecoration's vertical size. const TextStyle style = TextStyle(fontSize: 10.0); @@ -3847,7 +4172,7 @@ void main() { expect(tester.getTopLeft(find.text('text')).dy, useMaterial3 ? 28 : 24.75); }); - testWidgets('InputDecorator with empty style overrides', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator with empty style overrides', (WidgetTester tester) async { // Same as not specifying any style overrides await tester.pumpWidget( buildInputDecorator( @@ -3890,7 +4215,7 @@ void main() { expect(tester.getTopRight(find.text('counter')), const Offset(788.0, 64.0)); }); - testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -3915,7 +4240,7 @@ void main() { expect(getBorderWeight(tester), 0.0); }); - testWidgets('InputDecoration outline shape with no border and no floating placeholder not empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration outline shape with no border and no floating placeholder not empty', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -4067,7 +4392,7 @@ void main() { expect(merged.constraints, overrideTheme.constraints); }); - testWidgets('InputDecorationTheme outline border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme outline border', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -4093,7 +4418,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('InputDecorationTheme outline border, dense layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme outline border, dense layout', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -4121,7 +4446,7 @@ void main() { expect(getBorderWeight(tester), 1.0); }); - testWidgets('InputDecorationTheme style overrides', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme style overrides', (WidgetTester tester) async { const TextStyle defaultStyle = TextStyle(fontSize: 16.0); final TextStyle labelStyle = defaultStyle.merge(const TextStyle(color: Colors.red)); final TextStyle hintStyle = defaultStyle.merge(const TextStyle(color: Colors.green)); @@ -4187,7 +4512,7 @@ void main() { expect(getLabelStyle(tester).color, labelStyle.color); }); - testWidgets('InputDecorationTheme style overrides (focused)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme style overrides (focused)', (WidgetTester tester) async { const TextStyle defaultStyle = TextStyle(fontSize: 16.0); final TextStyle labelStyle = defaultStyle.merge(const TextStyle(color: Colors.red)); final TextStyle floatingLabelStyle = defaultStyle.merge(const TextStyle(color: Colors.indigo)); @@ -4255,7 +4580,7 @@ void main() { expect(getLabelStyle(tester).color, floatingLabelStyle.color); }); - testWidgets('InputDecorator.toString()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator.toString()', (WidgetTester tester) async { const Widget child = InputDecorator( key: Key('key'), decoration: InputDecoration(), @@ -4269,7 +4594,7 @@ void main() { ); }); - testWidgets('InputDecorator.debugDescribeChildren', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator.debugDescribeChildren', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -4311,7 +4636,7 @@ void main() { expect(nodeValues.length, 11); }); - testWidgets('InputDecorator with empty border and label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator with empty border and label', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/14165 await tester.pumpWidget( buildInputDecorator( @@ -4331,7 +4656,7 @@ void main() { expect(tester.getBottomLeft(find.text('label')).dy, 24.0); }); - testWidgets('InputDecorationTheme.inputDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme.inputDecoration', (WidgetTester tester) async { const TextStyle themeStyle = TextStyle(color: Color(0xFF00FFFF)); const Color themeColor = Color(0xFF00FF00); const InputBorder themeInputBorder = OutlineInputBorder( @@ -4508,7 +4833,7 @@ void main() { expect(decoration.constraints, const BoxConstraints(minWidth: 40, maxWidth: 50, minHeight: 60, maxHeight: 70)); }); - testWidgets('InputDecorationTheme.inputDecoration with MaterialState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme.inputDecoration with MaterialState', (WidgetTester tester) async { final MaterialStateTextStyle themeStyle = MaterialStateTextStyle.resolveWith((Set<MaterialState> states) { return const TextStyle(color: Colors.green); }); @@ -4612,7 +4937,7 @@ void main() { expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); }); - testWidgets('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/15742 await tester.pumpWidget( @@ -4653,7 +4978,7 @@ void main() { )); }); - testWidgets('InputDecorator UnderlineInputBorder fillColor is clipped by border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator UnderlineInputBorder fillColor is clipped by border', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -4686,7 +5011,7 @@ void main() { )); }); - testWidgets('InputDecorator constrained to 0x0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator constrained to 0x0', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17710 await tester.pumpWidget( Material( @@ -4706,7 +5031,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'InputDecorator OutlineBorder focused label with icon', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/82321 @@ -4754,7 +5079,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'InputDecorator OutlineBorder focused label with icon', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/18111 @@ -4800,7 +5125,7 @@ void main() { }, ); - testWidgets('InputDecorator draws and animates hoverColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator draws and animates hoverColor', (WidgetTester tester) async { final Color fillColor = useMaterial3 ? const Color(0xffffffff) : const Color(0x0A000000); const Color hoverColor = Color(0xFF00FF00); final Color disabledColor =useMaterial3 ? const Color(0x0A000000) : const Color(0x05000000); @@ -4881,7 +5206,7 @@ void main() { expect(getBorderColor(tester), equals(disabledColor)); }); - testWidgets('InputDecorator draws and animates focusColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator draws and animates focusColor', (WidgetTester tester) async { const Color focusColor = Color(0xFF0000FF); const Color disabledColor = Color(0x05000000); final Color enabledBorderColor = useMaterial3 ? const Color(0xffffffff) : const Color(0x61000000); @@ -4935,7 +5260,7 @@ void main() { expect(getBorderColor(tester), equals(disabledColor)); }); - testWidgets('InputDecorator withdraws label when not empty or focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator withdraws label when not empty or focused', (WidgetTester tester) async { Future<void> pumpDecorator({ required bool focused, bool enabled = true, @@ -5003,7 +5328,7 @@ void main() { expect(getLabelRect(tester).size, equals(labelSize * 0.75)); }); - testWidgets('InputDecorationTheme.toString()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme.toString()', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/19305 expect( const InputDecorationTheme( @@ -5055,7 +5380,7 @@ void main() { }); - testWidgets('InputDecoration default border uses colorScheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration default border uses colorScheme', (WidgetTester tester) async { final ThemeData theme = ThemeData.from(colorScheme: const ColorScheme.light()); final Color enabledColor = useMaterial3 ? theme.colorScheme.onSurfaceVariant : theme.colorScheme.onSurface.withOpacity(0.38); final Color disabledColor = useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.12) : theme.disabledColor; @@ -5147,7 +5472,7 @@ void main() { expect(getBorderColor(tester), useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : Colors.transparent); }); - testWidgets('InputDecoration borders', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration borders', (WidgetTester tester) async { const InputBorder errorBorder = OutlineInputBorder( borderSide: BorderSide(color: Colors.red, width: 1.5), ); @@ -5289,7 +5614,7 @@ void main() { expect(getBorder(tester), disabledBorder); }); - testWidgets('OutlineInputBorder borders scale down to fit when large values are passed in', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlineInputBorder borders scale down to fit when large values are passed in', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/34327 const double largerBorderRadius = 200.0; const double smallerBorderRadius = 100.0; @@ -5394,7 +5719,7 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/55317 - testWidgets('rounded OutlineInputBorder with zero padding just wraps the label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('rounded OutlineInputBorder with zero padding just wraps the label', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/82321 const double borderRadius = 30.0; const String labelText = 'label text'; @@ -5487,7 +5812,7 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/55317 - testWidgets('OutlineInputBorder with BorderRadius.zero should draw a rectangular border', (WidgetTester tester) async { +testWidgetsWithLeakTracking('OutlineInputBorder with BorderRadius.zero should draw a rectangular border', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/78855 const String labelText = 'Flutter'; @@ -5551,7 +5876,7 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/55317 - testWidgets('OutlineInputBorder radius carries over when lerping', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlineInputBorder radius carries over when lerping', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/23982 const Key key = Key('textField'); @@ -5589,7 +5914,7 @@ void main() { expect(getBorderRadius(tester), BorderRadius.zero); }); - testWidgets('OutlineInputBorder async lerp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlineInputBorder async lerp', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28724 final Completer<void> completer = Completer<void>(); @@ -5721,7 +6046,7 @@ void main() { ).hashCode)); }); - testWidgets('InputDecorationTheme implements debugFillDescription', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme implements debugFillDescription', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const InputDecorationTheme( labelStyle: TextStyle(), @@ -5772,12 +6097,15 @@ void main() { ]); }); - testWidgets('uses alphabetic baseline for CJK layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('uses alphabetic baseline for CJK layout', (WidgetTester tester) async { await tester.binding.setLocale('zh', 'CN'); final Typography typography = Typography.material2018(); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + // The dense theme uses ideographic baselines Widget buildFrame(bool alignLabelWithHint) { return MaterialApp( @@ -5815,7 +6143,7 @@ void main() { expect(tester.getBottomLeft(find.text('hint')).dy, isBrowser ? 45.75 : 47.75); }); - testWidgets('InputDecorator floating label Y coordinate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator floating label Y coordinate', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/54028 await tester.pumpWidget( buildInputDecorator( @@ -5838,7 +6166,7 @@ void main() { expect(tester.getTopLeft(find.text('label')).dy, -4.0); }); - testWidgets('InputDecorator floating label obeys floatingLabelBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator floating label obeys floatingLabelBehavior', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -5855,7 +6183,7 @@ void main() { expect(tester.getTopLeft(find.text('label')).dy, 20.0); }); - testWidgets('InputDecorator hint is displayed when floatingLabelBehavior is always', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator hint is displayed when floatingLabelBehavior is always', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( useMaterial3: useMaterial3, @@ -5873,7 +6201,7 @@ void main() { expect(getOpacity(tester, 'hint'), 1.0); }); - testWidgets('InputDecorator floating label width scales when focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorator floating label width scales when focused', (WidgetTester tester) async { final String longStringA = String.fromCharCodes(List<int>.generate(200, (_) => 65)); final String longStringB = String.fromCharCodes(List<int>.generate(200, (_) => 66)); @@ -5928,16 +6256,13 @@ void main() { } final Rect clipRect = arguments[0] as Rect; // _kFinalLabelScale = 0.75 - const double width = bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? 100 / 0.75 - : 133.0; - expect(clipRect, rectMoreOrLessEquals(const Rect.fromLTWH(0, 0, width, 16.0), epsilon: 1e-5)); + expect(clipRect, rectMoreOrLessEquals(const Rect.fromLTWH(0, 0, 100 / 0.75, 16.0), epsilon: 1e-5)); return true; }), ); }, skip: isBrowser); // TODO(yjbanov): https://github.com/flutter/flutter/issues/44020 - testWidgets('textAlignVertical can be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('textAlignVertical can be updated', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/56933 const String hintText = 'hint'; TextAlignVertical? alignment = TextAlignVertical.top; @@ -5977,7 +6302,7 @@ void main() { expect(tester.getTopLeft(find.text(hintText)).dy, topPosition); }); - testWidgets("InputDecorator label width isn't affected by prefix or suffix", (WidgetTester tester) async { + testWidgetsWithLeakTracking("InputDecorator label width isn't affected by prefix or suffix", (WidgetTester tester) async { const String labelText = 'My Label'; const String prefixText = 'The five boxing wizards jump quickly.'; const String suffixText = 'Suffix'; @@ -6038,7 +6363,7 @@ void main() { }); // Related issue: https://github.com/flutter/flutter/issues/64427 - testWidgets('OutlineInputBorder and InputDecorator long labels and in Floating, the width should ignore the icon width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlineInputBorder and InputDecorator long labels and in Floating, the width should ignore the icon width', (WidgetTester tester) async { const String labelText = 'Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.'; Widget getLabeledInputDecorator(FloatingLabelBehavior floatingLabelBehavior) => MaterialApp( @@ -6089,7 +6414,7 @@ void main() { expect(getLabelRect(tester).width, floatedLabelWidth); }); - testWidgets('given enough space, constrained and unconstrained heights result in the same size widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('given enough space, constrained and unconstrained heights result in the same size widget', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/65572 final UniqueKey keyUnconstrained = UniqueKey(); final UniqueKey keyConstrained = UniqueKey(); @@ -6140,7 +6465,9 @@ void main() { expect(constrainedHeightCompact, equals(unConstrainedHeightCompact)); }); - testWidgets('A vertically constrained TextField still positions its text inside of itself', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A vertically constrained TextField still positions its text inside of itself', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'A'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: Material( child: Center( @@ -6148,7 +6475,7 @@ void main() { width: 200, height: 28, child: TextField( - controller: TextEditingController(text: 'A'), + controller: controller, ), ), ), @@ -6165,7 +6492,7 @@ void main() { expect(textTop, lessThan(textFieldBottom)); }); - testWidgets('visual density is included in the intrinsic height calculation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('visual density is included in the intrinsic height calculation', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); final UniqueKey intrinsicHeightKey = UniqueKey(); await tester.pumpWidget(MaterialApp( @@ -6205,7 +6532,7 @@ void main() { expect(intrinsicHeight, equals(height)); }); - testWidgets('error message for negative baseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('error message for negative baseline', (WidgetTester tester) async { FlutterErrorDetails? errorDetails; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { @@ -6243,7 +6570,7 @@ void main() { expect(errorDetails?.toString(), contains('RenderStack')); }); - testWidgets('min intrinsic height for TextField with no content padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with no content padding', (WidgetTester tester) async { // Regression test for: https://github.com/flutter/flutter/issues/75509 await tester.pumpWidget(const MaterialApp( home: Material( @@ -6268,7 +6595,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('min intrinsic height for TextField with prefix icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with prefix icon', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'input'); + addTearDown(controller.dispose); + // Regression test for: https://github.com/flutter/flutter/issues/87403 await tester.pumpWidget(MaterialApp( home: Material( @@ -6279,7 +6609,7 @@ void main() { child: Column( children: <Widget>[ TextField( - controller: TextEditingController(text: 'input'), + controller: controller, maxLines: null, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), @@ -6296,7 +6626,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('min intrinsic height for TextField with suffix icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with suffix icon', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'input'); + addTearDown(controller.dispose); + // Regression test for: https://github.com/flutter/flutter/issues/87403 await tester.pumpWidget(MaterialApp( home: Material( @@ -6307,7 +6640,7 @@ void main() { child: Column( children: <Widget>[ TextField( - controller: TextEditingController(text: 'input'), + controller: controller, maxLines: null, decoration: const InputDecoration( suffixIcon: Icon(Icons.search), @@ -6324,7 +6657,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('min intrinsic height for TextField with prefix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with prefix', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'input'); + addTearDown(controller.dispose); + // Regression test for: https://github.com/flutter/flutter/issues/87403 await tester.pumpWidget(MaterialApp( home: Material( @@ -6335,7 +6671,7 @@ void main() { child: Column( children: <Widget>[ TextField( - controller: TextEditingController(text: 'input'), + controller: controller, maxLines: null, decoration: const InputDecoration( prefix: Text('prefix'), @@ -6352,7 +6688,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('min intrinsic height for TextField with suffix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with suffix', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'input'); + addTearDown(controller.dispose); + // Regression test for: https://github.com/flutter/flutter/issues/87403 await tester.pumpWidget(MaterialApp( home: Material( @@ -6363,7 +6702,7 @@ void main() { child: Column( children: <Widget>[ TextField( - controller: TextEditingController(text: 'input'), + controller: controller, maxLines: null, decoration: const InputDecoration( suffix: Text('suffix'), @@ -6380,7 +6719,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('min intrinsic height for TextField with icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('min intrinsic height for TextField with icon', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'input'); + addTearDown(controller.dispose); + // Regression test for: https://github.com/flutter/flutter/issues/87403 await tester.pumpWidget(MaterialApp( home: Material( @@ -6391,7 +6733,7 @@ void main() { child: Column( children: <Widget>[ TextField( - controller: TextEditingController(text: 'input'), + controller: controller, maxLines: null, decoration: const InputDecoration( icon: Icon(Icons.search), @@ -6408,7 +6750,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('InputDecorationTheme floatingLabelStyle overrides label widget styles when the widget is a text widget (focused)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme floatingLabelStyle overrides label widget styles when the widget is a text widget (focused)', (WidgetTester tester) async { const TextStyle style16 = TextStyle(fontSize: 16.0); final TextStyle floatingLabelStyle = style16.merge(const TextStyle(color: Colors.indigo)); @@ -6450,7 +6792,7 @@ void main() { expect(getLabelStyle(tester).color, floatingLabelStyle.color); }); - testWidgets('InputDecorationTheme labelStyle overrides label widget styles when the widget is a text widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecorationTheme labelStyle overrides label widget styles when the widget is a text widget', (WidgetTester tester) async { const TextStyle styleDefaultSize = TextStyle(fontSize: 16.0); final TextStyle labelStyle = styleDefaultSize.merge(const TextStyle(color: Colors.purple)); @@ -6491,7 +6833,7 @@ void main() { expect(getLabelStyle(tester).color, labelStyle.color); }); - testWidgets('hint style overflow works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hint style overflow works', (WidgetTester tester) async { final String hintText = 'hint text' * 20; const TextStyle hintStyle = TextStyle( fontSize: 14.0, @@ -6517,7 +6859,7 @@ void main() { expect(hintTextWidget.style!.overflow, decoration.hintStyle!.overflow); }); - testWidgets('prefixIcon in RTL with asymmetric padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prefixIcon in RTL with asymmetric padding', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/129591 const InputDecoration decoration = InputDecoration( contentPadding: EdgeInsetsDirectional.only(end: 24), @@ -6546,5 +6888,50 @@ void main() { // The prefix is inside the decorator. expect(decoratorRight, lessThanOrEqualTo(prefixRight)); }); -} + + testWidgetsWithLeakTracking('InputDecorator with counter does not crash when given a 0 size', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/129611 + const InputDecoration decoration = InputDecoration( + contentPadding: EdgeInsetsDirectional.all(99), + prefixIcon: Focus(child: Icon(Icons.search)), + counter: Text('COUNTER'), + ); + + await tester.pumpWidget( + Center( + child: SizedBox.square( + dimension: 0.0, + child: buildInputDecorator( + useMaterial3: useMaterial3, + decoration: decoration, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(InputDecorator), findsOneWidget); + expect(tester.renderObject<RenderBox>(find.text('COUNTER')).size, Size.zero); + }); + + group('isCollapsed parameter works with themeData', () { + test('parameter is provided in InputDecorationTheme', () { + final InputDecoration decoration = const InputDecoration( + hintText: 'Hello, Flutter!', + ).applyDefaults(const InputDecorationTheme( + isCollapsed: true, + )); + + expect(decoration.isCollapsed, true); + }); + + test('parameter is provided in InputDecoration', () { + final InputDecoration decoration = const InputDecoration( + isCollapsed: true, + hintText: 'Hello, Flutter!', + ).applyDefaults(const InputDecorationTheme()); + + expect(decoration.isCollapsed, true); + }); + }); } diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index d18beb02620fb..8e491d7193b26 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -10,8 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -52,7 +51,7 @@ class TestTextState extends State<TestText> { } void main() { - testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile geometry (LTR)', (WidgetTester tester) async { // See https://material.io/go/design-lists final Key leadingKey = GlobalKey(); @@ -156,25 +155,27 @@ void main() { await tester.pumpWidget(buildFrame(isTwoLine: true, textScaleFactor: 4.0)); testChildren(); testHorizontalGeometry(); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - testVerticalGeometry(hasIssue99933 ? 193 : 192.0); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + testVerticalGeometry(192.0); + } // Make sure that the height of a large subtitle is taken into account. await tester.pumpWidget(buildFrame(isTwoLine: true, textScaleFactor: 0.5, subtitleScaleFactor: 4.0)); testChildren(); testHorizontalGeometry(); - testVerticalGeometry(hasIssue99933 ? 109 : 108.0); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + testVerticalGeometry(108.0); + } await tester.pumpWidget(buildFrame(isThreeLine: true, textScaleFactor: 4.0)); testChildren(); testHorizontalGeometry(); - testVerticalGeometry(hasIssue99933 ? 193 : 192.0); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + testVerticalGeometry(192.0); + } }); - testWidgets('ListTile geometry (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile geometry (RTL)', (WidgetTester tester) async { const double leftPadding = 10.0; const double rightPadding = 20.0; await tester.pumpWidget(MaterialApp( @@ -210,7 +211,7 @@ void main() { testHorizontalGeometry(); }); - testWidgets('ListTile.divideTiles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.divideTiles', (WidgetTester tester) async { final List<String> titles = <String>[ 'first', 'second', 'third' ]; await tester.pumpWidget(MaterialApp( @@ -233,17 +234,17 @@ void main() { expect(find.text('third'), findsOneWidget); }); - testWidgets('ListTile.divideTiles with empty list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.divideTiles with empty list', (WidgetTester tester) async { final Iterable<Widget> output = ListTile.divideTiles(tiles: <Widget>[], color: Colors.grey); expect(output, isEmpty); }); - testWidgets('ListTile.divideTiles with single item list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.divideTiles with single item list', (WidgetTester tester) async { final Iterable<Widget> output = ListTile.divideTiles(tiles: const <Widget>[SizedBox()], color: Colors.grey); expect(output.single, isA<SizedBox>()); }); - testWidgets('ListTile.divideTiles only runs the generator once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.divideTiles only runs the generator once', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/78879 int callCount = 0; Iterable<Widget> generator() sync* { @@ -257,7 +258,7 @@ void main() { expect(callCount, 1); }); - testWidgets('ListTile semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -336,7 +337,7 @@ void main() { semantics.dispose(); }); - testWidgets('ListTile contentPadding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile contentPadding', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MediaQuery( data: const MediaQueryData(), @@ -378,7 +379,7 @@ void main() { expect(right('L'), 790.0); // 800 - contentPadding.start }); - testWidgets('ListTile wide leading Widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile wide leading Widget', (WidgetTester tester) async { const Key leadingKey = ValueKey<String>('L'); Widget buildFrame(double leadingWidth, TextDirection textDirection) { @@ -444,7 +445,7 @@ void main() { expect(right('subtitle'), 800.0 - 72.0); }); - testWidgets('ListTile leading and trailing positions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile leading and trailing positions', (WidgetTester tester) async { // This test is based on the redlines at // https://material.io/design/components/lists.html#specs @@ -504,13 +505,13 @@ void main() { ), ), ); - // TODO(tahatesser): https://github.com/flutter/flutter/issues/99933 - // A bug in the HTML renderer and/or Chrome 96+ causes a - // discrepancy in the paragraph height. - const bool hasIssue99933 = kIsWeb && !bool.fromEnvironment('FLUTTER_WEB_USE_SKIA'); - const double height = hasIssue99933 ? 301.0 : 300; - const double avatarTop = hasIssue99933 ? 130.5 : 130.0; - const double placeholderTop = hasIssue99933 ? 138.5 : 138.0; + + if (kIsWeb && !isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + return; + } + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; // LEFT TOP WIDTH HEIGHT expect(tester.getRect(find.byType(ListTile).at(0)), const Rect.fromLTWH( 0.0, 0.0, 800.0, height)); expect(tester.getRect(find.byType(CircleAvatar).at(0)), const Rect.fromLTWH( 16.0, avatarTop, 40.0, 40.0)); @@ -585,7 +586,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(3)), const Rect.fromLTWH(800.0 - 24.0 - 24.0, 328.0 + 16.0, 24.0, 24.0)); }); - testWidgets('ListTile leading icon height does not exceed ListTile height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile leading icon height does not exceed ListTile height', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/28765 const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder()); @@ -668,7 +669,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(16.0, 88.0 + 8.0, 24.0, 56.0)); }); - testWidgets('ListTile trailing icon height does not exceed ListTile height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile trailing icon height does not exceed ListTile height', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/28765 const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder()); @@ -757,7 +758,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(800.0 - 24.0 - 24.0, 88.0 + 8.0, 24.0, 56.0)); }); - testWidgets('ListTile only accepts focus when enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile only accepts focus when enabled', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( @@ -804,7 +805,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); }); - testWidgets('ListTile can autofocus unless disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile can autofocus unless disabled.', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( @@ -849,7 +850,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); }); - testWidgets('ListTile is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'ListTile'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; Widget buildApp({bool enabled = true}) { @@ -904,9 +905,11 @@ void main() { rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), ), ); + + focusNode.dispose(); }); - testWidgets('ListTile can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -989,7 +992,7 @@ void main() { ); }); - testWidgets('ListTile can be splashed and has correct splash color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile can be splashed and has correct splash color', (WidgetTester tester) async { final Widget buildApp = MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( @@ -1014,7 +1017,7 @@ void main() { await gesture.up(); }); - testWidgets('ListTile can be triggered by keyboard shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile can be triggered by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Key tileKey = Key('ListTile'); bool tapped = false; @@ -1053,7 +1056,7 @@ void main() { expect(tapped, isTrue); }); - testWidgets('ListTile responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -1090,7 +1093,7 @@ void main() { expect(box.size, equals(const Size(800, 44))); }); - testWidgets('ListTile shape is painted correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile shape is painted correctly', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/63877 const ShapeBorder rectShape = RoundedRectangleBorder(); const ShapeBorder stadiumShape = StadiumBorder(); @@ -1130,7 +1133,7 @@ void main() { ); }); - testWidgets('ListTile changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile changes mouse cursor when hovered', (WidgetTester tester) async { // Test ListTile() constructor await tester.pumpWidget( MaterialApp( @@ -1208,7 +1211,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('ListTile onFocusChange callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile onFocusChange callback', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'ListTile Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -1234,9 +1237,11 @@ void main() { await tester.pump(); expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('ListTile respects tileColor & selectedTileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile respects tileColor & selectedTileColor', (WidgetTester tester) async { bool isSelected = false; final Color tileColor = Colors.green.shade500; final Color selectedTileColor = Colors.red.shade500; @@ -1274,7 +1279,7 @@ void main() { expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); - testWidgets('ListTile shows Material ripple effects on top of tileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile shows Material ripple effects on top of tileColor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/73616 final Color tileColor = Colors.red.shade500; @@ -1309,7 +1314,7 @@ void main() { ); }); - testWidgets('ListTile default tile color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile default tile color', (WidgetTester tester) async { bool isSelected = false; final ThemeData theme = ThemeData(useMaterial3: true); const Color defaultColor = Colors.transparent; @@ -1344,7 +1349,7 @@ void main() { expect(find.byType(Material), paints..rect(color: defaultColor)); }); - testWidgets('Default tile color when ListTile is wrapped with an elevated widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default tile color when ListTile is wrapped with an elevated widget', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117700 bool isSelected = false; final ThemeData theme = ThemeData(useMaterial3: true); @@ -1397,7 +1402,7 @@ void main() { expect(find.byType(Material), paints..rect(color: defaultColor)); }); - testWidgets('ListTile layout at zero size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile layout at zero size', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66636 const Key key = Key('key'); @@ -1428,7 +1433,7 @@ void main() { feedback.dispose(); }); - testWidgets('ListTile with disabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile with disabled feedback', (WidgetTester tester) async { const bool enableFeedback = false; await tester.pumpWidget( @@ -1449,7 +1454,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('ListTile with enabled feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile with enabled feedback', (WidgetTester tester) async { const bool enableFeedback = true; await tester.pumpWidget( @@ -1470,7 +1475,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('ListTile with enabled feedback by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile with enabled feedback by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1489,7 +1494,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('ListTile with disabled feedback using ListTileTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile with disabled feedback using ListTileTheme', (WidgetTester tester) async { const bool enableFeedbackTheme = false; await tester.pumpWidget( @@ -1512,7 +1517,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('ListTile.enableFeedback overrides ListTileTheme.enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.enableFeedback overrides ListTileTheme.enableFeedback', (WidgetTester tester) async { const bool enableFeedbackTheme = false; const bool enableFeedback = true; @@ -1537,7 +1542,7 @@ void main() { expect(feedback.hapticCount, 0); }); - testWidgets('ListTile.mouseCursor overrides ListTileTheme.mouseCursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.mouseCursor overrides ListTileTheme.mouseCursor', (WidgetTester tester) async { final Key tileKey = UniqueKey(); await tester.pumpWidget( @@ -1565,7 +1570,7 @@ void main() { }); }); - testWidgets('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection, { double? themeHorizontalTitleGap, double? widgetHorizontalTitleGap }) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1617,7 +1622,7 @@ void main() { expect(right('title'), 760.0); }); - testWidgets('ListTile horizontalTitleGap = (default) && ListTile minLeadingWidth = (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap = (default) && ListTile minLeadingWidth = (default)', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1653,7 +1658,7 @@ void main() { expect(right('title'), 744.0); }); - testWidgets('ListTile horizontalTitleGap with visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap with visualDensity', (WidgetTester tester) async { Widget buildFrame({ double? horizontalTitleGap, VisualDensity? visualDensity, @@ -1698,7 +1703,7 @@ void main() { expect(left('title'), 42.0); }); - testWidgets('ListTile minVerticalPadding = 80.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile minVerticalPadding = 80.0', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection, { double? themeMinVerticalPadding, double? widgetMinVerticalPadding }) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1744,7 +1749,7 @@ void main() { expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0)); }); - testWidgets('ListTile minLeadingWidth = 60.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile minLeadingWidth = 60.0', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection, { double? themeMinLeadingWidth, double? widgetMinLeadingWidth }) { return MediaQuery( data: const MediaQueryData(), @@ -1799,7 +1804,7 @@ void main() { expect(right('title'), 708.0); }); - testWidgets('colors are applied to leading and trailing text widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('colors are applied to leading and trailing text widgets', (WidgetTester tester) async { final Key leadingKey = UniqueKey(); final Key trailingKey = UniqueKey(); @@ -1851,7 +1856,7 @@ void main() { expect(textColor(trailingKey), theme.disabledColor); }); - testWidgets('selected, enabled ListTile default icon color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selected, enabled ListTile default icon color', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colorScheme = theme.colorScheme; final Key leadingKey = UniqueKey(); @@ -1891,7 +1896,7 @@ void main() { expect(iconColor(trailingKey), colorScheme.onSurfaceVariant); }); - testWidgets('ListTile font size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile font size', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1924,7 +1929,7 @@ void main() { expect(trailing.text.style!.fontSize, 11.0); }); - testWidgets('ListTile text color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile text color', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1959,7 +1964,7 @@ void main() { expect(trailing.text.style!.color, theme.colorScheme.onSurfaceVariant); }); - testWidgets('Default ListTile debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ListTile debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTile().debugFillProperties(builder); @@ -1971,7 +1976,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ListTile implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTile( leading: Text('leading'), @@ -2044,7 +2049,7 @@ void main() { ); }); - testWidgets('ListTile.textColor respects MaterialStateColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.textColor respects MaterialStateColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; @@ -2100,7 +2105,7 @@ void main() { expect(title.text.style!.color, selectedColor); }); - testWidgets('ListTile.iconColor respects MaterialStateColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.iconColor respects MaterialStateColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; @@ -2155,7 +2160,7 @@ void main() { expect(iconColor(leadingKey), selectedColor); }); - testWidgets('ListTile.iconColor respects iconColor property with icon buttons Material 3 in presence of IconButtonTheme override', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.iconColor respects iconColor property with icon buttons Material 3 in presence of IconButtonTheme override', (WidgetTester tester) async { const Color iconButtonThemeColor = Colors.blue; const Color listTileIconColor = Colors.green; const Icon leadingIcon = Icon(Icons.favorite); @@ -2199,7 +2204,7 @@ void main() { expect(getIconStyle(tester, trailingIcon.icon!)?.color, listTileIconColor); }); - testWidgets('ListTile.dense does not throw assertion', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile.dense does not throw assertion', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/pull/116908 Widget buildFrame({required bool useMaterial3}) { @@ -2227,7 +2232,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('titleAlignment position with title widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('titleAlignment position with title widget', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; @@ -2326,7 +2331,7 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, bottomPosition); }); - testWidgets('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; @@ -2426,7 +2431,7 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, bottomPosition); }); - testWidgets("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; @@ -2486,7 +2491,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile geometry (LTR)', (WidgetTester tester) async { // See https://material.io/go/design-lists final Key leadingKey = GlobalKey(); @@ -2634,7 +2639,7 @@ void main() { testVerticalGeometry(128.0); }); - testWidgets('ListTile geometry (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile geometry (RTL)', (WidgetTester tester) async { const double leftPadding = 10.0; const double rightPadding = 20.0; await tester.pumpWidget(MaterialApp( @@ -2670,7 +2675,7 @@ void main() { testHorizontalGeometry(); }); - testWidgets('ListTile leading and trailing positions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile leading and trailing positions', (WidgetTester tester) async { // This test is based on the redlines at // https://material.io/design/components/lists.html#specs @@ -2905,7 +2910,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(3)), const Rect.fromLTWH(800.0 - 24.0 - 16.0, 216.0 + 16.0, 24.0, 24.0)); }); - testWidgets('ListTile leading icon height does not exceed ListTile height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile leading icon height does not exceed ListTile height', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/28765 const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder()); @@ -3078,7 +3083,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(16.0, 88.0 + 16.0, 24.0, 56.0)); }); - testWidgets('ListTile trailing icon height does not exceed ListTile height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile trailing icon height does not exceed ListTile height', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/28765 const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder()); @@ -3251,7 +3256,7 @@ void main() { expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(800.0 - 16.0 - 24.0, 88.0 + 16.0, 24.0, 56.0)); }); - testWidgets('ListTile wide leading Widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile wide leading Widget', (WidgetTester tester) async { const Key leadingKey = ValueKey<String>('L'); Widget buildFrame(double leadingWidth, TextDirection textDirection) { @@ -3316,7 +3321,7 @@ void main() { expect(right('subtitle'), 800.0 - 72.0); }); - testWidgets('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection, { double? themeHorizontalTitleGap, double? widgetHorizontalTitleGap }) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -3368,7 +3373,7 @@ void main() { expect(right('title'), 744.0); }); - testWidgets('ListTile horizontalTitleGap = (default) && ListTile minLeadingWidth = (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap = (default) && ListTile minLeadingWidth = (default)', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -3404,7 +3409,7 @@ void main() { expect(right('title'), 728.0); }); - testWidgets('ListTile horizontalTitleGap with visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile horizontalTitleGap with visualDensity', (WidgetTester tester) async { Widget buildFrame({ double? horizontalTitleGap, VisualDensity? visualDensity, @@ -3449,7 +3454,7 @@ void main() { expect(left('title'), 58.0); }); - testWidgets('ListTile minVerticalPadding = 80.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile minVerticalPadding = 80.0', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection, { double? themeMinVerticalPadding, double? widgetMinVerticalPadding }) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -3495,7 +3500,7 @@ void main() { expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 176.0)); }); - testWidgets('ListTile font size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile font size', (WidgetTester tester) async { Widget buildFrame({ bool dense = false, bool enabled = true, @@ -3573,7 +3578,7 @@ void main() { expect(trailing.text.style!.fontSize, 14.0); }); - testWidgets('ListTile text color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile text color', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); Widget buildFrame({ bool dense = false, @@ -3628,7 +3633,7 @@ void main() { expect(trailing.text.style!.color, theme.textTheme.bodyMedium!.color); }); - testWidgets('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/77004 const ColorScheme lightColorScheme = ColorScheme.light(); @@ -3688,7 +3693,7 @@ void main() { expect(iconColor(trailingKey), Colors.white); }); - testWidgets('ListTile default tile color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile default tile color', (WidgetTester tester) async { bool isSelected = false; const Color defaultColor = Colors.transparent; @@ -3722,7 +3727,7 @@ void main() { expect(find.byType(Material), paints..rect(color: defaultColor)); }); - testWidgets('titleAlignment position with title widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('titleAlignment position with title widget', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; @@ -3821,7 +3826,7 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, bottomPosition); }); - testWidgets('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('titleAlignment position with title and subtitle widgets', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; @@ -3921,7 +3926,7 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, bottomPosition); }); - testWidgets("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ListTile.isThreeLine updates ListTileTitleAlignment.threeLine's alignment", (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const double leadingHeight = 24.0; diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index c18921ba8f932..4be26d698df19 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestIcon extends StatefulWidget { const TestIcon({ super.key }); @@ -80,7 +79,7 @@ void main() { expect(themeData.titleAlignment, null); }); - testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTileThemeData().debugFillProperties(builder); @@ -92,7 +91,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTileThemeData( dense: true, @@ -147,7 +146,7 @@ void main() { ); }); - testWidgets('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { late ListTileThemeData theme; await tester.pumpWidget( @@ -197,7 +196,7 @@ void main() { expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); }); - testWidgets('ListTileTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme', (WidgetTester tester) async { final Key listTileKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key subtitleKey = UniqueKey(); @@ -333,7 +332,7 @@ void main() { expect(trailingOffset.dy - titleOffset.dy, 6); }); - testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { final Key leadingKey = UniqueKey(); final Key trailingKey = UniqueKey(); @@ -393,15 +392,31 @@ void main() { expect(textColor(trailingKey), theme.disabledColor); }); - testWidgets( - "ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", + testWidgetsWithLeakTracking( + "Material3 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", (WidgetTester tester) async { + const TextStyle titleTextStyle = TextStyle( + fontSize: 23.0, + color: Color(0xffff0000), + fontStyle: FontStyle.italic, + ); + const TextStyle subtitleTextStyle = TextStyle( + fontSize: 20.0, + color: Color(0xff00ff00), + fontStyle: FontStyle.italic, + ); + const TextStyle leadingAndTrailingTextStyle = TextStyle( + fontSize: 18.0, + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + final ThemeData theme = ThemeData( useMaterial3: true, listTileTheme: const ListTileThemeData( - titleTextStyle: TextStyle(fontSize: 20.0), - subtitleTextStyle: TextStyle(fontSize: 17.5), - leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, + leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, ), ); @@ -427,31 +442,51 @@ void main() { await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 15.0); + expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); + expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.fontSize, 20.0); + expect(title.text.style!.fontSize, titleTextStyle.fontSize); + expect(title.text.style!.color, titleTextStyle.color); + expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.fontSize, 17.5); + expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); + expect(subtitle.text.style!.color, subtitleTextStyle.color); + expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 15.0); + expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); + expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); - testWidgets( - "ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", + testWidgetsWithLeakTracking( + "Material2 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", (WidgetTester tester) async { + const TextStyle titleTextStyle = TextStyle( + fontSize: 23.0, + color: Color(0xffff0000), + fontStyle: FontStyle.italic, + ); + const TextStyle subtitleTextStyle = TextStyle( + fontSize: 20.0, + color: Color(0xff00ff00), + fontStyle: FontStyle.italic, + ); + const TextStyle leadingAndTrailingTextStyle = TextStyle( + fontSize: 18.0, + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + final ThemeData theme = ThemeData( - useMaterial3: true, - listTileTheme: const ListTileThemeData( - titleTextStyle: TextStyle(fontSize: 20.0), - subtitleTextStyle: TextStyle(fontSize: 17.5), - leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + useMaterial3: false, + listTileTheme: const ListTileThemeData( + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, + leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, ), ); - const TextStyle titleTextStyle = TextStyle(fontSize: 23.0); - const TextStyle subtitleTextStyle = TextStyle(fontSize: 20.0); - const TextStyle leadingAndTrailingTextStyle = TextStyle(fontSize: 18.0); - Widget buildFrame() { return MaterialApp( theme: theme, @@ -460,9 +495,6 @@ void main() { child: Builder( builder: (BuildContext context) { return const ListTile( - titleTextStyle: titleTextStyle, - subtitleTextStyle: subtitleTextStyle, - leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, leading: TestText('leading'), title: TestText('title'), subtitle: TestText('subtitle'), @@ -477,16 +509,162 @@ void main() { await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); - expect(leading.text.style!.fontSize, 18.0); + expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); + expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); - expect(title.text.style!.fontSize, 23.0); + expect(title.text.style!.fontSize, titleTextStyle.fontSize); + expect(title.text.style!.color, titleTextStyle.color); + expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); - expect(subtitle.text.style!.fontSize, 20.0); + expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); + expect(subtitle.text.style!.color, subtitleTextStyle.color); + expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); - expect(trailing.text.style!.fontSize, 18.0); + expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); + expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); + }); + + testWidgetsWithLeakTracking( + "Material3 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", + (WidgetTester tester) async { + final ThemeData theme = ThemeData( + useMaterial3: true, + listTileTheme: const ListTileThemeData( + titleTextStyle: TextStyle(fontSize: 20.0), + subtitleTextStyle: TextStyle(fontSize: 17.5), + leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + ), + ); + const TextStyle titleTextStyle = TextStyle( + fontSize: 23.0, + color: Color(0xffff0000), + fontStyle: FontStyle.italic, + ); + const TextStyle subtitleTextStyle = TextStyle( + fontSize: 20.0, + color: Color(0xff00ff00), + fontStyle: FontStyle.italic, + ); + const TextStyle leadingAndTrailingTextStyle = TextStyle( + fontSize: 18.0, + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return const ListTile( + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, + leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle'), + trailing: TestText('trailing'), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); + expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.fontSize, titleTextStyle.fontSize); + expect(title.text.style!.color, titleTextStyle.color); + expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); + expect(subtitle.text.style!.color, subtitleTextStyle.color); + expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); + expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); + }); + + testWidgetsWithLeakTracking( + "Material2 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", + (WidgetTester tester) async { + final ThemeData theme = ThemeData( + useMaterial3: false, + listTileTheme: const ListTileThemeData( + titleTextStyle: TextStyle(fontSize: 20.0), + subtitleTextStyle: TextStyle(fontSize: 17.5), + leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), + ), + ); + const TextStyle titleTextStyle = TextStyle( + fontSize: 23.0, + color: Color(0xffff0000), + fontStyle: FontStyle.italic, + ); + const TextStyle subtitleTextStyle = TextStyle( + fontSize: 20.0, + color: Color(0xff00ff00), + fontStyle: FontStyle.italic, + ); + const TextStyle leadingAndTrailingTextStyle = TextStyle( + fontSize: 18.0, + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + + Widget buildFrame() { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return const ListTile( + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, + leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, + leading: TestText('leading'), + title: TestText('title'), + subtitle: TestText('subtitle'), + trailing: TestText('trailing'), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); + expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); + expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); + final RenderParagraph title = _getTextRenderObject(tester, 'title'); + expect(title.text.style!.fontSize, titleTextStyle.fontSize); + expect(title.text.style!.color, titleTextStyle.color); + expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); + final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); + expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); + expect(subtitle.text.style!.color, subtitleTextStyle.color); + expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); + final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); + expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); + expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); + expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); - testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { late ListTileThemeData theme; bool isSelected = false; @@ -526,7 +704,7 @@ void main() { expect(find.byType(Material), paints..rect(color: theme.selectedTileColor)); }); - testWidgets("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { bool isSelected = false; final Color tileColor = Colors.green.shade500; final Color selectedTileColor = Colors.red.shade500; @@ -568,7 +746,7 @@ void main() { expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); - testWidgets('ListTile uses ListTileTheme shape in a drawer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile uses ListTileTheme shape in a drawer', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/106303 final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -601,7 +779,7 @@ void main() { expect(inkWellBorder, shapeBorder); }); - testWidgets('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; @@ -661,7 +839,7 @@ void main() { expect(title.text.style!.color, selectedColor); }); - testWidgets('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; @@ -720,7 +898,7 @@ void main() { expect(iconColor(leadingKey), selectedColor); }); - testWidgets('ListTileThemeData copyWith overrides all properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileThemeData copyWith overrides all properties', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/119734 const ListTileThemeData original = ListTileThemeData( @@ -782,7 +960,7 @@ void main() { expect(copy.titleAlignment, ListTileTitleAlignment.top); }); - testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const String titleText = '\nHeadline Text\n'; @@ -818,7 +996,7 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, 8.0); }); - testWidgets('ListTileTheme.merge supports all properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTileTheme.merge supports all properties', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( theme: ThemeData( diff --git a/packages/flutter/test/material/localizations_test.dart b/packages/flutter/test/material/localizations_test.dart index 67df019b002e9..e0fdaccbbba82 100644 --- a/packages/flutter/test/material/localizations_test.dart +++ b/packages/flutter/test/material/localizations_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('English translations exist for all MaterialLocalizations properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('English translations exist for all MaterialLocalizations properties', (WidgetTester tester) async { const MaterialLocalizations localizations = DefaultMaterialLocalizations(); expect(localizations.openAppDrawerTooltip, isNotNull); @@ -29,6 +30,9 @@ void main() { expect(localizations.copyButtonLabel, isNotNull); expect(localizations.cutButtonLabel, isNotNull); expect(localizations.scanTextButtonLabel, isNotNull); + expect(localizations.lookUpButtonLabel, isNotNull); + expect(localizations.searchWebButtonLabel, isNotNull); + expect(localizations.shareButtonLabel, isNotNull); expect(localizations.okButtonLabel, isNotNull); expect(localizations.pasteButtonLabel, isNotNull); expect(localizations.selectAllButtonLabel, isNotNull); @@ -38,6 +42,7 @@ void main() { expect(localizations.timePickerHourModeAnnouncement, isNotNull); expect(localizations.timePickerMinuteModeAnnouncement, isNotNull); expect(localizations.modalBarrierDismissLabel, isNotNull); + expect(localizations.menuDismissLabel, isNotNull); expect(localizations.drawerLabel, isNotNull); expect(localizations.menuBarMenuLabel, isNotNull); expect(localizations.popupMenuLabel, isNotNull); @@ -165,7 +170,7 @@ void main() { expect(localizations.licensesPackageDetailText(100).contains(r'$licensesCount'), isFalse); }); - testWidgets('MaterialLocalizations.of throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialLocalizations.of throws', (WidgetTester tester) async { final GlobalKey noLocalizationsAvailable = GlobalKey(); final GlobalKey localizationsAvailable = GlobalKey(); @@ -189,7 +194,7 @@ void main() { expect(MaterialLocalizations.of(localizationsAvailable.currentContext!), isA<MaterialLocalizations>()); }); - testWidgets("parseCompactDate doesn't throw an exception on invalid text", (WidgetTester tester) async { + testWidgetsWithLeakTracking("parseCompactDate doesn't throw an exception on invalid text", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/126397. final GlobalKey localizations = GlobalKey(); diff --git a/packages/flutter/test/material/magnifier_test.dart b/packages/flutter/test/material/magnifier_test.dart index 9abf8906b2fcd..275550523ff99 100644 --- a/packages/flutter/test/material/magnifier_test.dart +++ b/packages/flutter/test/material/magnifier_test.dart @@ -8,6 +8,7 @@ library; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final MagnifierController magnifierController = MagnifierController(); @@ -50,7 +51,7 @@ void main() { }); group('adaptiveMagnifierControllerBuilder', () { - testWidgets('should return a TextEditingMagnifier on Android', + testWidgetsWithLeakTracking('should return a TextEditingMagnifier on Android', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), @@ -58,16 +59,19 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); + addTearDown(magnifierPositioner.dispose); + final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), - ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), + magnifierPositioner, ); expect(builtWidget, isA<TextMagnifier>()); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('should return a CupertinoMagnifier on iOS', + testWidgetsWithLeakTracking('should return a CupertinoMagnifier on iOS', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), @@ -75,16 +79,19 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); + addTearDown(magnifierPositioner.dispose); + final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), - ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), + magnifierPositioner, ); expect(builtWidget, isA<CupertinoTextMagnifier>()); }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets('should return null on all platforms not Android, iOS', + testWidgetsWithLeakTracking('should return null on all platforms not Android, iOS', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), @@ -92,10 +99,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); + addTearDown(magnifierPositioner.dispose); + final Widget? builtWidget = TextMagnifier.adaptiveMagnifierConfiguration.magnifierBuilder( context, MagnifierController(), - ValueNotifier<MagnifierInfo>(MagnifierInfo.empty), + magnifierPositioner, ); expect(builtWidget, isNull); @@ -110,7 +120,7 @@ void main() { group('magnifier', () { group('position', () { - testWidgets( + testWidgetsWithLeakTracking( 'should be at gesture position if does not violate any positioning rules', (WidgetTester tester) async { final Key textField = UniqueKey(); @@ -155,6 +165,7 @@ void main() { // The tap position is dragBelow units below the text field. globalGesturePosition: fakeTextFieldRect.center, )); + addTearDown(magnifierInfo.dispose); await showMagnifier(context, tester, magnifierInfo); @@ -166,7 +177,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'should never move outside the right bounds of the editing line', (WidgetTester tester) async { const double gestureOutsideLine = 100; @@ -178,10 +189,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, // Inflate these two to make sure we're bounding on the @@ -199,7 +213,7 @@ void main() { lessThanOrEqualTo(reasonableTextField.right)); }); - testWidgets( + testWidgetsWithLeakTracking( 'should never move outside the left bounds of the editing line', (WidgetTester tester) async { const double gestureOutsideLine = 100; @@ -211,10 +225,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, // Inflate these two to make sure we're bounding on the @@ -231,7 +248,7 @@ void main() { greaterThanOrEqualTo(reasonableTextField.left)); }); - testWidgets('should position vertically at the center of the line', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should position vertically at the center of the line', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), )); @@ -239,10 +256,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -254,7 +274,7 @@ void main() { reasonableTextField.center.dy - basicOffset.dy); }); - testWidgets('should reposition vertically if mashed against the ceiling', + testWidgetsWithLeakTracking('should reposition vertically if mashed against the ceiling', (WidgetTester tester) async { final Rect topOfScreenTextFieldRect = Rect.fromPoints(Offset.zero, const Offset(200, 0)); @@ -266,10 +286,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: topOfScreenTextFieldRect, fieldBounds: topOfScreenTextFieldRect, @@ -289,7 +312,7 @@ void main() { return magnifier.additionalFocalPointOffset; } - testWidgets( + testWidgetsWithLeakTracking( 'should shift focal point so that the lens sees nothing out of bounds', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( @@ -299,10 +322,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -317,7 +343,7 @@ void main() { lessThan(reasonableTextField.left)); }); - testWidgets( + testWidgetsWithLeakTracking( 'focal point should shift if mashed against the top to always point to text', (WidgetTester tester) async { final Rect topOfScreenTextFieldRect = @@ -330,10 +356,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierPositioner; + addTearDown(() => magnifierPositioner.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: topOfScreenTextFieldRect, fieldBounds: topOfScreenTextFieldRect, @@ -354,7 +383,7 @@ void main() { return animatedPositioned.duration.compareTo(Duration.zero) != 0; } - testWidgets('should not be animated on the initial state', + testWidgetsWithLeakTracking('should not be animated on the initial state', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), @@ -363,10 +392,13 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); + late ValueNotifier<MagnifierInfo> magnifierInfo; + addTearDown(() => magnifierInfo.dispose()); + await showMagnifier( context, tester, - ValueNotifier<MagnifierInfo>( + magnifierInfo = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -379,7 +411,7 @@ void main() { expect(getIsAnimated(tester), false); }); - testWidgets('should not be animated on horizontal shifts', + testWidgetsWithLeakTracking('should not be animated on horizontal shifts', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Placeholder(), @@ -388,8 +420,7 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); - final ValueNotifier<MagnifierInfo> magnifierPositioner = - ValueNotifier<MagnifierInfo>( + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -397,6 +428,7 @@ void main() { globalGesturePosition: reasonableTextField.center, ), ); + addTearDown(magnifierPositioner.dispose); await showMagnifier(context, tester, magnifierPositioner); @@ -413,7 +445,7 @@ void main() { expect(getIsAnimated(tester), false); }); - testWidgets('should be animated on vertical shifts', + testWidgetsWithLeakTracking('should be animated on vertical shifts', (WidgetTester tester) async { const Offset verticalShift = Offset(0, 200); @@ -424,8 +456,7 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); - final ValueNotifier<MagnifierInfo> magnifierPositioner = - ValueNotifier<MagnifierInfo>( + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -433,6 +464,7 @@ void main() { globalGesturePosition: reasonableTextField.center, ), ); + addTearDown(magnifierPositioner.dispose); await showMagnifier(context, tester, magnifierPositioner); @@ -449,7 +481,7 @@ void main() { expect(getIsAnimated(tester), true); }); - testWidgets('should stop being animated when timer is up', + testWidgetsWithLeakTracking('should stop being animated when timer is up', (WidgetTester tester) async { const Offset verticalShift = Offset(0, 200); @@ -460,8 +492,7 @@ void main() { final BuildContext context = tester.firstElement(find.byType(Placeholder)); - final ValueNotifier<MagnifierInfo> magnifierPositioner = - ValueNotifier<MagnifierInfo>( + final ValueNotifier<MagnifierInfo> magnifierPositioner = ValueNotifier<MagnifierInfo>( MagnifierInfo( currentLineBoundaries: reasonableTextField, fieldBounds: reasonableTextField, @@ -469,6 +500,7 @@ void main() { globalGesturePosition: reasonableTextField.center, ), ); + addTearDown(magnifierPositioner.dispose); await showMagnifier(context, tester, magnifierPositioner); diff --git a/packages/flutter/test/material/material_button_test.dart b/packages/flutter/test/material/material_button_test.dart index 1b329a19485c5..e68e9d8511808 100644 --- a/packages/flutter/test/material/material_button_test.dart +++ b/packages/flutter/test/material/material_button_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -15,7 +14,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('MaterialButton defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton defaults', (WidgetTester tester) async { final Finder rawButtonMaterial = find.descendant( of: find.byType(MaterialButton), matching: find.byType(Material), @@ -98,7 +97,7 @@ void main() { expect(material.type, MaterialType.transparency); }); - testWidgets('Does MaterialButton work with hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does MaterialButton work with hover', (WidgetTester tester) async { const Color hoverColor = Color(0xff001122); await tester.pumpWidget( @@ -121,7 +120,7 @@ void main() { expect(inkFeatures, paints..rect(color: hoverColor)); }); - testWidgets('Does MaterialButton work with focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does MaterialButton work with focus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node'); @@ -143,9 +142,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('MaterialButton elevation and colors have proper precedence', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton elevation and colors have proper precedence', (WidgetTester tester) async { const double elevation = 10.0; const double focusElevation = 11.0; const double hoverElevation = 12.0; @@ -215,9 +216,11 @@ void main() { expect(inkFeatures, paints..rect(color: focusColor)..rect(color: highlightColor)); expect(material.elevation, equals(highlightElevation)); await gesture2.up(); + + focusNode.dispose(); }); - testWidgets("MaterialButton's disabledColor takes precedence over its default disabled color.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("MaterialButton's disabledColor takes precedence over its default disabled color.", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/30012. final Finder rawButtonMaterial = find.descendant( @@ -240,7 +243,7 @@ void main() { expect(material.color, const Color(0xff00ff00)); }); - testWidgets('Default MaterialButton meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default MaterialButton meets a11y contrast guidelines', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -267,7 +270,7 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('MaterialButton gets focus when autofocus is set.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton gets focus when autofocus is set.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton'); await tester.pumpWidget( MaterialApp( @@ -299,9 +302,11 @@ void main() { await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); + + focusNode.dispose(); }); - testWidgets('MaterialButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder materialButton; @@ -345,7 +350,7 @@ void main() { expect(tester.widget<MaterialButton>(materialButton).enabled, false); }); - testWidgets('MaterialButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -376,7 +381,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets('MaterialButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -430,7 +435,7 @@ void main() { // This test is very similar to the '...explicit splashColor and highlightColor' test // in icon_button_test.dart. If you change this one, you may want to also change that one. - testWidgets('MaterialButton with explicit splashColor and highlightColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton with explicit splashColor and highlightColor', (WidgetTester tester) async { const Color directSplashColor = Color(0xFF000011); const Color directHighlightColor = Color(0xFF000011); @@ -544,7 +549,7 @@ void main() { await gesture.up(); }); - testWidgets('MaterialButton has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton has no clip by default', (WidgetTester tester) async { final GlobalKey buttonKey = GlobalKey(); final Widget buttonWidget = Center( child: MaterialButton( @@ -571,7 +576,7 @@ void main() { ); }); - testWidgets('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const Rect expectedButtonSize = Rect.fromLTRB(0.0, 0.0, 116.0, 48.0); @@ -658,7 +663,7 @@ void main() { semantics.dispose(); }); - testWidgets('MaterialButton minWidth and height parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton minWidth and height parameters', (WidgetTester tester) async { Widget buildFrame({ double? minWidth, double? height, EdgeInsets padding = EdgeInsets.zero, Widget? child }) { return Directionality( textDirection: TextDirection.ltr, @@ -725,7 +730,7 @@ void main() { expect(tester.getSize(find.byType(MaterialButton)), const Size(18.0, 18.0)); }); - testWidgets('MaterialButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( Theme( @@ -765,7 +770,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); }); - testWidgets('MaterialButton shape overrides ButtonTheme shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton shape overrides ButtonTheme shape', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/29146 await tester.pumpWidget( Directionality( @@ -785,7 +790,7 @@ void main() { expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder()); }); - testWidgets('MaterialButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -846,7 +851,7 @@ void main() { expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); }); - testWidgets('disabledElevation is passed to RawMaterialButton', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabledElevation is passed to RawMaterialButton', (WidgetTester tester) async { const double disabledElevation = 16; final Finder rawMaterialButtonFinder = find.descendant( @@ -869,7 +874,7 @@ void main() { expect(rawMaterialButton.disabledElevation, equals(disabledElevation)); }); - testWidgets('MaterialButton.disabledElevation defaults to 0.0 when not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialButton.disabledElevation defaults to 0.0 when not provided', (WidgetTester tester) async { final Finder rawMaterialButtonFinder = find.descendant( of: find.byType(MaterialButton), matching: find.byType(RawMaterialButton), diff --git a/packages/flutter/test/material/material_state_mixin_test.dart b/packages/flutter/test/material/material_state_mixin_test.dart index be13d84dcd28c..f45afb6539ff1 100644 --- a/packages/flutter/test/material/material_state_mixin_test.dart +++ b/packages/flutter/test/material/material_state_mixin_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Key key = Key('testContainer'); const Color trueColor = Colors.red; @@ -83,7 +84,7 @@ void main() { expect(tester.widget<ColoredBox>(find.byKey(key)).color, falseColor); } - testWidgets('MaterialState.pressed is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.pressed is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -93,7 +94,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.focused is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.focused is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -103,7 +104,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.hovered is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.hovered is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -113,7 +114,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.disabled is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.disabled is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -123,7 +124,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.selected is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.selected is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -133,7 +134,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.scrolledUnder is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.scrolledUnder is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -143,7 +144,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.dragged is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.dragged is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, @@ -153,7 +154,7 @@ void main() { await verify(tester, widget, controller); }); - testWidgets('MaterialState.error is tracked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialState.error is tracked', (WidgetTester tester) async { final StreamController<bool> controller = StreamController<bool>(); final _MyWidget widget = _MyWidget( controller: controller, diff --git a/packages/flutter/test/material/material_states_controller_test.dart b/packages/flutter/test/material/material_states_controller_test.dart index 3449e975b6029..275c1034117e8 100644 --- a/packages/flutter/test/material/material_states_controller_test.dart +++ b/packages/flutter/test/material/material_states_controller_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('MaterialStatesController constructor', () { @@ -12,6 +13,13 @@ void main() { expect(MaterialStatesController(<MaterialState>{MaterialState.selected}).value, <MaterialState>{MaterialState.selected}); }); + test('MaterialStatesController dispatches memory events', () async { + await expectLater( + await memoryEvents(() => MaterialStatesController().dispose(), MaterialStatesController), + areCreateAndDispose, + ); + }); + test('MaterialStatesController update, listener', () { int count = 0; void valueChanged() { diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index f08733a29e578..44081098f443f 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -10,8 +10,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/test_border.dart' show TestBorder; class NotifyMaterial extends StatelessWidget { @@ -72,7 +71,7 @@ class ElevationColor { void main() { // Regression test for https://github.com/flutter/flutter/issues/81504 - testWidgets('MaterialApp.home nullable and update test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp.home nullable and update test', (WidgetTester tester) async { // _WidgetsAppState._usesNavigator == true await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); @@ -85,7 +84,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('default Material debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default Material debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Material().debugFillProperties(builder); @@ -97,7 +96,7 @@ void main() { expect(description, <String>['type: canvas']); }); - testWidgets('Material implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Material( color: Color(0xFFFFFFFF), @@ -123,7 +122,7 @@ void main() { ]); }); - testWidgets('LayoutChangedNotification test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutChangedNotification test', (WidgetTester tester) async { await tester.pumpWidget( const Material( child: NotifyMaterial(), @@ -131,7 +130,7 @@ void main() { ); }); - testWidgets('ListView scroll does not repaint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView scroll does not repaint', (WidgetTester tester) async { final List<Size> log = <Size>[]; await tester.pumpWidget( @@ -190,7 +189,7 @@ void main() { expect(log, isEmpty); }); - testWidgets('Shadow color defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shadow color defaults', (WidgetTester tester) async { Widget buildWithShadow(Color? shadowColor) { return Center( child: SizedBox( @@ -242,7 +241,7 @@ void main() { expect(getModel(tester).shadowColor, Colors.transparent); }); - testWidgets('Shadows animate smoothly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shadows animate smoothly', (WidgetTester tester) async { // This code verifies that the PhysicalModel's elevation animates over // a kThemeChangeDuration time interval. @@ -267,7 +266,7 @@ void main() { expect(modelE.elevation, equals(9.0)); }); - testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shadow colors animate smoothly', (WidgetTester tester) async { // This code verifies that the PhysicalModel's shadowColor animates over // a kThemeChangeDuration time interval. @@ -292,7 +291,7 @@ void main() { expect(modelE.shadowColor, equals(const Color(0xFFFF0000))); }); - testWidgets('Transparent material widget does not absorb hit test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Transparent material widget does not absorb hit test', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/58665. bool pressed = false; await tester.pumpWidget( @@ -323,7 +322,7 @@ void main() { }); group('Surface Tint Overlay', () { - testWidgets('applyElevationOverlayColor does not effect anything with useMaterial3 set to true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applyElevationOverlayColor does not effect anything with useMaterial3 set to true', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF121212); await tester.pumpWidget(Theme( data: ThemeData( @@ -337,7 +336,7 @@ void main() { expect(model.color, equals(surfaceColor)); }); - testWidgets('surfaceTintColor is used to as an overlay to indicate elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('surfaceTintColor is used to as an overlay to indicate elevation', (WidgetTester tester) async { const Color baseColor = Color(0xFF121212); const Color surfaceTintColor = Color(0xff44CCFF); @@ -400,7 +399,7 @@ void main() { group('Elevation Overlay M2', () { // These tests only apply to the Material 2 overlay mechanism. This group // can be removed after migration to Material 3 is complete. - testWidgets('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applyElevationOverlayColor set to false does not change surface color', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF121212); await tester.pumpWidget(Theme( data: ThemeData( @@ -414,7 +413,7 @@ void main() { expect(model.color, equals(surfaceColor)); }); - testWidgets('applyElevationOverlayColor set to true applies a semi-transparent onSurface color to the surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applyElevationOverlayColor set to true applies a semi-transparent onSurface color to the surface color', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF121212); const Color onSurfaceColor = Colors.greenAccent; @@ -456,7 +455,7 @@ void main() { } }); - testWidgets('overlay will not apply to materials using a non-surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlay will not apply to materials using a non-surface color', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData( @@ -475,7 +474,7 @@ void main() { expect(model.color, equals(Colors.cyan)); }); - testWidgets('overlay will not apply to materials using a light theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlay will not apply to materials using a light theme', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData( @@ -494,7 +493,7 @@ void main() { expect(model.color, equals(Colors.cyan)); }); - testWidgets('overlay will apply to materials with a non-opaque surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlay will apply to materials with a non-opaque surface color', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF121212); const Color surfaceColorWithOverlay = Color(0xC6353535); @@ -517,7 +516,7 @@ void main() { expect(model.color, isNot(equals(surfaceColor))); }); - testWidgets('Expected overlay color can be computed using colorWithOverlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Expected overlay color can be computed using colorWithOverlay', (WidgetTester tester) async { const Color surfaceColor = Color(0xFF123456); const Color onSurfaceColor = Color(0xFF654321); const double elevation = 8.0; @@ -550,7 +549,7 @@ void main() { }); // Elevation Overlay M2 group group('Transparency clipping', () { - testWidgets('No clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No clip by default', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -564,7 +563,7 @@ void main() { expect(renderClip.clipBehavior, equals(Clip.none)); }); - testWidgets('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clips to bounding rect by default given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -578,7 +577,7 @@ void main() { expect(find.byKey(materialKey), clipsWithBoundingRect); }); - testWidgets('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clips to rounded rect when borderRadius provided given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -598,7 +597,7 @@ void main() { ); }); - testWidgets('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clips to shape when provided given Clip.antiAlias', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -618,7 +617,7 @@ void main() { ); }); - testWidgets('supports directional clips', (WidgetTester tester) async { + testWidgetsWithLeakTracking('supports directional clips', (WidgetTester tester) async { final List<String> logs = <String>[]; final ShapeBorder shape = TestBorder((String message) { logs.add(message); }); Widget buildMaterial() { @@ -683,7 +682,7 @@ void main() { }); group('PhysicalModels', () { - testWidgets('canvas', (WidgetTester tester) async { + testWidgetsWithLeakTracking('canvas', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -699,7 +698,7 @@ void main() { )); }); - testWidgets('canvas with borderRadius and elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('canvas with borderRadius and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -717,7 +716,7 @@ void main() { )); }); - testWidgets('canvas with shape and elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('canvas with shape and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -734,7 +733,7 @@ void main() { )); }); - testWidgets('card', (WidgetTester tester) async { + testWidgetsWithLeakTracking('card', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -751,7 +750,7 @@ void main() { )); }); - testWidgets('card with borderRadius and elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('card with borderRadius and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -770,7 +769,7 @@ void main() { )); }); - testWidgets('card with shape and elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('card with shape and elevation', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -788,7 +787,7 @@ void main() { )); }); - testWidgets('circle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('circle', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -805,7 +804,7 @@ void main() { )); }); - testWidgets('button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('button', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -823,7 +822,7 @@ void main() { )); }); - testWidgets('button with elevation and borderRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('button with elevation and borderRadius', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -843,7 +842,7 @@ void main() { )); }); - testWidgets('button with elevation and shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('button with elevation and shape', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -864,7 +863,7 @@ void main() { }); group('Border painting', () { - testWidgets('border is painted on physical layers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('border is painted on physical layers', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -885,7 +884,7 @@ void main() { expect(box, paints..circle()); }); - testWidgets('border is painted for transparent material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('border is painted for transparent material', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -905,7 +904,7 @@ void main() { expect(box, paints..circle()); }); - testWidgets('border is not painted for when border side is none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('border is not painted for when border side is none', (WidgetTester tester) async { final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget( Material( @@ -920,7 +919,7 @@ void main() { expect(box, isNot(paints..circle())); }); - testWidgets('border is painted above child by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - border is painted above child by default', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( @@ -955,11 +954,50 @@ void main() { await expectLater( find.byKey(painterKey), - matchesGoldenFile('material.border_paint_above.png'), + matchesGoldenFile('m2_material.border_paint_above.png'), + ); + }); + + testWidgetsWithLeakTracking('Material3 - border is painted above child by default', (WidgetTester tester) async { + final Key painterKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: RepaintBoundary( + key: painterKey, + child: Card( + child: SizedBox( + width: 200, + height: 300, + child: Material( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + side: BorderSide(color: Colors.grey, width: 6), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Column( + children: <Widget>[ + Container( + color: Colors.green, + height: 150, + ), + ], + ), + ), + ), + ), + ), + ), + )); + + await expectLater( + find.byKey(painterKey), + matchesGoldenFile('m3_material.border_paint_above.png'), ); }); - testWidgets('border is painted below child when specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - border is painted below child when specified', (WidgetTester tester) async { final Key painterKey = UniqueKey(); await tester.pumpWidget(MaterialApp( @@ -995,12 +1033,52 @@ void main() { await expectLater( find.byKey(painterKey), - matchesGoldenFile('material.border_paint_below.png'), + matchesGoldenFile('m2_material.border_paint_below.png'), + ); + }); + + testWidgetsWithLeakTracking('Material3 - border is painted below child when specified', (WidgetTester tester) async { + final Key painterKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: RepaintBoundary( + key: painterKey, + child: Card( + child: SizedBox( + width: 200, + height: 300, + child: Material( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + side: BorderSide(color: Colors.grey, width: 6), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + borderOnForeground: false, + child: Column( + children: <Widget>[ + Container( + color: Colors.green, + height: 150, + ), + ], + ), + ), + ), + ), + ), + ), + )); + + await expectLater( + find.byKey(painterKey), + matchesGoldenFile('m3_material.border_paint_below.png'), ); }); }); - testWidgets('InkFeature skips painting if intermediate node skips', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InkFeature skips painting if intermediate node skips', (WidgetTester tester) async { final GlobalKey sizedBoxKey = GlobalKey(); final GlobalKey materialKey = GlobalKey(); await tester.pumpWidget(Material( @@ -1018,8 +1096,11 @@ void main() { controller.addInkFeature(tracker); expect(tracker.paintCount, 0); + final ContainerLayer layer1 = ContainerLayer(); + addTearDown(layer1.dispose); + // Force a repaint. Since it's offstage, the ink feature should not get painted. - materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero); + materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(layer1, Rect.largest), Offset.zero); expect(tracker.paintCount, 0); await tester.pumpWidget(Material( @@ -1033,13 +1114,16 @@ void main() { // now onstage. expect(tracker.paintCount, 1); + final ContainerLayer layer2 = ContainerLayer(); + addTearDown(layer2.dispose); + // Force a repaint again. This time, it gets repainted because it is onstage. - materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero); + materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(layer2, Rect.largest), Offset.zero); expect(tracker.paintCount, 2); }); group('LookupBoundary', () { - testWidgets('hides Material from Material.maybeOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hides Material from Material.maybeOf', (WidgetTester tester) async { MaterialInkController? material; await tester.pumpWidget( @@ -1058,7 +1142,7 @@ void main() { expect(material, isNull); }); - testWidgets('hides Material from Material.of', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hides Material from Material.of', (WidgetTester tester) async { await tester.pumpWidget( Material( child: LookupBoundary( @@ -1090,7 +1174,7 @@ void main() { ); }); - testWidgets('hides Material from debugCheckHasMaterial', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hides Material from debugCheckHasMaterial', (WidgetTester tester) async { await tester.pumpWidget( Material( child: LookupBoundary( diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart index e48e855ff1d97..19e5b2e72c272 100644 --- a/packages/flutter/test/material/menu_anchor_test.dart +++ b/packages/flutter/test/material/menu_anchor_test.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -81,6 +81,7 @@ void main() { TextDirection textDirection = TextDirection.ltr, }) { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( @@ -143,7 +144,7 @@ void main() { ); } - testWidgets('Menu responds to density changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Menu responds to density changes', (WidgetTester tester) async { Widget buildMenu({VisualDensity? visualDensity = VisualDensity.standard}) { return MaterialApp( theme: ThemeData(visualDensity: visualDensity, useMaterial3: false), @@ -238,7 +239,7 @@ void main() { ); }); - testWidgets('menu defaults colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Menu defaults', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); await tester.pumpWidget( MaterialApp( @@ -279,6 +280,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); // vertical menu await tester.tap(find.text(TestMenu.mainMenu1.label)); @@ -306,6 +309,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); await tester.tap(find.text(TestMenu.mainMenu0.label)); await tester.pump(); @@ -316,7 +321,7 @@ void main() { expect(iconRichText.text.style?.color, themeData.colorScheme.onSurfaceVariant); }); - testWidgets('menu defaults - disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Menu defaults - disabled', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); await tester.pumpWidget( MaterialApp( @@ -391,7 +396,7 @@ void main() { expect(iconRichText.text.style?.color, themeData.colorScheme.onSurface.withOpacity(0.38)); }); - testWidgets('Menu scrollbar inherits ScrollbarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Menu scrollbar inherits ScrollbarTheme', (WidgetTester tester) async { const ScrollbarThemeData scrollbarTheme = ScrollbarThemeData( thumbColor: MaterialStatePropertyAll<Color?>(Color(0xffff0000)), thumbVisibility: MaterialStatePropertyAll<bool?>(true), @@ -486,8 +491,56 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); + testWidgetsWithLeakTracking('focus is returned to previous focus before invoking onPressed', (WidgetTester tester) async { + final FocusNode buttonFocus = FocusNode(debugLabel: 'Button Focus'); + addTearDown(buttonFocus.dispose); + FocusNode? focusInOnPressed; + + void onMenuSelected(TestMenu item) { + focusInOnPressed = FocusManager.instance.primaryFocus; + } + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Column( + children: <Widget>[ + MenuBar( + controller: controller, + children: createTestMenus( + onPressed: onMenuSelected, + ), + ), + ElevatedButton( + autofocus: true, + onPressed: () {}, + focusNode: buttonFocus, + child: const Text('Press Me'), + ), + ], + ), + ), + ), + ); + + await tester.pump(); + expect(FocusManager.instance.primaryFocus, equals(buttonFocus)); + + await tester.tap(find.text(TestMenu.mainMenu1.label)); + await tester.pump(); + + await tester.tap(find.text(TestMenu.subMenu11.label)); + await tester.pump(); + + await tester.tap(find.text(TestMenu.subSubMenu110.label)); + await tester.pump(); + + expect(focusInOnPressed, equals(buttonFocus)); + expect(FocusManager.instance.primaryFocus, equals(buttonFocus)); + }); + group('Menu functions', () { - testWidgets('basic menu structure', (WidgetTester tester) async { + testWidgetsWithLeakTracking('basic menu structure', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -540,7 +593,7 @@ void main() { expect(opened.last, equals(TestMenu.subMenu11)); }); - testWidgets('geometry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('geometry', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -605,7 +658,7 @@ void main() { ); }); - testWidgets('geometry with RTL direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('geometry with RTL direction', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -682,7 +735,7 @@ void main() { ); }); - testWidgets('menu alignment and offset in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menu alignment and offset in LTR', (WidgetTester tester) async { await tester.pumpWidget(buildTestApp()); final Rect buttonRect = tester.getRect(find.byType(ElevatedButton)); @@ -725,7 +778,7 @@ void main() { ); }); - testWidgets('menu alignment and offset in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menu alignment and offset in RTL', (WidgetTester tester) async { await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl)); final Rect buttonRect = tester.getRect(find.byType(ElevatedButton)); @@ -767,7 +820,7 @@ void main() { expect(tester.getRect(findMenuScope).topLeft - menuRect.topLeft, equals(const Offset(-10, 20))); }); - testWidgets('menu position in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menu position in LTR', (WidgetTester tester) async { await tester.pumpWidget(buildTestApp(alignmentOffset: const Offset(100, 50))); final Rect buttonRect = tester.getRect(find.byType(ElevatedButton)); @@ -788,7 +841,7 @@ void main() { expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(526.0, 476.0, 800.0, 588.0))); }); - testWidgets('menu position in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menu position in RTL', (WidgetTester tester) async { await tester.pumpWidget(buildTestApp( alignmentOffset: const Offset(100, 50), textDirection: TextDirection.rtl, @@ -813,7 +866,7 @@ void main() { expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(526.0, 476.0, 800.0, 588.0))); }); - testWidgets('works with Padding around menu and overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with Padding around menu and overlay', (WidgetTester tester) async { await tester.pumpWidget( Padding( padding: const EdgeInsets.all(10.0), @@ -866,7 +919,7 @@ void main() { expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0))); }); - testWidgets('works with Padding around menu and overlay with RTL direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with Padding around menu and overlay with RTL direction', (WidgetTester tester) async { await tester.pumpWidget( Padding( padding: const EdgeInsets.all(10.0), @@ -922,7 +975,7 @@ void main() { expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0))); }); - testWidgets('visual attributes can be set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('visual attributes can be set', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -953,7 +1006,7 @@ void main() { expect(material.color, equals(Colors.red)); }); - testWidgets('MenuAnchor clip behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MenuAnchor clip behavior', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1014,7 +1067,7 @@ void main() { expect(getMenuBarMaterial(tester).clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('open and close works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('open and close works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1063,7 +1116,7 @@ void main() { expect(closed, equals(<TestMenu>[TestMenu.mainMenu1])); }); - testWidgets('select works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1098,7 +1151,7 @@ void main() { expect(find.text(TestMenu.subMenu11.label), findsNothing); }); - testWidgets('diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('diagnostics', (WidgetTester tester) async { const MenuItemButton item = MenuItemButton( shortcut: SingleActivator(LogicalKeyboardKey.keyA), child: Text('label2'), @@ -1137,7 +1190,7 @@ void main() { ); }); - testWidgets('keyboard tab traversal works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard tab traversal works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1196,7 +1249,7 @@ void main() { expect(closed, <TestMenu>[TestMenu.mainMenu0]); }); - testWidgets('keyboard directional traversal works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard directional traversal works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1276,7 +1329,7 @@ void main() { expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))')); }); - testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard directional traversal works in RTL mode', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -1362,7 +1415,7 @@ void main() { expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))')); }); - testWidgets('hover traversal works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hover traversal works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1418,8 +1471,9 @@ void main() { expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))')); }); - testWidgets('menus close on ancestor scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menus close on ancestor scroll', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -1456,9 +1510,10 @@ void main() { expect(closed, isNotEmpty); }); - testWidgets('menus do not close on root menu internal scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menus do not close on root menu internal scroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/122168. final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); bool rootOpened = false; await tester.pumpWidget( @@ -1528,8 +1583,9 @@ void main() { expect(closed, isNotEmpty); }); - testWidgets('menus close on view size change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('menus close on view size change', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final MediaQueryData mediaQueryData = MediaQueryData.fromView(tester.view); Widget build(Size size) { @@ -1618,7 +1674,7 @@ void main() { } }); - testWidgets('can invoke menu items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can invoke menu items', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1674,7 +1730,7 @@ void main() { selected.clear(); }, variant: TargetPlatformVariant(nonApple)); - testWidgets('can combine with regular keyboard navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can combine with regular keyboard navigation', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1711,7 +1767,7 @@ void main() { expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110])); }, variant: TargetPlatformVariant(nonApple)); - testWidgets('can combine with mouse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can combine with mouse', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1746,7 +1802,7 @@ void main() { expect(selected, equals(<TestMenu>[TestMenu.subSubMenu112])); }, variant: TargetPlatformVariant(nonApple)); - testWidgets("disabled items don't respond to accelerators", (WidgetTester tester) async { + testWidgetsWithLeakTracking("disabled items don't respond to accelerators", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1778,7 +1834,7 @@ void main() { expect(find.text(TestMenu.subMenu00.label), findsNothing); }, variant: TargetPlatformVariant(nonApple)); - testWidgets("Apple platforms don't react to accelerators", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Apple platforms don't react to accelerators", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1824,7 +1880,7 @@ void main() { }); group('MenuController', () { - testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving a controller to a new instance works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1862,7 +1918,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('closing via controller works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('closing via controller works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1905,7 +1961,7 @@ void main() { }); group('MenuItemButton', () { - testWidgets('Shortcut mnemonics are displayed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcut mnemonics are displayed', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -2025,7 +2081,7 @@ void main() { expect(mnemonic2.data, equals('↵')); }, variant: TargetPlatformVariant.all()); - testWidgets('leadingIcon is used when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('leadingIcon is used when set', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -2053,7 +2109,7 @@ void main() { expect(find.text('leadingIcon'), findsOneWidget); }); - testWidgets('trailingIcon is used when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trailingIcon is used when set', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -2081,7 +2137,7 @@ void main() { expect(find.text('trailingIcon'), findsOneWidget); }); - testWidgets('SubmenuButton uses supplied controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SubmenuButton uses supplied controller', (WidgetTester tester) async { final MenuController submenuController = MenuController(); await tester.pumpWidget( MaterialApp( @@ -2138,7 +2194,7 @@ void main() { expect(find.text(TestMenu.subMenu00.label), findsNothing); }); - testWidgets('diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('diagnostics', (WidgetTester tester) async { final ButtonStyle style = ButtonStyle( shape: MaterialStateProperty.all<OutlinedBorder?>(const StadiumBorder()), elevation: MaterialStateProperty.all<double?>(10.0), @@ -2198,7 +2254,7 @@ void main() { ); }); - testWidgets('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MenuItemButton respects closeOnActivate property', (WidgetTester tester) async { final MenuController controller = MenuController(); await tester.pumpWidget(MaterialApp( home: Material( @@ -2296,7 +2352,7 @@ void main() { return menuRects; } - testWidgets('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( MaterialApp( @@ -2340,7 +2396,7 @@ void main() { ); }); - testWidgets('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( MaterialApp( @@ -2387,7 +2443,7 @@ void main() { ); }); - testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constrained menus show up in the right place in LTR', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(300, 300)); await tester.pumpWidget( MaterialApp( @@ -2432,7 +2488,7 @@ void main() { ); }); - testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constrained menus show up in the right place in RTL', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(300, 300)); await tester.pumpWidget( MaterialApp( @@ -2477,7 +2533,7 @@ void main() { ); }); - testWidgets('constrained menus show up in the right place with offset in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constrained menus show up in the right place with offset in LTR', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( MaterialApp( @@ -2554,7 +2610,7 @@ void main() { ); }); - testWidgets('constrained menus show up in the right place with offset in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('constrained menus show up in the right place with offset in RTL', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( MaterialApp( @@ -2631,7 +2687,7 @@ void main() { ); }); - testWidgets('vertically constrained menus are positioned above the anchor by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('vertically constrained menus are positioned above the anchor by default', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( MaterialApp( @@ -2682,7 +2738,7 @@ void main() { ); }); - testWidgets('vertically constrained menus are positioned above the anchor with the provided offset', + testWidgetsWithLeakTracking('vertically constrained menus are positioned above the anchor with the provided offset', (WidgetTester tester) async { await changeSurfaceSize(tester, const Size(800, 600)); await tester.pumpWidget( @@ -2769,7 +2825,7 @@ void main() { await tester.pump(); } - testWidgets('submenus account for density in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for density in LTR', (WidgetTester tester) async { await buildDensityPaddingApp( tester, textDirection: TextDirection.ltr, @@ -2784,7 +2840,7 @@ void main() { ); }); - testWidgets('submenus account for menu density in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for menu density in RTL', (WidgetTester tester) async { await buildDensityPaddingApp( tester, textDirection: TextDirection.rtl, @@ -2799,7 +2855,7 @@ void main() { ); }); - testWidgets('submenus account for compact menu density in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for compact menu density in LTR', (WidgetTester tester) async { await buildDensityPaddingApp( tester, visualDensity: VisualDensity.compact, @@ -2815,7 +2871,7 @@ void main() { ); }); - testWidgets('submenus account for compact menu density in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for compact menu density in RTL', (WidgetTester tester) async { await buildDensityPaddingApp( tester, visualDensity: VisualDensity.compact, @@ -2831,7 +2887,7 @@ void main() { ); }); - testWidgets('submenus account for padding in LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for padding in LTR', (WidgetTester tester) async { await buildDensityPaddingApp( tester, menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13), @@ -2847,7 +2903,7 @@ void main() { ); }); - testWidgets('submenus account for padding in RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('submenus account for padding in RTL', (WidgetTester tester) async { await buildDensityPaddingApp( tester, menuPadding: const EdgeInsetsDirectional.only(start: 10, end: 11, top: 12, bottom: 13), @@ -2865,7 +2921,7 @@ void main() { }); group('LocalizedShortcutLabeler', () { - testWidgets('getShortcutLabel returns the right labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getShortcutLabel returns the right labels', (WidgetTester tester) async { String expectedMeta; String expectedCtrl; String expectedAlt; @@ -2897,7 +2953,17 @@ void main() { shift: true, alt: true, ); - final String allExpected = <String>[expectedAlt, expectedCtrl, expectedMeta, expectedShift, 'A'].join(expectedSeparator); + late String allExpected; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + allExpected = <String>[expectedAlt, expectedCtrl, expectedMeta, expectedShift, 'A'].join(expectedSeparator); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + allExpected = <String>[expectedCtrl, expectedAlt, expectedShift, expectedMeta, 'A'].join(expectedSeparator); + } const CharacterActivator charShortcuts = CharacterActivator('ñ'); const String charExpected = 'ñ'; await tester.pumpWidget( @@ -2933,7 +2999,7 @@ void main() { }); group('CheckboxMenuButton', () { - testWidgets('tapping toggles checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapping toggles checkbox', (WidgetTester tester) async { bool? checkBoxValue; await tester.pumpWidget( MaterialApp( @@ -2987,7 +3053,7 @@ void main() { }); group('RadioMenuButton', () { - testWidgets('tapping toggles radio button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapping toggles radio button', (WidgetTester tester) async { int? radioValue; await tester.pumpWidget( MaterialApp( @@ -3056,7 +3122,7 @@ void main() { }); group('Semantics', () { - testWidgets('MenuItemButton is not a semantic button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MenuItemButton is not a semantic button', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -3064,7 +3130,7 @@ void main() { child: Center( child: MenuItemButton( style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)), - onPressed: () { }, + onPressed: () {}, child: const Text('ABC'), ), ), @@ -3072,32 +3138,35 @@ void main() { ); // The flags should not have SemanticsFlag.isButton - expect(semantics, hasSemantics( - TestSemantics.root( - children: <TestSemantics>[ - TestSemantics.rootChild( - actions: <SemanticsAction>[ - SemanticsAction.tap, - ], - label: 'ABC', - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - transform: Matrix4.translationValues(356.0, 276.0, 0.0), - flags: <SemanticsFlag>[ - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - SemanticsFlag.isFocusable, - ], - textDirection: TextDirection.ltr, - ), - ], + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: <TestSemantics>[ + TestSemantics.rootChild( + actions: <SemanticsAction>[ + SemanticsAction.tap, + ], + label: 'ABC', + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + transform: Matrix4.translationValues(356.0, 276.0, 0.0), + flags: <SemanticsFlag>[ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, + ], + textDirection: TextDirection.ltr, + ), + ], + ), + ignoreId: true, ), - ignoreId: true, - )); + ); semantics.dispose(); }); - testWidgets('SubMenuButton is not a semantic button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SubMenuButton is not a semantic button', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -3114,26 +3183,210 @@ void main() { ); // The flags should not have SemanticsFlag.isButton - expect(semantics, hasSemantics( - TestSemantics.root( - children: <TestSemantics>[ - TestSemantics.rootChild( - label: 'ABC', - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - transform: Matrix4.translationValues(356.0, 276.0, 0.0), - flags: <SemanticsFlag>[ - SemanticsFlag.hasEnabledState, + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: <TestSemantics>[ + TestSemantics( + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, + SemanticsFlag.hasExpandedState], + label: 'ABC', + textDirection: TextDirection.ltr, + ), + ], + ), + ignoreTransform: true, + ignoreId: true, + ), + ); + + semantics.dispose(); + }); + + testWidgetsWithLeakTracking('SubmenuButton expanded/collapsed state', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget( + MaterialApp( + home: Center( + child: SubmenuButton( + onHover: (bool value) {}, + style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), + menuChildren: <Widget>[ + MenuItemButton( + style: SubmenuButton.styleFrom(fixedSize: const Size(120.0, 36.0)), + child: const Text('Item 0'), + onPressed: () {}, + ), ], - textDirection: TextDirection.ltr, + child: const Text('ABC'), ), - ], + ), ), - ignoreId: true, - )); + ); + + // Test expanded state. + await tester.tap(find.text('ABC')); + await tester.pumpAndSettle(); + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: <TestSemantics>[ + TestSemantics( + id: 1, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + children: <TestSemantics> [ + TestSemantics( + id: 2, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + children: <TestSemantics> [ + TestSemantics( + id: 3, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + flags: <SemanticsFlag> [SemanticsFlag.scopesRoute], + children: <TestSemantics> [ + TestSemantics( + id: 4, + flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isExpanded, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable], + actions: <SemanticsAction>[SemanticsAction.tap], + label: 'ABC', + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + ) + ] + ) + ] + ), + TestSemantics( + id: 6, + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0), + children: <TestSemantics> [ + TestSemantics( + id: 7, + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), + flags: <SemanticsFlag> [SemanticsFlag.hasImplicitScrolling], + children: <TestSemantics> [ + TestSemantics( + id: 8, + label: 'Item 0', + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), + flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable], + actions: <SemanticsAction>[SemanticsAction.tap], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ignoreTransform: true, + ), + ); + + // Test collapsed state. + await tester.tap(find.text('ABC')); + await tester.pumpAndSettle(); + expect( + semantics, + hasSemantics( + TestSemantics.root( + children: <TestSemantics>[ + TestSemantics( + id: 1, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + children: <TestSemantics> [ + TestSemantics( + id: 2, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + children: <TestSemantics> [ + TestSemantics( + id: 3, + rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + flags: <SemanticsFlag> [SemanticsFlag.scopesRoute], + children: <TestSemantics> [ + TestSemantics( + id: 4, + flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable], + actions: <SemanticsAction>[SemanticsAction.tap], + label: 'ABC', + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + ) + ] + ) + ] + ), + ], + ), + ], + ), + ignoreTransform: true, + ), + ); semantics.dispose(); }); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/131676. + testWidgetsWithLeakTracking('Material3 - Menu uses correct text styles', (WidgetTester tester) async { + const TextStyle menuTextStyle = TextStyle( + fontSize: 18.5, + fontStyle: FontStyle.italic, + wordSpacing: 1.2, + decoration: TextDecoration.lineThrough, + ); + final ThemeData themeData = ThemeData( + textTheme: const TextTheme( + labelLarge: menuTextStyle, + ) + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Material( + child: MenuBar( + controller: controller, + children: createTestMenus( + onPressed: onPressed, + onOpen: onOpen, + onClose: onClose, + ), + ), + ), + ), + ); + + // Test menu button text style uses the TextTheme.labelLarge. + Finder buttonMaterial = find.descendant( + of: find.byType(TextButton), + matching: find.byType(Material), + ).first; + Material material = tester.widget<Material>(buttonMaterial); + expect(material.textStyle?.fontSize, menuTextStyle.fontSize); + expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing); + expect(material.textStyle?.decoration, menuTextStyle.decoration); + + // Open the menu. + await tester.tap(find.text(TestMenu.mainMenu1.label)); + await tester.pump(); + + // Test menu item text style uses the TextTheme.labelLarge. + buttonMaterial = find.descendant( + of: find.widgetWithText(TextButton, TestMenu.subMenu10.label), + matching: find.byType(Material), + ).first; + material = tester.widget<Material>(buttonMaterial); + expect(material.textStyle?.fontSize, menuTextStyle.fontSize); + expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing); + expect(material.textStyle?.decoration, menuTextStyle.decoration); + }); } List<Widget> createTestMenus({ diff --git a/packages/flutter/test/material/menu_bar_theme_test.dart b/packages/flutter/test/material/menu_bar_theme_test.dart index 003ff987ed27c..b5dafecaec210 100644 --- a/packages/flutter/test/material/menu_bar_theme_test.dart +++ b/packages/flutter/test/material/menu_bar_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { void onPressed(TestMenu item) {} @@ -52,7 +53,7 @@ void main() { expect(identical(MenuBarThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('theme is honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('theme is honored', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -105,7 +106,7 @@ void main() { expect(subMenuMaterial.color, equals(Colors.green)); }); - testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Constructor parameters override theme parameters', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), diff --git a/packages/flutter/test/material/menu_style_test.dart b/packages/flutter/test/material/menu_style_test.dart index 9f1de88855344..c3fc058976de0 100644 --- a/packages/flutter/test/material/menu_style_test.dart +++ b/packages/flutter/test/material/menu_style_test.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + void main() { Finder findMenuPanels() { @@ -41,7 +44,7 @@ void main() { expect(identical(MenuStyle.lerp(data, data, 0.5), data), true); }); - testWidgets('fixedSize affects geometry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fixedSize affects geometry', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -80,11 +83,13 @@ void main() { expect(tester.getRect(findMenuPanels().first).size, equals(const Size(600.0, 60.0))); // MenuTheme affects menus. - expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0))); - expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0))); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0))); + expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0))); + } }); - testWidgets('maximumSize affects geometry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('maximumSize affects geometry', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -127,7 +132,7 @@ void main() { expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0))); }); - testWidgets('minimumSize affects geometry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('minimumSize affects geometry', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -170,7 +175,7 @@ void main() { expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(300.0, 300.0))); }); - testWidgets('Material parameters are honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material parameters are honored', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -236,7 +241,7 @@ void main() { expect(panelPadding.padding, equals(const EdgeInsets.all(20))); }); - testWidgets('visual density', (WidgetTester tester) async { + testWidgetsWithLeakTracking('visual density', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), diff --git a/packages/flutter/test/material/menu_theme_test.dart b/packages/flutter/test/material/menu_theme_test.dart index f6fb5d324e3e4..49089f724ce4d 100644 --- a/packages/flutter/test/material/menu_theme_test.dart +++ b/packages/flutter/test/material/menu_theme_test.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + void main() { void onPressed(TestMenu item) {} @@ -52,7 +54,7 @@ void main() { expect(identical(MenuThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('theme is honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('theme is honored', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -105,7 +107,8 @@ void main() { expect(subMenuMaterial.color, equals(Colors.red)); }); - testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Constructor parameters override theme parameters', + (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), diff --git a/packages/flutter/test/material/mergeable_material_test.dart b/packages/flutter/test/material/mergeable_material_test.dart index 08b3fc735da94..19539527b4f7c 100644 --- a/packages/flutter/test/material/mergeable_material_test.dart +++ b/packages/flutter/test/material/mergeable_material_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; enum RadiusType { Sharp, @@ -65,7 +64,7 @@ BorderRadius? getBorderRadius(WidgetTester tester, int index) { } void main() { - testWidgets('MergeableMaterial empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial empty', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -80,7 +79,7 @@ void main() { expect(box.size.height, equals(0)); }); - testWidgets('MergeableMaterial update slice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial update slice', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -128,7 +127,7 @@ void main() { expect(box.size.height, equals(200.0)); }); - testWidgets('MergeableMaterial swap slices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial swap slices', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -199,7 +198,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Sharp, RadiusType.Round); }); - testWidgets('MergeableMaterial paints shadows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial paints shadows', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget( MaterialApp( @@ -234,7 +233,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('MergeableMaterial skips shadow for zero elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial skips shadow for zero elevation', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget( const MaterialApp( @@ -264,7 +263,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('MergeableMaterial merge gap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial merge gap', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -341,7 +340,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Sharp, RadiusType.Round); }); - testWidgets('MergeableMaterial separate slices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial separate slices', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -418,7 +417,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Round, RadiusType.Round); }); - testWidgets('MergeableMaterial separate merge separate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial separate merge separate', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -577,7 +576,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Round, RadiusType.Round); }); - testWidgets('MergeableMaterial insert slice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial insert slice', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -652,7 +651,7 @@ void main() { matches(getBorderRadius(tester, 2), RadiusType.Sharp, RadiusType.Round); }); - testWidgets('MergeableMaterial remove slice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial remove slice', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -728,7 +727,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Sharp, RadiusType.Round); }); - testWidgets('MergeableMaterial insert chunk', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial insert chunk', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -817,7 +816,7 @@ void main() { matches(getBorderRadius(tester, 2), RadiusType.Round, RadiusType.Round); }); - testWidgets('MergeableMaterial remove chunk', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial remove chunk', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -905,7 +904,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Sharp, RadiusType.Round); }); - testWidgets('MergeableMaterial replace gap with chunk', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial replace gap with chunk', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -997,7 +996,7 @@ void main() { matches(getBorderRadius(tester, 2), RadiusType.Round, RadiusType.Round); }); - testWidgets('MergeableMaterial replace chunk with gap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial replace chunk with gap', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -1088,7 +1087,7 @@ void main() { matches(getBorderRadius(tester, 1), RadiusType.Round, RadiusType.Round); }); - testWidgets('MergeableMaterial insert and separate slice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial insert and separate slice', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -1168,7 +1167,7 @@ void main() { ); } - testWidgets('MergeableMaterial dividers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial dividers', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1293,7 +1292,7 @@ void main() { expect(isDivider(boxes[offset + 3], true, false), isTrue); }); - testWidgets('MergeableMaterial respects dividerColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial respects dividerColor', (WidgetTester tester) async { const Color dividerColor = Colors.red; await tester.pumpWidget( const MaterialApp( @@ -1330,7 +1329,7 @@ void main() { expect(decoration.border!.top.color, dividerColor); }); - testWidgets('MergeableMaterial respects MaterialSlice.color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeableMaterial respects MaterialSlice.color', (WidgetTester tester) async { const Color themeCardColor = Colors.red; const Color materialSliceColor = Colors.green; diff --git a/packages/flutter/test/material/navigation_bar_test.dart b/packages/flutter/test/material/navigation_bar_test.dart index 77e6ecc0d76df..9e01060611f42 100644 --- a/packages/flutter/test/material/navigation_bar_test.dart +++ b/packages/flutter/test/material/navigation_bar_test.dart @@ -12,12 +12,12 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation bar updates destinations when tapped', (WidgetTester tester) async { int mutatedIndex = -1; final Widget widget = _buildWidget( NavigationBar( @@ -49,7 +49,7 @@ void main() { expect(mutatedIndex, 0); }); - testWidgets('NavigationBar can update background color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBar can update background color', (WidgetTester tester) async { const Color color = Colors.yellow; await tester.pumpWidget( @@ -74,7 +74,7 @@ void main() { expect(_getMaterial(tester).color, equals(color)); }); - testWidgets('NavigationBar can update elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBar can update elevation', (WidgetTester tester) async { const double elevation = 42.0; await tester.pumpWidget( @@ -99,7 +99,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(elevation)); }); - testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBar adds bottom padding to height', (WidgetTester tester) async { const double bottomPadding = 40.0; await tester.pumpWidget( @@ -148,7 +148,7 @@ void main() { expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight); }); - testWidgets('NavigationBar respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBar respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { const double safeAreaPadding = 40.0; Widget navigationBar() { return NavigationBar( @@ -246,7 +246,7 @@ void main() { ); }); - testWidgets('NavigationBar uses proper defaults when no parameters are given - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async { // M2 settings that were hand coded. await tester.pumpWidget( _buildWidget( @@ -275,7 +275,7 @@ void main() { expect(_getIndicatorDecoration(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))); }); - testWidgets('NavigationBar uses proper defaults when no parameters are given - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async { // M3 settings from the token database. final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -305,7 +305,7 @@ void main() { expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); }); - testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - NavigationBar shows tooltips with text scaling', (WidgetTester tester) async { const String label = 'A'; Widget buildApp({ required double textScaleFactor }) { @@ -364,7 +364,69 @@ void main() { expect(tester.getSize(find.text(label).last), Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4)); }); - testWidgets('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - NavigationBar shows tooltips with text scaling', (WidgetTester tester) async { + const String label = 'A'; + + Widget buildApp({ required double textScaleFactor }) { + return MediaQuery( + data: MediaQueryData(textScaleFactor: textScaleFactor), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return Scaffold( + bottomNavigationBar: NavigationBar( + destinations: const <NavigationDestination>[ + NavigationDestination( + label: label, + icon: Icon(Icons.ac_unit), + tooltip: label, + ), + NavigationDestination( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp(textScaleFactor: 1.0)); + expect(find.text(label), findsOneWidget); + await tester.longPress(find.text(label)); + expect(find.text(label), findsNWidgets(2)); + + if (!kIsWeb || isCanvasKit) { + expect(tester.getSize(find.text(label).last), const Size(14.25, 20.0)); + } + // The duration is needed to ensure the tooltip disappears. + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await tester.pumpWidget(buildApp(textScaleFactor: 4.0)); + expect(find.text(label), findsOneWidget); + await tester.longPress(find.text(label)); + + if (!kIsWeb || isCanvasKit) { + expect(tester.getSize(find.text(label).last), const Size(56.25, 80.0)); + } + }); + + testWidgetsWithLeakTracking('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -404,7 +466,7 @@ void main() { }); - testWidgets('Navigation bar semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation bar semantics', (WidgetTester tester) async { Widget widget({int selectedIndex = 0}) { return _buildWidget( NavigationBar( @@ -468,7 +530,7 @@ void main() { ); }); - testWidgets('Navigation bar semantics with some labels hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation bar semantics with some labels hidden', (WidgetTester tester) async { Widget widget({int selectedIndex = 0}) { return _buildWidget( NavigationBar( @@ -533,7 +595,7 @@ void main() { ); }); - testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation bar does not grow with text scale factor', (WidgetTester tester) async { const int animationMilliseconds = 800; Widget widget({double textScaleFactor = 1}) { @@ -566,7 +628,7 @@ void main() { expect(newHeight, equals(initialHeight)); }); - testWidgets('Navigation indicator renders ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Navigation indicator renders ripple', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/116751. int selectedIndex = 0; @@ -765,9 +827,9 @@ void main() { color: const Color(0x0a000000), ) ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('Navigation indicator ripple golden test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Navigation indicator ripple golden test', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117420. Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) { @@ -824,7 +886,7 @@ void main() { await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m3.png')); }); - testWidgets('Navigation indicator scale transform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation indicator scale transform', (WidgetTester tester) async { int selectedIndex = 0; Widget buildNavigationBar() { @@ -875,8 +937,8 @@ void main() { expect(transform.getColumn(0)[0], 1.0); }); - testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); + testWidgetsWithLeakTracking('Material3 - Navigation destination updates indicator color and shape', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); const Color color = Color(0xff0000ff); const ShapeBorder shape = RoundedRectangleBorder(); @@ -884,9 +946,112 @@ void main() { return MaterialApp( theme: theme, home: Scaffold( - bottomNavigationBar: NavigationBar( - indicatorColor: indicatorColor, - indicatorShape: indicatorShape, + bottomNavigationBar: RepaintBoundary( + child: NavigationBar( + indicatorColor: indicatorColor, + indicatorShape: indicatorShape, + destinations: const <Widget>[ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) { }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildNavigationBar()); + + // Test default indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer); + expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last)); + await tester.pumpAndSettle(); + + // Test default indicator color and shape with ripple. + await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.default.indicator.inkwell.shape.png')); + + await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape)); + + // Test custom indicator color and shape. + expect(_getIndicatorDecoration(tester)?.color, color); + expect(_getIndicatorDecoration(tester)?.shape, shape); + + // Test custom indicator color and shape with ripple. + await expectLater(find.byType(NavigationBar), matchesGoldenFile('m3.navigation_bar.custom.indicator.inkwell.shape.png')); + }); + + testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async { + int selectedIndex = 0; + + await tester.pumpWidget( + _buildWidget( + NavigationBar( + destinations: const <Widget>[ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + NavigationDestination( + icon: Icon(Icons.bookmark), + label: 'Bookmark', + enabled: false, + ), + ], + onDestinationSelected: (int i) => selectedIndex = i, + selectedIndex: selectedIndex, + ), + ) + ); + + await tester.tap(find.text('AC')); + expect(selectedIndex, 0); + + await tester.tap(find.text('Alarm')); + expect(selectedIndex, 1); + + await tester.tap(find.text('Bookmark')); + expect(selectedIndex, 1); + }); + + testWidgetsWithLeakTracking('NavigationBar respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color hoverColor = Color(0xff0000ff); + const Color focusColor = Color(0xff00ffff); + const Color pressedColor = Color(0xffff00ff); + final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>( + (Set<MaterialState> states) { + if (states.contains(MaterialState.hovered)) { + return hoverColor; + } + if (states.contains(MaterialState.focused)) { + return focusColor; + } + if (states.contains(MaterialState.pressed)) { + return pressedColor; + } + return Colors.transparent; + }); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + bottomNavigationBar: RepaintBoundary( + child: NavigationBar( + overlayColor: overlayColor, destinations: const <Widget>[ NavigationDestination( icon: Icon(Icons.ac_unit), @@ -900,20 +1065,47 @@ void main() { onDestinationSelected: (int i) { }, ), ), - ); - } + ), + )); - await tester.pumpWidget(buildNavigationBar()); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last)); + await tester.pumpAndSettle(); - // Test default indicator color and shape. - expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer); - expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder()); + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); - await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape)); + // Test hovered state. + expect( + inkFeatures, + kIsWeb + ? (paints..rrect()..rrect()..circle(color: hoverColor)) + : (paints..circle(color: hoverColor)), + ); - // Test custom indicator color and shape. - expect(_getIndicatorDecoration(tester)?.color, color); - expect(_getIndicatorDecoration(tester)?.shape, shape); + await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last)); + await tester.pumpAndSettle(); + + // Test pressed state. + expect( + inkFeatures, + kIsWeb + ? (paints..circle()..circle()..circle(color: pressedColor)) + : (paints..circle()..circle(color: pressedColor)), + ); + + await gesture.up(); + await tester.pumpAndSettle(); + + // Press tab to focus the navigation bar. + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + + // Test focused state. + expect( + inkFeatures, + kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)), + ); }); group('Material 2', () { @@ -921,7 +1113,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Navigation destination updates indicator color and shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); const Color color = Color(0xff0000ff); const ShapeBorder shape = RoundedRectangleBorder(); @@ -965,7 +1157,7 @@ void main() { expect(_getIndicatorDecoration(tester)?.shape, shape); }); - testWidgets('Navigation indicator renders ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Navigation indicator renders ripple', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/116751. int selectedIndex = 0; @@ -1166,7 +1358,7 @@ void main() { ); }); - testWidgets('Navigation indicator ripple golden test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Navigation indicator ripple golden test', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117420. Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) { @@ -1223,7 +1415,7 @@ void main() { await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png')); }); - testWidgets('Destination icon does not rebuild when tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination icon does not rebuild when tapped', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/122811. Widget buildNavigationBar() { diff --git a/packages/flutter/test/material/navigation_bar_theme_test.dart b/packages/flutter/test/material/navigation_bar_theme_test.dart index 201b6a246ca23..028c0ffbdeb4b 100644 --- a/packages/flutter/test/material/navigation_bar_theme_test.dart +++ b/packages/flutter/test/material/navigation_bar_theme_test.dart @@ -7,10 +7,13 @@ @Tags(<String>['reduced-test-set']) library; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('copyWith, ==, hashCode basics', () { @@ -24,7 +27,7 @@ void main() { expect(identical(NavigationBarThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Default debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const NavigationBarThemeData().debugFillProperties(builder); @@ -36,7 +39,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('Custom debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const NavigationBarThemeData( height: 200.0, @@ -47,6 +50,7 @@ void main() { labelTextStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000097))), labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, + overlayColor: MaterialStatePropertyAll<Color>(Color(0x00000096)), ).debugFillProperties(builder); final List<String> description = builder.properties @@ -60,15 +64,14 @@ void main() { expect(description[3], 'indicatorColor: Color(0x00000098)'); expect(description[4], 'indicatorShape: CircleBorder(BorderSide(width: 0.0, style: none))'); expect(description[5], 'labelTextStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 7.0))'); - // Ignore instance address for IconThemeData. expect(description[6].contains('iconTheme: MaterialStatePropertyAll(IconThemeData'), isTrue); expect(description[6].contains('(color: Color(0x00000097))'), isTrue); - expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide'); + expect(description[8], 'overlayColor: MaterialStatePropertyAll(Color(0x00000096))'); }); - testWidgets('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async { const double height = 200.0; const Color backgroundColor = Color(0x00000001); const double elevation = 42.0; @@ -140,7 +143,7 @@ void main() { expect(_labelBehavior(tester), labelBehavior); }); - testWidgets('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async { const double height = 200.0; const Color backgroundColor = Color(0x00000001); const double elevation = 42.0; @@ -174,7 +177,7 @@ void main() { expect(_labelBehavior(tester), labelBehavior); }); - testWidgets('Custom label style renders ink ripple properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom label style renders ink ripple properly', (WidgetTester tester) async { Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) { return MaterialApp( theme: ThemeData( @@ -215,6 +218,86 @@ void main() { await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_custom_label_style.png')); }); + + testWidgetsWithLeakTracking('NavigationBar respects NavigationBarTheme.overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color hoverColor = Color(0xff0000ff); + const Color focusColor = Color(0xff00ffff); + const Color pressedColor = Color(0xffff00ff); + final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>( + (Set<MaterialState> states) { + if (states.contains(MaterialState.hovered)) { + return hoverColor; + } + if (states.contains(MaterialState.focused)) { + return focusColor; + } + if (states.contains(MaterialState.pressed)) { + return pressedColor; + } + return Colors.transparent; + }); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(navigationBarTheme: NavigationBarThemeData(overlayColor: overlayColor)), + home: Scaffold( + bottomNavigationBar: RepaintBoundary( + child: NavigationBar( + destinations: const <Widget>[ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) { }, + ), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last)); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + + // Test hovered state. + expect( + inkFeatures, + kIsWeb + ? (paints..rrect()..rrect()..circle(color: hoverColor)) + : (paints..circle(color: hoverColor)), + ); + + await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last)); + await tester.pumpAndSettle(); + + // Test pressed state. + expect( + inkFeatures, + kIsWeb + ? (paints..circle()..circle()..circle(color: pressedColor)) + : (paints..circle()..circle(color: pressedColor)), + ); + + await gesture.up(); + await tester.pumpAndSettle(); + + // Press tab to focus the navigation bar. + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + + // Test focused state. + expect( + inkFeatures, + kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)), + ); + }); } List<NavigationDestination> _destinations() { diff --git a/packages/flutter/test/material/navigation_drawer_test.dart b/packages/flutter/test/material/navigation_drawer_test.dart index cde4e4eb0eace..6df86c5a29cb3 100644 --- a/packages/flutter/test/material/navigation_drawer_test.dart +++ b/packages/flutter/test/material/navigation_drawer_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Navigation drawer updates destinations when tapped', + testWidgetsWithLeakTracking('Navigation drawer updates destinations when tapped', (WidgetTester tester) async { int mutatedIndex = -1; final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -49,7 +50,7 @@ void main() { expect(mutatedIndex, 0); }); - testWidgets('NavigationDrawer can update background color', + testWidgetsWithLeakTracking('NavigationDrawer can update background color', (WidgetTester tester) async { const Color color = Colors.yellow; final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -82,7 +83,7 @@ void main() { expect(_getMaterial(tester).color, equals(color)); }); - testWidgets('NavigationDrawer can update elevation', + testWidgetsWithLeakTracking('NavigationDrawer can update elevation', (WidgetTester tester) async { const double elevation = 42.0; final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -114,7 +115,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(elevation)); }); - testWidgets( + testWidgetsWithLeakTracking( 'NavigationDrawer uses proper defaults when no parameters are given', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -163,7 +164,7 @@ void main() { expect(iconBox.size, const Size(24.0, 24.0)); }); - testWidgets('Navigation drawer is scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation drawer is scrollable', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); widgetSetup(tester, 500, viewHeight: 300); await tester.pumpWidget( @@ -210,7 +211,7 @@ void main() { expect(find.text('Label10'), findsNothing); }); - testWidgets('Safe Area test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe Area test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const double viewHeight = 300; widgetSetup(tester, 500, viewHeight: viewHeight); @@ -251,7 +252,7 @@ void main() { expect(tester.getBottomRight(find.widgetWithText(NavigationDrawerDestination,'Label4')).dy, viewHeight); }); - testWidgets('Navigation drawer semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation drawer semantics', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final ThemeData theme= ThemeData.from(colorScheme: const ColorScheme.light()); Widget widget({int selectedIndex = 0}) { @@ -321,7 +322,7 @@ void main() { ); }); - testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation destination updates indicator color and shape', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final ThemeData theme = ThemeData(useMaterial3: true); const Color color = Color(0xff0000ff); @@ -372,7 +373,7 @@ void main() { expect(_getInkWell(tester)?.customBorder, shape); }); - testWidgets('NavigationDrawer.tilePadding defaults to EdgeInsets.symmetric(horizontal: 12.0)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationDrawer.tilePadding defaults to EdgeInsets.symmetric(horizontal: 12.0)', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); widgetSetup(tester, 3000, viewHeight: 3000); final Widget widget = _buildWidget( @@ -394,6 +395,57 @@ void main() { final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer)); expect(drawer.tilePadding, const EdgeInsets.symmetric(horizontal: 12.0)); }); + + testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + int selectedIndex = 0; + + widgetSetup(tester, 800); + + final Widget widget = _buildWidget( + scaffoldKey, + NavigationDrawer( + children: const <Widget>[ + NavigationDrawerDestination( + icon: Icon(Icons.ac_unit), + label: Text('AC'), + ), + NavigationDrawerDestination( + icon: Icon(Icons.access_alarm), + label: Text('Alarm'), + ), + NavigationDrawerDestination( + icon: Icon(Icons.accessible), + label: Text('Accessible'), + enabled: false, + ), + ], + onDestinationSelected: (int i) { + selectedIndex = i; + }, + ), + ); + + await tester.pumpWidget(widget); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); + + expect(find.text('AC'), findsOneWidget); + expect(find.text('Alarm'), findsOneWidget); + expect(find.text('Accessible'), findsOneWidget); + + await tester.pump(const Duration(seconds: 1)); + + expect(selectedIndex, 0); + + await tester.tap(find.text('Alarm')); + expect(selectedIndex, 1); + + await tester.tap(find.text('Accessible')); + expect(selectedIndex, 1); + + tester.pumpAndSettle(); + }); } Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, { bool? useMaterial3 }) { diff --git a/packages/flutter/test/material/navigation_drawer_theme_test.dart b/packages/flutter/test/material/navigation_drawer_theme_test.dart index 411ad46d8e8ff..f055198c020f2 100644 --- a/packages/flutter/test/material/navigation_drawer_theme_test.dart +++ b/packages/flutter/test/material/navigation_drawer_theme_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('NavigationDrawerThemeData copyWith, ==, hashCode, basics', () { @@ -16,4 +18,294 @@ void main() { const NavigationDrawerThemeData data = NavigationDrawerThemeData(); expect(identical(NavigationDrawerThemeData.lerp(data, data, 0.5), data), true); }); + + testWidgetsWithLeakTracking('Default debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const NavigationDrawerThemeData().debugFillProperties(builder); + + final List<String> description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, <String>[]); + }); + + testWidgetsWithLeakTracking('NavigationDrawerThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const NavigationDrawerThemeData( + tileHeight: 50, + backgroundColor: Color(0x00000099), + elevation: 5.0, + shadowColor: Color(0x00000098), + surfaceTintColor: Color(0x00000097), + indicatorColor: Color(0x00000096), + indicatorShape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + indicatorSize: Size(10, 10), + labelTextStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), + iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000095))), + ).debugFillProperties(builder); + + final List<String> description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, equalsIgnoringHashCodes( + <String>[ + 'tileHeight: 50.0', + 'backgroundColor: Color(0x00000099)', + 'elevation: 5.0', + 'shadowColor: Color(0x00000098)', + 'surfaceTintColor: Color(0x00000097)', + 'indicatorColor: Color(0x00000096)', + 'indicatorShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', + 'indicatorSize: Size(10.0, 10.0)', + 'labelTextStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 7.0))', + 'iconTheme: MaterialStatePropertyAll(IconThemeData#00000(color: Color(0x00000095)))' + ], + )); + }); + + testWidgetsWithLeakTracking( + 'NavigationDrawerThemeData values are used when no NavigationDrawer properties are specified', + (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + const NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerThemeData( + backgroundColor: Color(0x00000001), + elevation: 7.0, + shadowColor: Color(0x00000002), + surfaceTintColor: Color(0x00000003), + indicatorColor: Color(0x00000004), + indicatorShape: RoundedRectangleBorder(borderRadius: BorderRadius.only(topRight: Radius.circular(16.0))), + labelTextStyle:MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), + iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000005))), + ); + + await tester.pumpWidget( + _buildWidget( + scaffoldKey, + NavigationDrawer( + children: const <Widget>[ + Text('Headline'), + NavigationDrawerDestination( + icon: Icon(Icons.ac_unit), + label: Text('AC'), + ), + NavigationDrawerDestination( + icon: Icon(Icons.access_alarm), + label: Text('Alarm'), + ), + ], + onDestinationSelected: (int i) {}, + ), + theme: ThemeData( + navigationDrawerTheme: navigationDrawerTheme, + ), + ), + ); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(seconds: 1)); + + // Test drawer Material. + expect(_getMaterial(tester).color, navigationDrawerTheme.backgroundColor); + expect(_getMaterial(tester).surfaceTintColor, navigationDrawerTheme.surfaceTintColor); + expect(_getMaterial(tester).shadowColor, navigationDrawerTheme.shadowColor); + expect(_getMaterial(tester).elevation, 7); + // Test indicator decoration. + expect(_getIndicatorDecoration(tester)?.color, navigationDrawerTheme.indicatorColor); + expect(_getIndicatorDecoration(tester)?.shape, navigationDrawerTheme.indicatorShape); + // Test icon. + expect( + _iconStyle(tester, Icons.ac_unit)?.color, + navigationDrawerTheme.iconTheme?.resolve(<MaterialState>{})?.color, + ); + expect( + _iconStyle(tester, Icons.access_alarm)?.color, + navigationDrawerTheme.iconTheme?.resolve(<MaterialState>{})?.color, + ); + // Test label. + expect( + _labelStyle(tester, 'AC'), + navigationDrawerTheme.labelTextStyle?.resolve(<MaterialState>{}) + ); + expect( + _labelStyle(tester, 'Alarm'), + navigationDrawerTheme.labelTextStyle?.resolve(<MaterialState>{}) + ); + }); + + testWidgetsWithLeakTracking( + 'NavigationDrawer values take priority over NavigationDrawerThemeData values when both properties are specified', + (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + const NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerThemeData( + backgroundColor: Color(0x00000001), + elevation: 7.0, + shadowColor: Color(0x00000002), + surfaceTintColor: Color(0x00000003), + indicatorColor: Color(0x00000004), + indicatorShape: RoundedRectangleBorder(borderRadius: BorderRadius.only(topRight: Radius.circular(16.0))), + labelTextStyle:MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), + iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000005))), + ); + const Color backgroundColor = Color(0x00000009); + const double elevation = 14.0; + const Color shadowColor = Color(0x00000008); + const Color surfaceTintColor = Color(0x00000007); + const RoundedRectangleBorder indicatorShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(32.0))); + const Color indicatorColor = Color(0x00000006); + + await tester.pumpWidget( + _buildWidget( + scaffoldKey, + NavigationDrawer( + backgroundColor: backgroundColor, + elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + indicatorShape: indicatorShape, + indicatorColor: indicatorColor, + children: const <Widget>[ + Text('Headline'), + NavigationDrawerDestination( + icon: Icon(Icons.ac_unit), + label: Text('AC'), + ), + NavigationDrawerDestination( + icon: Icon(Icons.access_alarm), + label: Text('Alarm'), + ), + ], + onDestinationSelected: (int i) {}, + ), + theme: ThemeData( + navigationDrawerTheme: navigationDrawerTheme, + ), + ), + ); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(seconds: 1)); + + // Test drawer Material. + expect(_getMaterial(tester).color, backgroundColor); + expect(_getMaterial(tester).surfaceTintColor, surfaceTintColor); + expect(_getMaterial(tester).shadowColor, shadowColor); + expect(_getMaterial(tester).elevation, elevation); + // Test indicator decoration. + expect(_getIndicatorDecoration(tester)?.color, indicatorColor); + expect(_getIndicatorDecoration(tester)?.shape, indicatorShape); + }); + + testWidgetsWithLeakTracking('Local NavigationDrawerTheme takes priority over ThemeData.navigationDrawerTheme', (WidgetTester tester) async { + final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); + const Color backgroundColor = Color(0x00000009); + const double elevation = 7.0; + const Color shadowColor = Color(0x00000008); + const Color surfaceTintColor = Color(0x00000007); + const Color iconColor = Color(0x00000006); + const TextStyle labelStyle = TextStyle(fontSize: 7.0); + const ShapeBorder indicatorShape = CircleBorder(); + const Color indicatorColor = Color(0x00000005); + + await tester.pumpWidget( + _buildWidget( + scaffoldKey, + NavigationDrawerTheme( + data: const NavigationDrawerThemeData( + backgroundColor: backgroundColor, + elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + indicatorShape: indicatorShape, + indicatorColor: indicatorColor, + labelTextStyle:MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), + iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: iconColor)), + ), + child: NavigationDrawer( + children: const <Widget>[ + Text('Headline'), + NavigationDrawerDestination( + icon: Icon(Icons.ac_unit), + label: Text('AC'), + ), + NavigationDrawerDestination( + icon: Icon(Icons.access_alarm), + label: Text('Alarm'), + ), + ], + onDestinationSelected: (int i) {}, + ), + ), + theme: ThemeData( + navigationDrawerTheme: const NavigationDrawerThemeData( + backgroundColor: Color(0x00000001), + elevation: 7.0, + shadowColor: Color(0x00000002), + surfaceTintColor: Color(0x00000003), + indicatorColor: Color(0x00000004), + indicatorShape: RoundedRectangleBorder(borderRadius: BorderRadius.only(topRight: Radius.circular(16.0))), + labelTextStyle:MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)), + iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000005))), + ), + ), + ), + ); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(seconds: 1)); + + // Test drawer Material. + expect(_getMaterial(tester).color, backgroundColor); + expect(_getMaterial(tester).surfaceTintColor, surfaceTintColor); + expect(_getMaterial(tester).shadowColor, shadowColor); + expect(_getMaterial(tester).elevation, elevation); + // Test indicator decoration. + expect(_getIndicatorDecoration(tester)?.color, indicatorColor); + expect(_getIndicatorDecoration(tester)?.shape, indicatorShape); + // Test icon. + expect(_iconStyle(tester, Icons.ac_unit)?.color, iconColor); + expect(_iconStyle(tester, Icons.access_alarm)?.color, iconColor); + // Test label. + expect(_labelStyle(tester, 'AC'), labelStyle); + expect(_labelStyle(tester, 'Alarm'), labelStyle); + }); +} + +Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, { ThemeData? theme }) { + return MaterialApp( + theme: theme, + home: Scaffold( + key: scaffoldKey, + drawer: child, + body: Container(), + ), + ); +} + +Material _getMaterial(WidgetTester tester) { + return tester.firstWidget<Material>(find.descendant( + of: find.byType(NavigationDrawer), + matching: find.byType(Material), + )); +} + +ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { + return tester.firstWidget<Container>(find.descendant( + of: find.byType(FadeTransition), + matching: find.byType(Container), + )).decoration as ShapeDecoration?; +} + +TextStyle? _iconStyle(WidgetTester tester, IconData icon) { + return tester.widget<RichText>( + find.descendant(of: find.byIcon(icon), + matching: find.byType(RichText)), + ).text.style; +} + +TextStyle? _labelStyle(WidgetTester tester, String label) { + return tester.widget<RichText>(find.descendant( + of: find.text(label), + matching: find.byType(RichText), + )).text.style; } diff --git a/packages/flutter/test/material/navigation_rail_test.dart b/packages/flutter/test/material/navigation_rail_test.dart index fb0086b9d2446..3689777c1c40e 100644 --- a/packages/flutter/test/material/navigation_rail_test.dart +++ b/packages/flutter/test/material/navigation_rail_test.dart @@ -7,12 +7,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Custom selected and unselected textStyles are honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom selected and unselected textStyles are honored', (WidgetTester tester) async { const TextStyle selectedTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 17.0); const TextStyle unselectedTextStyle = TextStyle(fontWeight: FontWeight.w800, fontSize: 11.0); @@ -35,7 +34,7 @@ void main() { expect(actualUnselectedTextStyle.fontWeight, equals(actualUnselectedTextStyle.fontWeight)); }); - testWidgets('Custom selected and unselected iconThemes are honored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom selected and unselected iconThemes are honored', (WidgetTester tester) async { const IconThemeData selectedIconTheme = IconThemeData(size: 36, color: Color(0x00000001)); const IconThemeData unselectedIconTheme = IconThemeData(size: 18, color: Color(0x00000002)); @@ -58,7 +57,7 @@ void main() { expect(actualUnselectedIconTheme.fontSize, equals(unselectedIconTheme.size)); }); - testWidgets('No selected destination when selectedIndex is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No selected destination when selectedIndex is null', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -71,7 +70,7 @@ void main() { expect(semantics.where((Semantics s) => s.properties.selected ?? false), isEmpty); }); - testWidgets('backgroundColor can be changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backgroundColor can be changed', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -96,7 +95,7 @@ void main() { expect(_railMaterial(tester).color, equals(Colors.green)); }); - testWidgets('elevation can be changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('elevation can be changed', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -121,7 +120,7 @@ void main() { expect(_railMaterial(tester).elevation, equals(7)); }); - testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -134,7 +133,7 @@ void main() { expect(renderBox.size.width, 80.0); }); - testWidgets('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -148,7 +147,7 @@ void main() { expect(renderBox.size.width, 80.0); }); - testWidgets('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -162,7 +161,7 @@ void main() { expect(renderBox.size.width, 80.0); }); - testWidgets('Renders wider for a destination with a long label - [labelType]=all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders wider for a destination with a long label - [labelType]=all', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -188,7 +187,7 @@ void main() { expect(renderBox.size.width, _labelRenderBox(tester, 'Longer Label').size.width + 16.0); }); - testWidgets('Renders only icons - [labelType]=none (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders only icons - [labelType]=none (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -209,7 +208,7 @@ void main() { expect(_labelOpacity(tester, 'Jkl'), 0); }); - testWidgets('Renders icons and labels - [labelType]=all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders icons and labels - [labelType]=all', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -236,7 +235,7 @@ void main() { expect(_opacityAboveLabel('Jkl'), findsNothing); }); - testWidgets('Renders icons and selected label - [labelType]=selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders icons and selected label - [labelType]=selected', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -258,7 +257,7 @@ void main() { expect(_labelOpacity(tester, 'Jkl'), 0); }); - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -332,7 +331,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. @@ -410,7 +409,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. @@ -488,7 +487,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -542,48 +541,54 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. - const double destinationWidth = bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 125.5 : 126.0; + const double destinationWidth = 125.5; // Height of a destination indicator with icon. const double destinationHeight = 32.0; // Space between the indicator and label. @@ -634,44 +639,50 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -726,44 +737,50 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -817,48 +834,54 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. - const double destinationWidth = bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 125.5 : 126.0; + const double destinationWidth = 125.5; // Height of a destination indicator with icon. const double destinationHeight = 32.0; // Space between the indicator and label. @@ -909,44 +932,50 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1001,44 +1030,50 @@ void main() { // The second destination is below the first with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - expect( - secondIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The third destination is below the second with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - expect( - thirdIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } // The fourth destination is below the third with some spacing. nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - expect( - fourthIconRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (destinationWidth - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), ), - ), - ); + ); + } }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1113,7 +1148,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1191,7 +1226,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1269,7 +1304,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1343,7 +1378,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1418,7 +1453,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1493,7 +1528,7 @@ void main() { ); }); - testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leading and trailing appear in the correct places', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -1510,7 +1545,7 @@ void main() { expect(trailing.localToGlobal(Offset.zero), Offset((80 - trailing.size.width) / 2, 248.0)); }); - testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1656,7 +1691,7 @@ void main() { ); }); - testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { // Padding at the top of the rail. const double topPadding = 8.0; // Width of a destination. @@ -1809,7 +1844,7 @@ void main() { ); }); - testWidgets('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -1868,7 +1903,7 @@ void main() { expect(rail.size.width, equals(526.0)); }); - testWidgets('Extended rail final width can be changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail final width can be changed', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -1911,7 +1946,7 @@ void main() { }); /// Regression test for https://github.com/flutter/flutter/issues/65657 - testWidgets('Extended rail transition does not jump from the beginning', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail transition does not jump from the beginning', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -1970,7 +2005,7 @@ void main() { expect(tester.getSize(rail).width, closeTo(80.0, 1.0)); }); - testWidgets('Extended rail animation can be consumed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail animation can be consumed', (WidgetTester tester) async { bool extended = false; late Animation<double> animation; late StateSetter stateSetter; @@ -2015,7 +2050,7 @@ void main() { expect(animation.isCompleted, isTrue); }); - testWidgets('onDestinationSelected is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onDestinationSelected is called', (WidgetTester tester) async { late int selectedIndex; await _pumpNavigationRail( @@ -2040,7 +2075,7 @@ void main() { tester.pumpAndSettle(); }); - testWidgets('onDestinationSelected is not called if null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onDestinationSelected is not called if null', (WidgetTester tester) async { const int selectedIndex = 0; await _pumpNavigationRail( tester, @@ -2058,7 +2093,7 @@ void main() { tester.pumpAndSettle(); }); - testWidgets('Changing destinations animate when [labelType]=selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing destinations animate when [labelType]=selected', (WidgetTester tester) async { int selectedIndex = 0; await tester.pumpWidget( @@ -2123,7 +2158,7 @@ void main() { expect(_labelOpacity(tester, 'Ghi'), equals(1.0)); }); - testWidgets('Changing destinations animate for selectedIndex=null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing destinations animate for selectedIndex=null', (WidgetTester tester) async { int? selectedIndex = 0; late StateSetter stateSetter; @@ -2178,7 +2213,7 @@ void main() { expect(_labelOpacity(tester, 'Abc'), equals(1.0)); }); - testWidgets('Changing destinations animate when selectedIndex=null during transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing destinations animate when selectedIndex=null during transition', (WidgetTester tester) async { int? selectedIndex = 0; late StateSetter stateSetter; @@ -2233,7 +2268,7 @@ void main() { expect(_labelOpacity(tester, 'Def'), equals(0.0)); }); - testWidgets('Semantics - labelType=[none]', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics - labelType=[none]', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.none); @@ -2243,7 +2278,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics - labelType=[selected]', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics - labelType=[selected]', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.selected); @@ -2253,7 +2288,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics - labelType=[all]', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics - labelType=[all]', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.all); @@ -2263,7 +2298,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics - extended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics - extended', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await _pumpLocalizedTestRail(tester, extended: true); @@ -2273,7 +2308,7 @@ void main() { semantics.dispose(); }); - testWidgets('NavigationRailDestination padding properly applied - NavigationRailLabelType.all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination padding properly applied - NavigationRailLabelType.all', (WidgetTester tester) async { const EdgeInsets defaultPadding = EdgeInsets.symmetric(horizontal: 8.0); const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0); const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0); @@ -2330,7 +2365,7 @@ void main() { expect(thirdItem.padding, thirdItemPadding); }); - testWidgets('NavigationRailDestination padding properly applied - NavigationRailLabelType.selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination padding properly applied - NavigationRailLabelType.selected', (WidgetTester tester) async { const EdgeInsets defaultPadding = EdgeInsets.symmetric(horizontal: 8.0); const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0); const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0); @@ -2387,7 +2422,7 @@ void main() { expect(thirdItem.padding, thirdItemPadding); }); - testWidgets('NavigationRailDestination padding properly applied - NavigationRailLabelType.none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination padding properly applied - NavigationRailLabelType.none', (WidgetTester tester) async { const EdgeInsets defaultPadding = EdgeInsets.zero; const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0); const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0); @@ -2444,7 +2479,7 @@ void main() { expect(thirdItem.padding, thirdItemPadding); }); - testWidgets('NavigationRailDestination adds indicator by default when ThemeData.useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds indicator by default when ThemeData.useMaterial3 is true', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2473,7 +2508,7 @@ void main() { expect(find.byType(NavigationIndicator), findsWidgets); }); - testWidgets('NavigationRailDestination adds indicator when useIndicator is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds indicator when useIndicator is true', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2503,7 +2538,7 @@ void main() { expect(find.byType(NavigationIndicator), findsWidgets); }); - testWidgets('NavigationRailDestination does not add indicator when useIndicator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination does not add indicator when useIndicator is false', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2533,7 +2568,7 @@ void main() { expect(find.byType(NavigationIndicator), findsNothing); }); - testWidgets('NavigationRailDestination adds an oval indicator when no labels are present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds an oval indicator when no labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2566,7 +2601,7 @@ void main() { expect(indicator.height, 32); }); - testWidgets('NavigationRailDestination adds an oval indicator when selected labels are present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds an oval indicator when selected labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2599,7 +2634,7 @@ void main() { expect(indicator.height, 32); }); - testWidgets('NavigationRailDestination adds an oval indicator when all labels are present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds an oval indicator when all labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2632,7 +2667,7 @@ void main() { expect(indicator.height, 32); }); - testWidgets('NavigationRailDestination has center aligned indicator - [labelType]=none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination has center aligned indicator - [labelType]=none', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/97753 await _pumpNavigationRail( @@ -2676,7 +2711,7 @@ void main() { expect(lastIndicator.localToGlobal(Offset.zero).dx, 28.0); }); - testWidgets('NavigationRail respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { const double safeAreaPadding = 40.0; NavigationRail navigationRail() { return NavigationRail( @@ -2729,7 +2764,7 @@ void main() { expect(updatedWidthRTL, defaultWidth + safeAreaPadding); }); - testWidgets('NavigationRail indicator renders ripple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders ripple', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2788,9 +2823,9 @@ void main() { color: const Color(0xffe8def8), ), ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('NavigationRail indicator renders ripple - extended', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders ripple - extended', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117126 await _pumpNavigationRail( tester, @@ -2853,7 +2888,7 @@ void main() { ); }); - testWidgets('NavigationRail indicator renders properly when padding is applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders properly when padding is applied', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117126 await _pumpNavigationRail( tester, @@ -2918,7 +2953,7 @@ void main() { ); }); - testWidgets('Indicator renders properly when NavigationRai.minWidth < default minWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Indicator renders properly when NavigationRai.minWidth < default minWidth', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117126 await _pumpNavigationRail( tester, @@ -2982,7 +3017,7 @@ void main() { ); }); - testWidgets('NavigationRail indicator renders properly with custom padding and minWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders properly with custom padding and minWidth', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/117126 await _pumpNavigationRail( tester, @@ -3048,7 +3083,7 @@ void main() { ); }); - testWidgets('NavigationRail indicator renders properly with long labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders properly with long labels', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/128005. await _pumpNavigationRail( tester, @@ -3082,6 +3117,9 @@ void main() { const double destinationWidth = 72.0; const double destinationHorizontalPadding = 8.0; const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0 + const double verticalSpacer = 8.0; + const double verticalIconLabelSpacing = 4.0; + const double verticalDestinationSpacing = 12.0; // The navigation rail width is larger than default because of the first destination long label. final double railWidth = tester.getSize(find.byType(NavigationRail)).width; @@ -3093,6 +3131,12 @@ void main() { final Rect includedRect = indicatorRect; final Rect excludedRect = includedRect.inflate(10); + // Compute the vertical position for the selected destination (the one with 'bookmark' icon). + const double labelHeight = 16; // fontSize is 12 and height is 1.3. + const double destinationHeight = indicatorHeight + verticalIconLabelSpacing + labelHeight + verticalDestinationSpacing; + const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight; + const double secondIndicatorVerticalOffset = secondDestinationVerticalOffset; + expect( inkFeatures, paints @@ -3117,13 +3161,221 @@ void main() { color: const Color(0x0a6750a4), ) ..rrect( - rrect: RRect.fromLTRBR(indicatorLeft, 72.0, indicatorRight, 104.0, const Radius.circular(16)), + rrect: RRect.fromLTRBR( + indicatorLeft, + secondIndicatorVerticalOffset, + indicatorRight, + secondIndicatorVerticalOffset + indicatorHeight, + const Radius.circular(16), + ), color: const Color(0xffe8def8), ), ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('NavigationRail indicator scale transform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail indicator renders properly with large icon', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/133799. + const double iconSize = 50; + await _pumpNavigationRail( + tester, + navigationRailTheme: const NavigationRailThemeData( + selectedIconTheme: IconThemeData(size: iconSize), + unselectedIconTheme: IconThemeData(size: iconSize), + ), + navigationRail: NavigationRail( + selectedIndex: 1, + destinations: const <NavigationRailDestination>[ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('ABC'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('DEF'), + ), + ], + labelType: NavigationRailLabelType.all, + ), + ); + + // Hover the first destination. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border))); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + + // Default values from M3 specification. + const double railMinWidth = 80.0; + const double indicatorHeight = 32.0; + const double destinationWidth = 72.0; + const double destinationHorizontalPadding = 8.0; + const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0 + const double verticalSpacer = 8.0; + const double verticalIconLabelSpacing = 4.0; + const double verticalDestinationSpacing = 12.0; + + // The navigation rail width is the default one because labels are short. + final double railWidth = tester.getSize(find.byType(NavigationRail)).width; + expect(railWidth, railMinWidth); + + // Expected indicator position. + final double indicatorLeft = (railWidth - indicatorWidth) / 2; + final double indicatorRight = (railWidth + indicatorWidth) / 2; + const double indicatorTop = (iconSize - indicatorHeight) / 2; + const double indicatorBottom = (iconSize + indicatorHeight) / 2; + final Rect indicatorRect = Rect.fromLTRB(indicatorLeft, indicatorTop, indicatorRight, indicatorBottom); + final Rect includedRect = indicatorRect; + final Rect excludedRect = includedRect.inflate(10); + + // Compute the vertical position for the selected destination (the one with 'bookmark' icon). + const double labelHeight = 16; // fontSize is 12 and height is 1.3. + const double destinationHeight = iconSize + verticalIconLabelSpacing + labelHeight + verticalDestinationSpacing; + const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight; + const double indicatorOffset = (iconSize - indicatorHeight) / 2; + const double secondIndicatorVerticalOffset = secondDestinationVerticalOffset + indicatorOffset; + + expect( + inkFeatures, + paints + ..clipPath( + pathMatcher: isPathThat( + includes: <Offset>[ + includedRect.centerLeft, + includedRect.topCenter, + includedRect.centerRight, + includedRect.bottomCenter, + ], + excludes: <Offset>[ + excludedRect.centerLeft, + excludedRect.topCenter, + excludedRect.centerRight, + excludedRect.bottomCenter, + ], + ), + ) + // Hover highlight for the hovered destination (the one with 'favorite' icon). + ..rect( + rect: indicatorRect, + color: const Color(0x0a6750a4), + ) + // Indicator for the selected destination (the one with 'bookmark' icon). + ..rrect( + rrect: RRect.fromLTRBR( + indicatorLeft, + secondIndicatorVerticalOffset, + indicatorRight, + secondIndicatorVerticalOffset + indicatorHeight, + const Radius.circular(16), + ), + color: const Color(0xffe8def8), + ), + ); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + + testWidgetsWithLeakTracking('NavigationRail indicator renders properly when text direction is rtl', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/134361. + await tester.pumpWidget(_buildWidget( + NavigationRail( + selectedIndex: 1, + extended: true, + destinations: const <NavigationRailDestination>[ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('ABC'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('DEF'), + ), + ], + ), + isRTL: true, + )); + + // Hover the first destination. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border))); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + + // Default values from M3 specification. + const double railMinExtendedWidth = 256.0; + const double indicatorHeight = 32.0; + const double destinationWidth = 72.0; + const double destinationHorizontalPadding = 8.0; + const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0 + const double verticalSpacer = 8.0; + const double verticalDestinationSpacingM3 = 12.0; + + // The navigation rail width is the default one because labels are short. + final double railWidth = tester.getSize(find.byType(NavigationRail)).width; + expect(railWidth, railMinExtendedWidth); + + // Expected indicator position. + final double indicatorLeft = railWidth - (destinationWidth - destinationHorizontalPadding / 2); + final double indicatorRight = indicatorLeft + indicatorWidth; + final Rect indicatorRect = Rect.fromLTRB( + indicatorLeft, + verticalDestinationSpacingM3 / 2, + indicatorRight, + verticalDestinationSpacingM3 / 2 + indicatorHeight, + ); + final Rect includedRect = indicatorRect; + final Rect excludedRect = includedRect.inflate(10); + + // Compute the vertical position for the selected destination (the one with 'bookmark' icon). + const double destinationHeight = indicatorHeight + verticalDestinationSpacingM3; + const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight; + const double secondIndicatorVerticalOffset = secondDestinationVerticalOffset + verticalDestinationSpacingM3 / 2; + const double secondDestinationHorizontalOffset = 800 - railMinExtendedWidth; // RTL. + + expect( + inkFeatures, + paints + ..clipPath( + pathMatcher: isPathThat( + includes: <Offset>[ + includedRect.centerLeft, + includedRect.topCenter, + includedRect.centerRight, + includedRect.bottomCenter, + ], + excludes: <Offset>[ + excludedRect.centerLeft, + excludedRect.topCenter, + excludedRect.centerRight, + excludedRect.bottomCenter, + ], + ), + ) + // Hover highlight for the hovered destination (the one with 'favorite' icon). + ..rect( + rect: indicatorRect, + color: const Color(0x0a6750a4), + ) + // Indicator for the selected destination (the one with 'bookmark' icon). + ..rrect( + rrect: RRect.fromLTRBR( + secondDestinationHorizontalOffset + indicatorLeft, + secondIndicatorVerticalOffset, + secondDestinationHorizontalOffset + indicatorRight, + secondIndicatorVerticalOffset + indicatorHeight, + const Radius.circular(16), + ), + color: const Color(0xffe8def8), + ), + ); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + + testWidgetsWithLeakTracking('NavigationRail indicator scale transform', (WidgetTester tester) async { int selectedIndex = 0; Future<void> buildWidget() async { await _pumpNavigationRail( @@ -3167,7 +3419,7 @@ void main() { expect(transform.getColumn(0)[0], 1.0); }); - testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation destination updates indicator color and shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); const Color color = Color(0xff0000ff); const ShapeBorder shape = RoundedRectangleBorder(); @@ -3211,7 +3463,7 @@ void main() { expect(_getIndicatorDecoration(tester)?.shape, shape); }); - testWidgets("Destination's respect their disabled state", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Destination's respect their disabled state", (WidgetTester tester) async { late int selectedIndex; await _pumpNavigationRail( tester, @@ -3255,12 +3507,53 @@ void main() { tester.pumpAndSettle(); }); + testWidgetsWithLeakTracking("Destination's label with the right opacity while disabled", (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: const <NavigationRailDestination>[ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Bcd'), + disabled: true, + ), + ], + onDestinationSelected: (int index) {}, + labelType: NavigationRailLabelType.all, + ), + ); + + await tester.pumpAndSettle(); + + double? defaultTextStyleOpacity(String text) { + return tester.widget<DefaultTextStyle>( + find.ancestor( + of: find.text(text), + matching: find.byType(DefaultTextStyle), + ).first, + ).style.color?.opacity; + } + + final double? abcLabelOpacity = defaultTextStyleOpacity('Abc'); + final double? bcdLabelOpacity = defaultTextStyleOpacity('Bcd'); + + expect(abcLabelOpacity, 1.0); + expect(bcdLabelOpacity, closeTo(0.38, 0.01)); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3274,7 +3567,7 @@ void main() { expect(renderBox.size.width, 72.0); }); - testWidgets('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3289,7 +3582,7 @@ void main() { expect(renderBox.size.width, 72.0); }); - testWidgets('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3304,7 +3597,7 @@ void main() { expect(renderBox.size.width, 72.0); }); - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3369,9 +3662,9 @@ void main() { ), ), ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. await _pumpNavigationRail( @@ -3439,9 +3732,9 @@ void main() { ), ), ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. await _pumpNavigationRail( @@ -3509,9 +3802,9 @@ void main() { ), ), ); - }); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3589,7 +3882,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3668,7 +3961,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3747,7 +4040,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3855,7 +4148,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -3962,7 +4255,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4072,7 +4365,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4140,7 +4433,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4211,7 +4504,7 @@ void main() { ); }); - testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4282,7 +4575,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4346,7 +4639,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4409,7 +4702,7 @@ void main() { ); }); - testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4472,7 +4765,7 @@ void main() { ); }); - testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leading and trailing appear in the correct places', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4490,7 +4783,7 @@ void main() { expect(trailing.localToGlobal(Offset.zero), Offset((72 - trailing.size.width) / 2.0, 360.0)); }); - testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -4628,7 +4921,7 @@ void main() { ); }); - testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -4773,7 +5066,7 @@ void main() { ); }); - testWidgets('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -4832,7 +5125,7 @@ void main() { expect(rail.size.width, equals(584.0)); }); - testWidgets('Extended rail final width can be changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail final width can be changed', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -4875,7 +5168,7 @@ void main() { }); /// Regression test for https://github.com/flutter/flutter/issues/65657 - testWidgets('Extended rail transition does not jump from the beginning', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Extended rail transition does not jump from the beginning', (WidgetTester tester) async { bool extended = false; late StateSetter stateSetter; @@ -4934,7 +5227,7 @@ void main() { expect(tester.getSize(rail).width, closeTo(72.0, 1.0)); }); - testWidgets('NavigationRailDestination adds circular indicator when no labels are present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination adds circular indicator when no labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, useMaterial3: false, @@ -4968,7 +5261,7 @@ void main() { expect(indicator.height, 56); }); - testWidgets('NavigationRailDestination has center aligned indicator - [labelType]=none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailDestination has center aligned indicator - [labelType]=none', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/97753 await _pumpNavigationRail( @@ -5013,7 +5306,7 @@ void main() { expect(lastIndicator.localToGlobal(Offset.zero).dx, 24.0); }); - testWidgets('NavigationRail respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async { const double safeAreaPadding = 40.0; NavigationRail navigationRail() { return NavigationRail( @@ -5152,10 +5445,14 @@ Future<void> _pumpNavigationRail( double textScaleFactor = 1.0, required NavigationRail navigationRail, bool useMaterial3 = true, + NavigationRailThemeData? navigationRailTheme, }) async { await tester.pumpWidget( MaterialApp( - theme: ThemeData(useMaterial3: useMaterial3), + theme: ThemeData( + useMaterial3: useMaterial3, + navigationRailTheme: navigationRailTheme, + ), home: Builder( builder: (BuildContext context) { return MediaQuery( diff --git a/packages/flutter/test/material/navigation_rail_theme_test.dart b/packages/flutter/test/material/navigation_rail_theme_test.dart index bf77080fd61f9..c3c29ce513511 100644 --- a/packages/flutter/test/material/navigation_rail_theme_test.dart +++ b/packages/flutter/test/material/navigation_rail_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('copyWith, ==, hashCode basics', () { @@ -12,7 +13,7 @@ void main() { expect(const NavigationRailThemeData().hashCode, const NavigationRailThemeData().copyWith().hashCode); }); - testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async { final ThemeData theme = ThemeData.light(useMaterial3: true); // Material 3 defaults await tester.pumpWidget( @@ -47,7 +48,7 @@ void main() { expect(inkResponse.customBorder, const StadiumBorder()); }); - testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified (Material 2)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async { // This test can be removed when `useMaterial3` is deprecated. await tester.pumpWidget( MaterialApp( @@ -77,7 +78,7 @@ void main() { expect(find.byType(NavigationIndicator), findsNothing); }); - testWidgets('NavigationRailThemeData values are used when no NavigationRail properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailThemeData values are used when no NavigationRail properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const double elevation = 7.0; const double selectedIconSize = 25.0; @@ -145,7 +146,7 @@ void main() { expect(_indicatorDecoration(tester)?.shape, indicatorShape); }); - testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async { const Color backgroundColor = Color(0x00000001); const double elevation = 7.0; const double selectedIconSize = 25.0; @@ -229,14 +230,14 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/118618. - testWidgets('NavigationRailThemeData lerps correctly with null iconThemes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NavigationRailThemeData lerps correctly with null iconThemes', (WidgetTester tester) async { final NavigationRailThemeData lerp = NavigationRailThemeData.lerp(const NavigationRailThemeData(), const NavigationRailThemeData(), 0.5)!; expect(lerp.selectedIconTheme, isNull); expect(lerp.unselectedIconTheme, isNull); }); - testWidgets('Default debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const NavigationRailThemeData().debugFillProperties(builder); @@ -248,7 +249,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('Custom debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const NavigationRailThemeData( backgroundColor: Color(0x00000099), diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index eb25c9401993e..cc6ec12f4fcf4 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -7,12 +7,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('OutlinedButton, OutlinedButton.icon defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton, OutlinedButton.icon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); final bool material3 = theme.useMaterial3; @@ -176,7 +175,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('OutlinedButton default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -227,9 +226,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('Does OutlinedButton work with hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does OutlinedButton work with hover', (WidgetTester tester) async { const Color hoverColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -257,7 +258,7 @@ void main() { expect(inkFeatures, paints..rect(color: hoverColor)); }); - testWidgets('Does OutlinedButton work with focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does OutlinedButton work with focus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -285,9 +286,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('Does OutlinedButton work with autofocus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does OutlinedButton work with autofocus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -315,9 +318,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('Default OutlinedButton meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default OutlinedButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -360,11 +365,13 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); await expectLater(tester, meetsGuideline(textContrastGuideline)); + + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('OutlinedButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); Color getTextColor(Set<MaterialState> states) { @@ -429,11 +436,13 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. await expectLater(tester, meetsGuideline(textContrastGuideline)); + + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('OutlinedButton uses stateful color for text color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton uses stateful color for text color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -498,9 +507,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(textColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('OutlinedButton uses stateful color for icon color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton uses stateful color for icon color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final Key buttonKey = UniqueKey(); @@ -565,9 +576,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(iconColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('OutlinedButton uses stateful color for border color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton uses stateful color for border color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -633,9 +646,11 @@ void main() { await gesture.down(center); await tester.pumpAndSettle(); expect(outlinedButton, paints..drrect(color: pressedColor)); + + focusNode.dispose(); }); - testWidgets('OutlinedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder outlinedButton; @@ -679,7 +694,7 @@ void main() { expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false); }); - testWidgets("OutlinedButton response doesn't hover when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("OutlinedButton response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Focus'); final GlobalKey childKey = GlobalKey(); @@ -727,9 +742,11 @@ void main() { await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + + focusNode.dispose(); }); - testWidgets('disabled and hovered OutlinedButton responds to mouse-exit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled and hovered OutlinedButton responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; @@ -791,7 +808,7 @@ void main() { expect(hover, false); }); - testWidgets('Can set OutlinedButton focus and Can set unFocus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set OutlinedButton focus and Can set unFocus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -818,9 +835,11 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('When OutlinedButton disable, Can not set OutlinedButton focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When OutlinedButton disable, Can not set OutlinedButton focus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -841,9 +860,11 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets("Outline button doesn't crash if disabled during a gesture", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Outline button doesn't crash if disabled during a gesture", (WidgetTester tester) async { Widget buildFrame(VoidCallback? onPressed) { return Directionality( textDirection: TextDirection.ltr, @@ -863,7 +884,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('OutlinedButton shape and border component overrides', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton shape and border component overrides', (WidgetTester tester) async { const Color fillColor = Color(0xFF00FF00); const BorderSide disabledBorderSide = BorderSide(color: Color(0xFFFF0000), width: 3); const BorderSide enabledBorderSide = BorderSide(color: Color(0xFFFF00FF), width: 4); @@ -941,7 +962,7 @@ void main() { expect(getBorderSide(), enabledBorderSide); }); - testWidgets('OutlinedButton has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton has no clip by default', (WidgetTester tester) async { final GlobalKey buttonKey = GlobalKey(); await tester.pumpWidget( Directionality( @@ -963,7 +984,7 @@ void main() { }); - testWidgets('OutlinedButton contributes semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton contributes semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( @@ -1011,7 +1032,7 @@ void main() { semantics.dispose(); }); - testWidgets('OutlinedButton scales textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton scales textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -1066,10 +1087,7 @@ void main() { ); expect(tester.getSize(find.byType(OutlinedButton)), equals(const Size(88.0, 48.0))); - expect(tester.getSize(find.byType(Text)), const Size( - bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 52.5 : 53.0, - 18.0, - )); + expect(tester.getSize(find.byType(Text)), const Size(52.5, 18.0)); // Set text scale large enough to expand text and button. await tester.pumpWidget( @@ -1094,7 +1112,7 @@ void main() { expect(tester.getSize(find.byType(Text)), const Size(126.0, 42.0)); }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/122066 - testWidgets('OutlinedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -1125,7 +1143,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets('OutlinedButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -1258,7 +1276,7 @@ void main() { if (textDirection == TextDirection.rtl) 'RTL', ].join(', '); - testWidgets(testName, (WidgetTester tester) async { + testWidgetsWithLeakTracking(testName, (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1402,7 +1420,7 @@ void main() { } }); - testWidgets('Override OutlinedButton default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override OutlinedButton default padding', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1436,7 +1454,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.all(22)); }); - testWidgets('M3 OutlinedButton has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 OutlinedButton has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1462,7 +1480,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 24)); }); - testWidgets('M3 OutlinedButton.icon has correct padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 OutlinedButton.icon has correct padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1489,7 +1507,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(16.0, 0.0, 24.0, 0.0)); }); - testWidgets('Fixed size OutlinedButtons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size OutlinedButtons', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1522,7 +1540,7 @@ void main() { expect(tester.getSize(find.widgetWithText(OutlinedButton, 'wx200')).height, 200); }); - testWidgets('OutlinedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) { return MaterialApp( home: Scaffold( @@ -1562,7 +1580,7 @@ void main() { } }); - testWidgets('OutlinedButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -1589,7 +1607,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('OutlinedButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( @@ -1611,7 +1629,7 @@ void main() { expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory)); }, variant: TargetPlatformVariant.all()); - testWidgets('OutlinedButton.icon does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton.icon does not overflow', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/77815 await tester.pumpWidget( MaterialApp( @@ -1632,7 +1650,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('OutlinedButton.icon icon,label layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton.icon icon,label layout', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); final Key iconKey = UniqueKey(); final Key labelKey = UniqueKey(); @@ -1669,7 +1687,7 @@ void main() { expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0)); }); - testWidgets('OutlinedButton maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton maximumSize', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); @@ -1711,7 +1729,7 @@ void main() { expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0)); }); - testWidgets('Fixed size OutlinedButton, same as minimumSize == maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size OutlinedButton, same as minimumSize == maximumSize', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1741,7 +1759,7 @@ void main() { expect(tester.getSize(find.widgetWithText(OutlinedButton, '200,200')), const Size(200, 200)); }); - testWidgets('OutlinedButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1819,7 +1837,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('OutlinedButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104595. await tester.pumpWidget(MaterialApp( home: SelectionArea( @@ -1842,7 +1860,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('OutlinedButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1872,6 +1890,7 @@ void main() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( @@ -1972,20 +1991,21 @@ void main() { await gesture.removePointer(); } - testWidgets('OutlinedButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton statesController', (WidgetTester tester) async { testStatesController(null, tester); }); - testWidgets('OutlinedButton.icon statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OutlinedButton.icon statesController', (WidgetTester tester) async { testStatesController(const Icon(Icons.add), tester); }); - testWidgets('Disabled OutlinedButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled OutlinedButton statesController', (WidgetTester tester) async { int count = 0; void valueChanged() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( @@ -2003,7 +2023,7 @@ void main() { expect(count, 1); }); - testWidgets("OutlinedButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("OutlinedButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/118071. await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/material/outlined_button_theme_test.dart b/packages/flutter/test/material/outlined_button_theme_test.dart index 93e10033f7549..d29697eaa896f 100644 --- a/packages/flutter/test/material/outlined_button_theme_test.dart +++ b/packages/flutter/test/material/outlined_button_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('OutlinedButtonThemeData lerp special cases', () { @@ -12,7 +13,7 @@ void main() { expect(identical(OutlinedButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Material3: Passing no OutlinedButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Passing no OutlinedButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -53,7 +54,7 @@ void main() { expect(align.alignment, Alignment.center); }); - testWidgets('Material2: Passing no OutlinedButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Passing no OutlinedButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -196,19 +197,19 @@ void main() { expect(align.alignment, alignment); } - testWidgets('Button style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(themeStyle: style)); await tester.pumpAndSettle(); checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallStyle: style)); await tester.pumpAndSettle(); checkButton(tester); @@ -216,26 +217,26 @@ void main() { // Same as the previous tests with empty ButtonStyle's instead of null. - testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); }); - testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -306,7 +307,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); diff --git a/packages/flutter/test/material/page_selector_test.dart b/packages/flutter/test/material/page_selector_test.dart index 538f718c19c44..27da67f09402f 100644 --- a/packages/flutter/test/material/page_selector_test.dart +++ b/packages/flutter/test/material/page_selector_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color kSelectedColor = Color(0xFF00FF00); const Color kUnselectedColor = Colors.transparent; @@ -64,11 +65,12 @@ List<Color> indicatorColors(WidgetTester tester) { } void main() { - testWidgets('PageSelector responds correctly to setting the TabController index', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector responds correctly to setting the TabController index', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(), length: 3, ); + addTearDown(tabController.dispose); await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 0); @@ -85,11 +87,12 @@ void main() { expect(indicatorColors(tester), const <Color>[kUnselectedColor, kUnselectedColor, kSelectedColor]); }); - testWidgets('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(), length: 3, ); + addTearDown(tabController.dispose); await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 0); @@ -128,12 +131,13 @@ void main() { expect(indicatorColors(tester), const <Color>[kUnselectedColor, kUnselectedColor, kSelectedColor]); }); - testWidgets('PageSelector responds correctly to TabBarView drags', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector responds correctly to TabBarView drags', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(), initialIndex: 1, length: 3, ); + addTearDown(tabController.dispose); await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 1); @@ -184,10 +188,9 @@ void main() { await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 1000.0); await tester.pumpAndSettle(); expect(indicatorColors(tester), const <Color>[kUnselectedColor, kSelectedColor, kUnselectedColor]); - }); - testWidgets('PageSelector indicatorColors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector indicatorColors', (WidgetTester tester) async { const Color kRed = Color(0xFFFF0000); const Color kBlue = Color(0xFF0000FF); @@ -196,6 +199,7 @@ void main() { initialIndex: 1, length: 3, ); + addTearDown(tabController.dispose); await tester.pumpWidget(buildFrame(tabController, color: kRed, selectedColor: kBlue)); expect(tabController.index, 1); @@ -206,12 +210,13 @@ void main() { expect(indicatorColors(tester), const <Color>[kBlue, kRed, kRed]); }); - testWidgets('PageSelector indicatorSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector indicatorSize', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(), initialIndex: 1, length: 3, ); + addTearDown(tabController.dispose); await tester.pumpWidget(buildFrame(tabController, indicatorSize: 16.0)); final Iterable<Element> indicatorElements = find.descendant( @@ -227,12 +232,13 @@ void main() { expect(tester.getSize(find.byType(TabPageSelector)).height, 24.0); }); - testWidgets('PageSelector circle border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageSelector circle border', (WidgetTester tester) async { final TabController tabController = TabController( vsync: const TestVSync(), initialIndex: 1, length: 3, ); + addTearDown(tabController.dispose); Iterable<TabPageSelectorIndicator> indicators; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index b0486a9cfac82..8b130928a9abb 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -11,12 +11,12 @@ import 'package:flutter/cupertino.dart' show CupertinoPageRoute; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('test page transition (_FadeUpwardsPageTransition)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test page transition (_FadeUpwardsPageTransition)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: const Material(child: Text('Page 1')), @@ -80,7 +80,7 @@ void main() { expect(find.text('Page 2'), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('test page transition (CupertinoPageTransition)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test page transition (CupertinoPageTransition)', (WidgetTester tester) async { final Key page2Key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -161,7 +161,7 @@ void main() { expect(widget1InitialTopLeft == widget1TransientTopLeft, true); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async { Iterable<Layer> findLayers(Finder of) { return tester.layerListOf( find.ancestor(of: of, matching: find.byType(SnapshotWidget)).first, @@ -240,7 +240,7 @@ void main() { expect(find.text('Page 2'), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('test page transition (_ZoomPageTransition) with rasterization re-rasterizes when view insets change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - test page transition (_ZoomPageTransition) with rasterization re-rasterizes when view insets change', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view.physicalSize = const Size(1000, 1000); tester.view.viewInsets = FakeViewPadding.zero; @@ -270,7 +270,48 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); - await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.small.png')); + await expectLater(find.byKey(key), matchesGoldenFile('m2_zoom_page_transition.small.png')); + + // Change the view insets. + tester.view.viewInsets = const FakeViewPadding(bottom: 500); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + await expectLater(find.byKey(key), matchesGoldenFile('m2_zoom_page_transition.big.png')); + }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. + + testWidgetsWithLeakTracking('Material3 - test page transition (_ZoomPageTransition) with rasterization re-rasterizes when view insets change', (WidgetTester tester) async { + addTearDown(tester.view.reset); + tester.view.physicalSize = const Size(1000, 1000); + tester.view.viewInsets = FakeViewPadding.zero; + + // Intentionally use nested scaffolds to simulate the view insets being + // consumed. + final Key key = GlobalKey(); + await tester.pumpWidget( + RepaintBoundary( + key: key, + child: MaterialApp( + theme: ThemeData(useMaterial3: true), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return const Scaffold(body: Scaffold( + body: Material(child: SizedBox.shrink()) + )); + }, + ); + }, + ), + ), + ); + + tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next'); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + await expectLater(find.byKey(key), matchesGoldenFile('m3_zoom_page_transition.small.png')); // Change the view insets. tester.view.viewInsets = const FakeViewPadding(bottom: 500); @@ -278,10 +319,10 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); - await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.big.png')); + await expectLater(find.byKey(key), matchesGoldenFile('m3_zoom_page_transition.big.png')); }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. - testWidgets( + testWidgetsWithLeakTracking( 'test page transition (_ZoomPageTransition) with rasterization disables snapshotting for enter route', (WidgetTester tester) async { Iterable<Layer> findLayers(Finder of) { @@ -363,7 +404,7 @@ void main() { expect(isSnapshotted(page1Finder), isFalse); }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. - testWidgets('test fullscreen dialog transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test fullscreen dialog transition', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material(child: Text('Page 1')), @@ -423,7 +464,7 @@ void main() { expect(widget1InitialTopLeft == widget1TransientTopLeft, true); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test no back gesture on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test no back gesture on Android', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: const Scaffold(body: Text('Page 1')), @@ -453,7 +494,7 @@ void main() { expect(tester.getTopLeft(find.text('Page 2')), Offset.zero); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('test back gesture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test back gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: const Scaffold(body: Text('Page 1')), @@ -494,7 +535,7 @@ void main() { expect(tester.getTopLeft(find.text('Page 2')), const Offset(100.0, 0.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('back gesture while OS changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('back gesture while OS changes', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => Material( child: TextButton( @@ -585,7 +626,7 @@ void main() { expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.macOS); }); - testWidgets('test no back gesture on fullscreen dialogs', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test no back gesture on fullscreen dialogs', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold(body: Text('Page 1')), @@ -615,7 +656,7 @@ void main() { expect(tester.getTopLeft(find.text('Page 2')), Offset.zero); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test adaptable transitions switch during execution', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test adaptable transitions switch during execution', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -696,7 +737,7 @@ void main() { expect(widget1InitialTopLeft == widget1TransientTopLeft, true); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test edge swipe then drop back at starting point works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test edge swipe then drop back at starting point works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( onGenerateRoute: (RouteSettings settings) { @@ -731,7 +772,7 @@ void main() { expect(find.text('Page 2'), isOnstage); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test edge swipe then drop back at ending point works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test edge swipe then drop back at ending point works', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( onGenerateRoute: (RouteSettings settings) { @@ -764,7 +805,7 @@ void main() { expect(find.text('Page 2'), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Back swipe dismiss interrupted by route push', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Back swipe dismiss interrupted by route push', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28728 final GlobalKey scaffoldKey = GlobalKey(); @@ -859,7 +900,7 @@ void main() { expect(find.text('push'), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('During back swipe the route ignores input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('During back swipe the route ignores input', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/39989 final GlobalKey homeScaffoldKey = GlobalKey(); @@ -929,7 +970,7 @@ void main() { expect(tester.getTopLeft(find.byKey(homeScaffoldKey)).dx, lessThan(0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('After a pop caused by a back-swipe, input reaches the exposed route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('After a pop caused by a back-swipe, input reaches the exposed route', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/41024 final GlobalKey homeScaffoldKey = GlobalKey(); @@ -1000,7 +1041,7 @@ void main() { expect(pageTapCount, 1); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('A MaterialPageRoute should slide out with CupertinoPageTransition when a compatible PageRoute is pushed on top of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A MaterialPageRoute should slide out with CupertinoPageTransition when a compatible PageRoute is pushed on top of it', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/44864. await tester.pumpWidget( @@ -1026,9 +1067,10 @@ void main() { // Title of the first route slides to the left. expect(titleInitialTopLeft.dy, equals(titleTransientTopLeft.dy)); expect(titleInitialTopLeft.dx, greaterThan(titleTransientTopLeft.dx)); - }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('MaterialPage works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialPage works', (WidgetTester tester) async { final LocalKey pageKey = UniqueKey(); final TransitionDetector detector = TransitionDetector(); List<Page<void>> myPages = <Page<void>>[ @@ -1071,7 +1113,7 @@ void main() { expect(find.text('second'), findsOneWidget); }); - testWidgets('MaterialPage can toggle MaintainState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialPage can toggle MaintainState', (WidgetTester tester) async { final LocalKey pageKeyOne = UniqueKey(); final LocalKey pageKeyTwo = UniqueKey(); final TransitionDetector detector = TransitionDetector(); @@ -1120,7 +1162,7 @@ void main() { expect(find.text('second'), findsOneWidget); }); - testWidgets('MaterialPage does not lose its state when transitioning out', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialPage does not lose its state when transitioning out', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget(KeepsStateTestWidget(navigatorKey: navigator)); expect(find.text('subpage'), findsOneWidget); @@ -1133,7 +1175,7 @@ void main() { expect(find.text('home'), findsOneWidget); }); - testWidgets('MaterialPage restores its state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialPage restores its state', (WidgetTester tester) async { await tester.pumpWidget( RootRestorationScope( restorationId: 'root', @@ -1190,6 +1232,45 @@ void main() { expect(find.text('p1'), findsOneWidget); expect(find.text('count: 1'), findsOneWidget); }); + + testWidgetsWithLeakTracking('MaterialPageRoute can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132138. + final GlobalKey scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: Center( + child: ElevatedButton( + onPressed: () { + Navigator.push<void>(scaffoldKey.currentContext!, MaterialPageRoute<void>( + builder: (BuildContext context) { + return const Scaffold( + body: Center(child: Text('route')), + ); + }, + barrierDismissible: true, + )); + }, + child: const Text('push'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + expect(find.text('route'), findsOneWidget); + expect(find.text('push'), findsNothing); + + // Try to dismiss the route with the escape key. + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + expect(find.text('route'), findsNothing); + }); } class TransitionDetector extends DefaultTransitionDelegate<void> { @@ -1309,6 +1390,12 @@ class _TestRestorableWidgetState extends State<TestRestorableWidget> with Restor ], ); } + + @override + void dispose() { + counter.dispose(); + super.dispose(); + } } class TestDependencies extends StatelessWidget { diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index 323d4ed0c03d2..2f79094a67c18 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -7,9 +7,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Default PageTransitionsTheme platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default PageTransitionsTheme platform', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Text('home'))); final PageTransitionsTheme theme = Theme.of(tester.element(find.text('home'))).pageTransitionsTheme; expect(theme.builders, isNotNull); @@ -35,7 +36,7 @@ void main() { } }); - testWidgets('Default PageTransitionsTheme builds a CupertinoPageTransition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default PageTransitionsTheme builds a CupertinoPageTransition', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => Material( child: TextButton( @@ -61,7 +62,7 @@ void main() { expect(find.byType(CupertinoPageTransition), findsOneWidget); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Default PageTransitionsTheme builds a _ZoomPageTransition for android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default PageTransitionsTheme builds a _ZoomPageTransition for android', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => Material( child: TextButton( @@ -94,7 +95,7 @@ void main() { expect(findZoomPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('PageTransitionsTheme override builds a _OpenUpwardsPageTransition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageTransitionsTheme override builds a _OpenUpwardsPageTransition', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => Material( child: TextButton( @@ -134,7 +135,7 @@ void main() { expect(findOpenUpwardsPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('PageTransitionsTheme override builds a _FadeUpwardsTransition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageTransitionsTheme override builds a _FadeUpwardsTransition', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => Material( child: TextButton( @@ -214,7 +215,7 @@ void main() { return !(hasOneOpacityLayer && hasOneTransformLayer); } - testWidgets('ZoomPageTransitionsBuilder default route snapshotting behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ZoomPageTransitionsBuilder default route snapshotting behavior', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(themeAllowSnapshotting: true), ); @@ -247,7 +248,7 @@ void main() { expect(isTransitioningWithSnapshotting(tester, page1), isTrue); }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. - testWidgets('ZoomPageTransitionsBuilder.allowSnapshotting can disable route snapshotting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ZoomPageTransitionsBuilder.allowSnapshotting can disable route snapshotting', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(themeAllowSnapshotting: false), ); @@ -280,7 +281,7 @@ void main() { expect(isTransitioningWithSnapshotting(tester, page1), isFalse); }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. - testWidgets('Setting PageRoute.allowSnapshotting to false overrides ZoomPageTransitionsBuilder.allowSnapshotting = true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting PageRoute.allowSnapshotting to false overrides ZoomPageTransitionsBuilder.allowSnapshotting = true', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( themeAllowSnapshotting: true, @@ -305,7 +306,7 @@ void main() { await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. - testWidgets('_ZoomPageTransition only causes child widget built once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_ZoomPageTransition only causes child widget built once', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/58345 int builtCount = 0; diff --git a/packages/flutter/test/material/paginated_data_table_test.dart b/packages/flutter/test/material/paginated_data_table_test.dart index b68223c4b18b6..8004dc5312762 100644 --- a/packages/flutter/test/material/paginated_data_table_test.dart +++ b/packages/flutter/test/material/paginated_data_table_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'data_table_test_utils.dart'; @@ -66,9 +67,11 @@ class TestDataSource extends DataTableSource { void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('PaginatedDataTable paging', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); + late TestDataSource source; + setUp(() => source = TestDataSource()); + tearDown(() => source.dispose()); + testWidgetsWithLeakTracking('PaginatedDataTable paging', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(MaterialApp( @@ -170,9 +173,140 @@ void main() { log.clear(); }); - testWidgets('PaginatedDataTable control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable footer page number', (WidgetTester tester) async { + int rowsPerPage = 2; + + Widget buildTable(TestDataSource source, int rowsPerPage) { + return PaginatedDataTable( + header: const Text('Test table'), + source: source, + rowsPerPage: rowsPerPage, + showFirstLastButtons: true, + availableRowsPerPage: const <int>[ + 2, 3, 4, 5, 7, 8, + ], + onRowsPerPageChanged: (int? rowsPerPage) { + }, + onPageChanged: (int rowIndex) { + }, + columns: const <DataColumn>[ + DataColumn(label: Text('Name')), + DataColumn(label: Text('Calories'), numeric: true), + DataColumn(label: Text('Generation')), + ], + ); + } + + await tester.pumpWidget(MaterialApp( + home: buildTable(source, rowsPerPage) + )); + + expect(find.text('1–2 of 500'), findsOneWidget); + + await tester.tap(find.byTooltip('Next page')); + await tester.pump(); + + expect(find.text('3–4 of 500'), findsOneWidget); + + final Finder lastPageButton = find.ancestor( + of: find.byTooltip('Last page'), + matching: find.byWidgetPredicate((Widget widget) => widget is IconButton), + ); + + expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull); + + await tester.tap(lastPageButton); + await tester.pump(); + + expect(find.text('499–500 of 500'), findsOneWidget); + + final PaginatedDataTableState state = tester.state(find.byType(PaginatedDataTable)); + + state.pageTo(1); + rowsPerPage = 3; + + await tester.pumpWidget(MaterialApp( + home: buildTable(source, rowsPerPage) + )); + + expect(find.textContaining('1–3 of 500'), findsOneWidget); + + await tester.tap(find.byTooltip('Next page')); + await tester.pump(); + + expect(find.text('4–6 of 500'), findsOneWidget); + expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull); + + await tester.tap(lastPageButton); + await tester.pump(); + + expect(find.text('499–500 of 500'), findsOneWidget); + + state.pageTo(1); + rowsPerPage = 4; + + await tester.pumpWidget(MaterialApp( + home: buildTable(source, rowsPerPage) + )); + + expect(find.textContaining('1–4 of 500'), findsOneWidget); + + await tester.tap(find.byTooltip('Next page')); + await tester.pump(); + + expect(find.text('5–8 of 500'), findsOneWidget); + expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull); + + await tester.tap(lastPageButton); + await tester.pump(); + + expect(find.text('497–500 of 500'), findsOneWidget); + + state.pageTo(1); + rowsPerPage = 5; + + await tester.pumpWidget(MaterialApp( + home: buildTable(source, rowsPerPage) + )); + + expect(find.textContaining('1–5 of 500'), findsOneWidget); + + await tester.tap(find.byTooltip('Next page')); + await tester.pump(); + + expect(find.text('6–10 of 500'), findsOneWidget); + expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull); + + await tester.tap(lastPageButton); + await tester.pump(); + + expect(find.text('496–500 of 500'), findsOneWidget); + + state.pageTo(1); + rowsPerPage = 8; + + await tester.pumpWidget(MaterialApp( + home: buildTable(source, rowsPerPage) + )); + + expect(find.textContaining('1–8 of 500'), findsOneWidget); + + await tester.tap(find.byTooltip('Next page')); + await tester.pump(); + + expect(find.text('9–16 of 500'), findsOneWidget); + expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull); + + await tester.tap(lastPageButton); + await tester.pump(); + + expect(find.text('497–500 of 500'), findsOneWidget); + }); + + testWidgetsWithLeakTracking('PaginatedDataTable control test', (WidgetTester tester) async { TestDataSource source = TestDataSource() ..generation = 42; + addTearDown(source.dispose); final List<String> log = <String>[]; @@ -236,6 +370,7 @@ void main() { source = TestDataSource() ..generation = 15; + addTearDown(source.dispose); await tester.pumpWidget(MaterialApp( home: buildTable(source), @@ -263,11 +398,11 @@ void main() { log.clear(); }); - testWidgets('PaginatedDataTable text alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable text alignment', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: PaginatedDataTable( header: const Text('HEADER'), - source: TestDataSource(), + source: source, rowsPerPage: 8, availableRowsPerPage: const <int>[ 8, 9, @@ -285,17 +420,20 @@ void main() { expect(tester.getTopRight(find.text('8')).dx, tester.getTopRight(find.text('Rows per page:')).dx + 40.0); // per spec }); - testWidgets('PaginatedDataTable with and without header and actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable with and without header and actions', (WidgetTester tester) async { await binding.setSurfaceSize(const Size(800, 800)); const String headerText = 'HEADER'; final List<Widget> actions = <Widget>[ IconButton(onPressed: () {}, icon: const Icon(Icons.add)), ]; + final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); + Widget buildTable({String? header, List<Widget>? actions}) => MaterialApp( home: PaginatedDataTable( header: header != null ? Text(header) : null, actions: actions, - source: TestDataSource(allowSelection: true), + source: source, columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), @@ -321,8 +459,7 @@ void main() { await binding.setSurfaceSize(null); }); - testWidgets('PaginatedDataTable with large text', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); + testWidgetsWithLeakTracking('PaginatedDataTable with large text', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: MediaQuery( data: const MediaQueryData( @@ -358,8 +495,7 @@ void main() { expect(tester.getTopRight(find.text('501')).dx, greaterThanOrEqualTo(tester.getTopRight(find.text('Rows per page:')).dx + 40.0)); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/43433 - testWidgets('PaginatedDataTable footer scrolls', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); + testWidgetsWithLeakTracking('PaginatedDataTable footer scrolls', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Align( @@ -394,9 +530,7 @@ void main() { expect(tester.getTopLeft(find.text('Rows per page:')).dx, 18.0); // 14 padding in the footer row, 4 padding from the card }); - testWidgets('PaginatedDataTable custom row height', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); - + testWidgetsWithLeakTracking('PaginatedDataTable custom row height', (WidgetTester tester) async { Widget buildCustomHeightPaginatedTable({ double? dataRowHeight, double? dataRowMinHeight, @@ -486,7 +620,7 @@ void main() { ).size.height, 51.0); }); - testWidgets('PaginatedDataTable custom horizontal padding - checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable custom horizontal padding - checkbox', (WidgetTester tester) async { const double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; @@ -502,6 +636,7 @@ void main() { await binding.setSurfaceSize(const Size(width, height)); final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); Finder cellContent; Finder checkbox; Finder padding; @@ -649,12 +784,11 @@ void main() { await binding.setSurfaceSize(originalSize); }); - testWidgets('PaginatedDataTable custom horizontal padding - no checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable custom horizontal padding - no checkbox', (WidgetTester tester) async { const double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; const double customColumnSpacing = 15.0; - final TestDataSource source = TestDataSource(); Finder cellContent; Finder padding; @@ -772,9 +906,7 @@ void main() { ); }); - testWidgets('PaginatedDataTable table fills Card width', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); - + testWidgetsWithLeakTracking('PaginatedDataTable table fills Card width', (WidgetTester tester) async { // 800 is wide enough to ensure that all of the columns fit in the // Card. The test makes sure that the DataTable is exactly as wide // as the Card, minus the Card's margin. @@ -836,14 +968,16 @@ void main() { await binding.setSurfaceSize(originalSize); }); - testWidgets('PaginatedDataTable with optional column checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable with optional column checkbox', (WidgetTester tester) async { await binding.setSurfaceSize(const Size(800, 800)); addTearDown(() => binding.setSurfaceSize(null)); + final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); Widget buildTable(bool checkbox) => MaterialApp( home: PaginatedDataTable( header: const Text('Test table'), - source: TestDataSource(allowSelection: true), + source: source, showCheckboxColumn: checkbox, columns: const <DataColumn>[ DataColumn(label: Text('Name')), @@ -860,11 +994,14 @@ void main() { expect(find.byType(Checkbox), findsNothing); }); - testWidgets('Table should not use decoration from DataTableTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table should not use decoration from DataTableTheme', (WidgetTester tester) async { final Size originalSize = binding.renderView.size; await binding.setSurfaceSize(const Size(800, 800)); Widget buildTable() { + final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); + return MaterialApp( theme: ThemeData.light().copyWith( dataTableTheme: const DataTableThemeData( @@ -873,7 +1010,7 @@ void main() { ), home: PaginatedDataTable( header: const Text('Test table'), - source: TestDataSource(allowSelection: true), + source: source, columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), @@ -891,7 +1028,42 @@ void main() { await binding.setSurfaceSize(originalSize); }); - testWidgets('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dataRowMinHeight & dataRowMaxHeight if not set will use DataTableTheme', (WidgetTester tester) async { + addTearDown(() => binding.setSurfaceSize(null)); + await binding.setSurfaceSize(const Size(800, 800)); + + const double minMaxDataRowHeight = 30.0; + + final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + dataTableTheme: const DataTableThemeData( + dataRowMinHeight: minMaxDataRowHeight, + dataRowMaxHeight: minMaxDataRowHeight, + ), + ), + home: PaginatedDataTable( + header: const Text('Test table'), + source: source, + columns: const <DataColumn>[ + DataColumn(label: Text('Name')), + DataColumn(label: Text('Calories'), numeric: true), + DataColumn(label: Text('Generation')), + ], + ), + )); + + final Container rowContainer = tester.widget<Container>(find.descendant( + of: find.byType(Table), + matching: find.byType(Container), + ).last); + expect(rowContainer.constraints?.minHeight, minMaxDataRowHeight); + expect(rowContainer.constraints?.maxHeight, minMaxDataRowHeight); + }); + + testWidgetsWithLeakTracking('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (WidgetTester tester) async { const double customCheckboxHorizontalMargin = 15.0; const double customHorizontalMargin = 10.0; @@ -905,6 +1077,8 @@ void main() { await binding.setSurfaceSize(const Size(width, height)); final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); + Finder cellContent; Finder checkbox; Finder padding; @@ -957,17 +1131,20 @@ void main() { await binding.setSurfaceSize(originalSize); }); - testWidgets('Items selected text uses secondary color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Items selected text uses secondary color', (WidgetTester tester) async { const Color selectedTextColor = Color(0xff00ddff); final ColorScheme colors = const ColorScheme.light().copyWith(secondary: selectedTextColor); final ThemeData theme = ThemeData.from(colorScheme: colors); + final TestDataSource source = TestDataSource(allowSelection: true); + addTearDown(source.dispose); + Widget buildTable() { return MaterialApp( theme: theme, home: PaginatedDataTable( header: const Text('Test table'), - source: TestDataSource(allowSelection: true), + source: source, columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), @@ -996,7 +1173,7 @@ void main() { await binding.setSurfaceSize(null); }); - testWidgets('PaginatedDataTable arrowHeadColor set properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable arrowHeadColor set properly', (WidgetTester tester) async { await binding.setSurfaceSize(const Size(800, 800)); addTearDown(() => binding.setSurfaceSize(null)); const Color arrowHeadColor = Color(0xFFE53935); @@ -1007,7 +1184,7 @@ void main() { arrowHeadColor: arrowHeadColor, showFirstLastButtons: true, header: const Text('Test table'), - source: TestDataSource(), + source: source, columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), @@ -1025,7 +1202,7 @@ void main() { expect(icons.elementAt(3).color, arrowHeadColor); }); - testWidgets('OverflowBar header left alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar header left alignment', (WidgetTester tester) async { // Test an old special case that tried to align the first child of a ButtonBar // and the left edge of a Text header widget. Still possible with OverflowBar // albeit without any special case in the implementation's build method. @@ -1034,7 +1211,7 @@ void main() { home: PaginatedDataTable( header: header, rowsPerPage: 2, - source: TestDataSource(), + source: source, columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), @@ -1053,9 +1230,9 @@ void main() { expect(headerX, tester.getTopLeft(find.byType(ElevatedButton)).dx); }); - testWidgets('PaginatedDataTable can be scrolled using ScrollController', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); + testWidgetsWithLeakTracking('PaginatedDataTable can be scrolled using ScrollController', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildTable(TestDataSource source) { return Align( @@ -1102,9 +1279,9 @@ void main() { expect(scrollController.offset, 50.0); }); - testWidgets('PaginatedDataTable uses PrimaryScrollController when primary ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaginatedDataTable uses PrimaryScrollController when primary ', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); - final TestDataSource source = TestDataSource(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( MaterialApp( @@ -1133,4 +1310,32 @@ void main() { final Scrollable footerScrollView = tester.widget(find.byType(Scrollable).last); expect(footerScrollView.controller, null); }); + + testWidgetsWithLeakTracking('PaginatedDataTable custom heading row color', (WidgetTester tester) async { + const MaterialStateProperty<Color> headingRowColor = MaterialStatePropertyAll<Color>(Color(0xffFF0000)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PaginatedDataTable( + primary: true, + header: const Text('Test table'), + source: source, + rowsPerPage: 2, + headingRowColor: headingRowColor, + columns: const <DataColumn>[ + DataColumn(label: Text('Name')), + DataColumn(label: Text('Calories'), numeric: true), + DataColumn(label: Text('Generation')), + ], + ), + ), + ) + ); + + final Table table = tester.widget(find.byType(Table)); + final TableRow tableRow = table.children[0]; + final BoxDecoration tableRowBoxDecoration = tableRow.decoration! as BoxDecoration; + expect(tableRowBoxDecoration.color, headingRowColor.resolve(<MaterialState>{})); + }); } diff --git a/packages/flutter/test/material/persistent_bottom_sheet_test.dart b/packages/flutter/test/material/persistent_bottom_sheet_test.dart index 8c51e01ef030a..8254715db181e 100644 --- a/packages/flutter/test/material/persistent_bottom_sheet_test.dart +++ b/packages/flutter/test/material/persistent_bottom_sheet_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Pumps and ensures that the BottomSheet animates non-linearly. @@ -22,7 +23,7 @@ void main() { expect(dyDelta1, isNot(moreOrLessEquals(dyDelta2, epsilon: 0.1))); } - testWidgets('Persistent draggableScrollableSheet localHistoryEntries test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent draggableScrollableSheet localHistoryEntries test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110123 Widget buildFrame(Widget? bottomSheet) { return MaterialApp( @@ -78,7 +79,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/83668 - testWidgets('Scaffold.bottomSheet update test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.bottomSheet update test', (WidgetTester tester) async { Widget buildFrame(Widget? bottomSheet) { return MaterialApp( home: Scaffold( @@ -95,7 +96,7 @@ void main() { await tester.pumpWidget(buildFrame(const Text('I love Flutter!'))); }); - testWidgets('Verify that a BottomSheet can be rebuilt with ScaffoldFeatureController.setState()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a BottomSheet can be rebuilt with ScaffoldFeatureController.setState()', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); int buildCount = 0; @@ -122,7 +123,7 @@ void main() { expect(buildCount, equals(2)); }); - testWidgets('Verify that a persistent BottomSheet cannot be dismissed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a persistent BottomSheet cannot be dismissed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: const Center(child: Text('body')), @@ -153,7 +154,7 @@ void main() { expect(find.text('Two'), findsOneWidget); }); - testWidgets('Verify that a scrollable BottomSheet can be dismissed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a scrollable BottomSheet can be dismissed', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -185,7 +186,7 @@ void main() { expect(find.text('Two'), findsNothing); }); - testWidgets('Verify DraggableScrollableSheet.shouldCloseOnMinExtent == false prevents dismissal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify DraggableScrollableSheet.shouldCloseOnMinExtent == false prevents dismissal', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -223,7 +224,7 @@ void main() { expect(find.text('Two'), findsOneWidget); }); - testWidgets('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -258,7 +259,7 @@ void main() { expect(find.text('Two'), findsNothing); }); - testWidgets('Verify that a scrollControlled BottomSheet can be dismissed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a scrollControlled BottomSheet can be dismissed', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -297,7 +298,7 @@ void main() { expect(find.text('Two'), findsNothing); }); - testWidgets('Verify that a persistent BottomSheet can fling up and hide the fab', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a persistent BottomSheet can fling up and hide the fab', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -347,7 +348,7 @@ void main() { expect(find.byType(FloatingActionButton).hitTestable(), findsNothing); }); - testWidgets('Verify that a back button resets a persistent BottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a back button resets a persistent BottomSheet', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -408,7 +409,7 @@ void main() { expect(find.text('Item 22'), findsNothing); }); - testWidgets('Verify that a scrollable BottomSheet hides the fab when scrolled up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that a scrollable BottomSheet hides the fab when scrolled up', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -462,7 +463,7 @@ void main() { expect(find.byType(FloatingActionButton).hitTestable(), findsNothing); }); - testWidgets('showBottomSheet()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showBottomSheet()', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -486,7 +487,7 @@ void main() { expect(buildCount, equals(1)); }); - testWidgets('Scaffold removes top MediaQuery padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold removes top MediaQuery padding', (WidgetTester tester) async { late BuildContext scaffoldContext; late BuildContext bottomSheetContext; @@ -529,7 +530,7 @@ void main() { ); }); - testWidgets('Scaffold.bottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold.bottomSheet', (WidgetTester tester) async { final Key bottomSheetKey = UniqueKey(); await tester.pumpWidget( @@ -612,7 +613,7 @@ void main() { }, ); - testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that visual properties are passed through', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); const Color color = Colors.pink; const double elevation = 9.0; @@ -647,7 +648,7 @@ void main() { expect(bottomSheet.clipBehavior, clipBehavior); }); - testWidgets('PersistentBottomSheetController.close dismisses the bottom sheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PersistentBottomSheetController.close dismisses the bottom sheet', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); await tester.pumpWidget(MaterialApp( home: Scaffold( diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 13c24d0b8f028..5dec66dd1d442 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -9,13 +9,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; void main() { - testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async { final Key targetKey = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -65,7 +65,7 @@ void main() { expect(find.text('Next'), findsOneWidget); }); - testWidgets('PopupMenuButton calls onOpened callback when the menu is opened', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton calls onOpened callback when the menu is opened', (WidgetTester tester) async { int opens = 0; late BuildContext popupContext; final Key noItemsKey = UniqueKey(); @@ -134,7 +134,7 @@ void main() { expect(opens, equals(1)); }); - testWidgets('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async { int cancels = 0; late BuildContext popupContext; final Key noCallbackKey = UniqueKey(); @@ -200,7 +200,7 @@ void main() { expect(cancels, equals(2)); }); - testWidgets('disabled PopupMenuButton will not call itemBuilder, onOpened, onSelected or onCanceled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled PopupMenuButton will not call itemBuilder, onOpened, onSelected or onCanceled', (WidgetTester tester) async { final GlobalKey popupButtonKey = GlobalKey(); bool itemBuilderCalled = false; bool onOpenedCalled = false; @@ -285,7 +285,7 @@ void main() { expect(onCanceledCalled, isFalse); }); - testWidgets('disabled PopupMenuButton is not focusable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled PopupMenuButton is not focusable', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final GlobalKey childKey = GlobalKey(); bool itemBuilderCalled = false; @@ -327,7 +327,7 @@ void main() { expect(onSelectedCalled, isFalse); }); - testWidgets('disabled PopupMenuButton is focusable with directional navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled PopupMenuButton is focusable with directional navigation', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final GlobalKey childKey = GlobalKey(); @@ -368,7 +368,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue); }); - testWidgets('PopupMenuItem onTap callback is called when defined', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem onTap callback is called when defined', (WidgetTester tester) async { final List<int> menuItemTapCounters = <int>[0, 0]; await tester.pumpWidget( @@ -430,7 +430,7 @@ void main() { expect(menuItemTapCounters, <int>[2, 1]); }); - testWidgets('PopupMenuItem can have both onTap and value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem can have both onTap and value', (WidgetTester tester) async { final List<int> menuItemTapCounters = <int>[0, 0]; String? selected; @@ -501,7 +501,7 @@ void main() { expect(selected, 'third'); }); - testWidgets('PopupMenuItem is only focusable when enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem is only focusable when enabled', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final GlobalKey childKey = GlobalKey(); bool itemBuilderCalled = false; @@ -578,7 +578,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); }); - testWidgets('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async { Widget build(TargetPlatform platform) { debugDefaultTargetPlatformOverride = platform; return MaterialApp( @@ -632,7 +632,7 @@ void main() { ]; } - testWidgets('PopupMenuButton fails when given both child and icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton fails when given both child and icon', (WidgetTester tester) async { expect(() { PopupMenuButton<int>( icon: const Icon(Icons.view_carousel), @@ -642,7 +642,7 @@ void main() { }, throwsAssertionError); }); - testWidgets('PopupMenuButton creates IconButton when given an icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton creates IconButton when given an icon', (WidgetTester tester) async { final PopupMenuButton<int> button = PopupMenuButton<int>( icon: const Icon(Icons.view_carousel), itemBuilder: simplePopupMenuItemBuilder, @@ -662,7 +662,7 @@ void main() { }); }); - testWidgets('PopupMenu positioning', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu positioning', (WidgetTester tester) async { final Widget testButton = PopupMenuButton<int>( itemBuilder: (BuildContext context) { return <PopupMenuItem<int>>[ @@ -842,8 +842,10 @@ void main() { await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 500.0, 0.0, 0.0)); }); - testWidgets('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( MaterialApp( @@ -853,7 +855,7 @@ void main() { padding: const EdgeInsets.all(8.0), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (_) => Center( child: PopupMenuButton<int>( key: buttonKey, @@ -881,7 +883,7 @@ void main() { expect(tester.getTopLeft(popupFinder), buttonTopLeft); }); - testWidgets('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); await tester.pumpWidget( @@ -925,7 +927,7 @@ void main() { expect(tester.getTopLeft(popupFinder), buttonTopLeft); }); - testWidgets('PopupMenu positioning around display features', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu positioning around display features', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); await tester.pumpWidget( @@ -985,7 +987,7 @@ void main() { expect(tester.getTopRight(popupFinder), const Offset(390 - 8, 8)); }); - testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu removes MediaQuery padding', (WidgetTester tester) async { late BuildContext popupContext; await tester.pumpWidget(MaterialApp( @@ -1026,7 +1028,7 @@ void main() { expect(MediaQuery.of(popupContext).padding, EdgeInsets.zero); }); - testWidgets('Popup Menu Offset Test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup Menu Offset Test', (WidgetTester tester) async { PopupMenuButton<int> buildMenuButton({Offset offset = Offset.zero}) { return PopupMenuButton<int>( offset: offset, @@ -1085,7 +1087,7 @@ void main() { expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(50.0, 50.0)); }); - testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Opened PopupMenu has correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( @@ -1196,7 +1198,7 @@ void main() { ), TestSemantics( actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], - label: 'Dismiss', + label: 'Dismiss menu', textDirection: TextDirection.ltr, ), ], @@ -1209,7 +1211,7 @@ void main() { semantics.dispose(); }); - testWidgets('PopupMenuItem merges the semantics of its descendants', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem merges the semantics of its descendants', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( @@ -1284,7 +1286,7 @@ void main() { ), TestSemantics( actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], - label: 'Dismiss', + label: 'Dismiss menu', textDirection: TextDirection.ltr, ), ], @@ -1297,7 +1299,7 @@ void main() { semantics.dispose(); }); - testWidgets('disabled PopupMenuItem has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled PopupMenuItem has correct semantics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/45044. final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1407,7 +1409,7 @@ void main() { ), TestSemantics( actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], - label: 'Dismiss', + label: 'Dismiss menu', textDirection: TextDirection.ltr, ), ], @@ -1420,7 +1422,7 @@ void main() { semantics.dispose(); }); - testWidgets('PopupMenuButton PopupMenuDivider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton PopupMenuDivider', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/27072 late String selectedValue; @@ -1472,7 +1474,7 @@ void main() { expect(selectedValue, '2'); }); - testWidgets('PopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType; @@ -1554,7 +1556,83 @@ void main() { ); }); - testWidgets('PopupMenuItem custom padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - PopupMenuItem default padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + const PopupMenuItem<String>( + value: '0', + enabled: false, + child: Text('Item 0'), + ), + const PopupMenuItem<String>( + value: '1', + child: Text('Item 1'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, const EdgeInsets.symmetric(horizontal: 12.0)); + expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, const EdgeInsets.symmetric(horizontal: 12.0)); + }); + + testWidgetsWithLeakTracking('Material2 - PopupMenuItem default padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + const PopupMenuItem<String>( + value: '0', + enabled: false, + child: Text('Item 0'), + ), + const PopupMenuItem<String>( + value: '1', + child: Text('Item 1'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, const EdgeInsets.symmetric(horizontal: 16.0)); + expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, const EdgeInsets.symmetric(horizontal: 16.0)); + }); + + testWidgetsWithLeakTracking('PopupMenuItem custom padding', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType; @@ -1616,7 +1694,7 @@ void main() { expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20)); }); - testWidgets('CheckedPopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckedPopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType; @@ -1705,7 +1783,7 @@ void main() { ); }); - testWidgets('CheckedPopupMenuItem custom padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckedPopupMenuItem custom padding', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType; @@ -1766,7 +1844,7 @@ void main() { expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20)); }); - testWidgets('Update PopupMenuItem layout while the menu is visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update PopupMenuItem layout while the menu is visible', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType; @@ -1850,7 +1928,7 @@ void main() { }, throwsAssertionError); }); - testWidgets('PopupMenuButton default tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton default tooltip', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1906,7 +1984,7 @@ void main() { expect(find.byTooltip(const DefaultMaterialLocalizations().showMenuTooltip), findsNWidgets(3)); }); - testWidgets('PopupMenuButton custom tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton custom tooltip', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1963,7 +2041,7 @@ void main() { expect(find.byTooltip('Test tooltip'), findsNWidgets(3)); }); - testWidgets('Allow Widget for PopupMenuButton.icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Allow Widget for PopupMenuButton.icon', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1986,7 +2064,7 @@ void main() { expect(find.text('PopupMenuButton icon'), findsOneWidget); }); - testWidgets('showMenu uses nested navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showMenu uses nested navigator by default', (WidgetTester tester) async { final MenuObserver rootObserver = MenuObserver(); final MenuObserver nestedObserver = MenuObserver(); @@ -2024,7 +2102,7 @@ void main() { expect(nestedObserver.menuCount, 1); }); - testWidgets('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async { final MenuObserver rootObserver = MenuObserver(); final MenuObserver nestedObserver = MenuObserver(); @@ -2063,7 +2141,7 @@ void main() { expect(nestedObserver.menuCount, 0); }); - testWidgets('PopupMenuButton calling showButtonMenu manually', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton calling showButtonMenu manually', (WidgetTester tester) async { final GlobalKey<PopupMenuButtonState<int>> globalKey = GlobalKey(); await tester.pumpWidget( @@ -2098,7 +2176,7 @@ void main() { expect(find.text('Tap me please!'), findsOneWidget); }); - testWidgets('PopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async { const Key key = ValueKey<int>(1); // Test PopupMenuItem() constructor await tester.pumpWidget( @@ -2177,7 +2255,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('CheckedPopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CheckedPopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async { const Key key = ValueKey<int>(1); // Test CheckedPopupMenuItem() constructor await tester.pumpWidget( @@ -2257,7 +2335,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('PopupMenu in AppBar does not overlap with the status bar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu in AppBar does not overlap with the status bar', (WidgetTester tester) async { const List<PopupMenuItem<int>> choices = <PopupMenuItem<int>>[ PopupMenuItem<int>(value: 1, child: Text('Item 1')), PopupMenuItem<int>(value: 2, child: Text('Item 2')), @@ -2316,7 +2394,7 @@ void main() { expect(tester.getTopLeft(find.byWidget(firstItem)).dy, greaterThan(statusBarHeight)); }); - testWidgets('Vertically long PopupMenu does not overlap with the status bar and bottom notch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertically long PopupMenu does not overlap with the status bar and bottom notch', (WidgetTester tester) async { const double windowPaddingTop = 44; const double windowPaddingBottom = 34; @@ -2360,7 +2438,7 @@ void main() { expect(bottomRightOfMenu.dy, 600.0 - windowPaddingBottom - 8.0); // Screen height is 600. }); - testWidgets('PopupMenu position test when have unsafe area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu position test when have unsafe area', (WidgetTester tester) async { final GlobalKey buttonKey = GlobalKey(); Widget buildFrame(double width, double height) { @@ -2418,7 +2496,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/82874 - testWidgets('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async { final GlobalKey buttonKey = GlobalKey(); const EdgeInsets padding = EdgeInsets.only(left: 300.0, top: 32.0, right: 310.0, bottom: 64.0); EdgeInsets? mediaQueryPadding; @@ -2528,7 +2606,7 @@ void main() { ); } - testWidgets('PopupMenuButton enableFeedback works properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton enableFeedback works properly', (WidgetTester tester) async { expect(feedback.clickSoundCount, 0); expect(feedback.hapticCount, 0); @@ -2577,35 +2655,33 @@ void main() { }); }); - testWidgets('iconSize parameter tests', (WidgetTester tester) async { - Future<void> buildFrame({double? iconSize}) { - return tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Center( - child: PopupMenuButton<String>( - iconSize: iconSize, - itemBuilder: (_) => <PopupMenuEntry<String>>[ - const PopupMenuItem<String>( - value: 'value', - child: Text('child'), - ), - ], - ), + testWidgetsWithLeakTracking('Can customize PopupMenuButton icon', (WidgetTester tester) async { + const Color iconColor = Color(0xffffff00); + const double iconSize = 29.5; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + iconColor: iconColor, + iconSize: iconSize, + itemBuilder: (_) => <PopupMenuEntry<String>>[ + const PopupMenuItem<String>( + value: 'value', + child: Text('child'), + ), + ], ), ), ), - ); - } - - await buildFrame(); - expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(24, 24)); + ), + ); - await buildFrame(iconSize: 50); - expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(50, 50)); + expect(_iconStyle(tester, Icons.adaptive.more)?.color, iconColor); + expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(iconSize, iconSize)); }); - testWidgets('does not crash in small overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not crash in small overlay', (WidgetTester tester) async { final GlobalKey navigator = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -2646,7 +2722,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/80869 - testWidgets('The menu position test in the scrollable widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The menu position test in the scrollable widget', (WidgetTester tester) async { final GlobalKey buttonKey = GlobalKey(); await tester.pumpWidget( @@ -2710,7 +2786,7 @@ void main() { expect(popupMenu, const Offset(8.0, 50.0)); }); - testWidgets('PopupMenuButton custom splash radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuButton custom splash radius', (WidgetTester tester) async { Future<void> buildFrameWithoutChild({double? splashRadius}) { return tester.pumpWidget( MaterialApp( @@ -2768,7 +2844,7 @@ void main() { testSplashRadius); }); - testWidgets('Can override menu size constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can override menu size constraints', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType; @@ -2801,7 +2877,7 @@ void main() { expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).width, 500); }); - testWidgets('Can change menu position and offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can change menu position and offset', (WidgetTester tester) async { PopupMenuButton<int> buildMenuButton({required PopupMenuPosition position}) { return PopupMenuButton<int>( position: position, @@ -2930,7 +3006,7 @@ void main() { expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 90.0)); }); - testWidgets("PopupMenuButton icon inherits IconTheme's size", (WidgetTester tester) async { + testWidgetsWithLeakTracking("PopupMenuButton icon inherits IconTheme's size", (WidgetTester tester) async { Widget buildPopupMenu({double? themeIconSize, double? iconSize}) { return MaterialApp( theme: ThemeData( @@ -2972,7 +3048,7 @@ void main() { expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(50.0, 50.0)); }); - testWidgets('Popup menu clip behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu clip behavior', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/107215 final Key popupButtonKey = UniqueKey(); const double radius = 20.0; @@ -3024,7 +3100,7 @@ void main() { expect(material.clipBehavior, Clip.hardEdge); }); - testWidgets('Uses closed loop focus traversal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses closed loop focus traversal', (WidgetTester tester) async { FocusNode nodeA() => Focus.of(find.text('A').evaluate().single); FocusNode nodeB() => Focus.of(find.text('B').evaluate().single); @@ -3098,7 +3174,7 @@ void main() { expect(nodeB().hasFocus, false); }); - testWidgets('Popup menu scrollbar inherits ScrollbarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu scrollbar inherits ScrollbarTheme', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); const ScrollbarThemeData scrollbarTheme = ScrollbarThemeData( thumbColor: MaterialStatePropertyAll<Color?>(Color(0xffff0000)), @@ -3186,7 +3262,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Popup menu with RouteSettings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu with RouteSettings', (WidgetTester tester) async { late RouteSettings currentRouteSetting; await tester.pumpWidget( @@ -3228,7 +3304,7 @@ void main() { expect(currentRouteSetting.name, '/'); }); - testWidgets('Popup menu is positioned under the child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu is positioned under the child', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final Key childKey = UniqueKey(); await tester.pumpWidget( @@ -3266,7 +3342,7 @@ void main() { expect(childBottomLeft, menuTopLeft); }); - testWidgets('PopupmenuItem onTap should be calling after Navigator.pop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuItem onTap should be calling after Navigator.pop', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -3276,7 +3352,7 @@ void main() { itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[ PopupMenuItem<int>( onTap: () { - showModalBottomSheet( + showModalBottomSheet<void>( context: context, builder: (BuildContext context) { return const SizedBox( @@ -3307,6 +3383,408 @@ void main() { final Finder modalBottomSheet = find.text('ModalBottomSheet'); expect(modalBottomSheet, findsOneWidget); }); + + testWidgetsWithLeakTracking('Material3 - CheckedPopupMenuItem.labelTextStyle uses correct text style', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + ThemeData theme = ThemeData(useMaterial3: true); + + Widget buildMenu() { + return MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar( + actions: <Widget>[ + PopupMenuButton<void>( + key: popupMenuButtonKey, + itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[ + const CheckedPopupMenuItem<void>( + child: Text('Item 1'), + ), + const CheckedPopupMenuItem<int>( + checked: true, + child: Text('Item 2'), + ), + ], + ), + ], + ), + ), + ); + } + + await tester.pumpWidget(buildMenu()); + + // Show the menu + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test default text style. + expect(_labelStyle(tester, 'Item 1')!.fontSize, 14.0); + expect(_labelStyle(tester, 'Item 1')!.color, theme.colorScheme.onSurface); + + // Close the menu. + await tester.tapAt(const Offset(20.0, 20.0)); + await tester.pumpAndSettle(); + + // Test custom text theme. + const TextStyle customTextStyle = TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ); + theme = theme.copyWith( + textTheme: const TextTheme(labelLarge: customTextStyle), + ); + await tester.pumpWidget(buildMenu()); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test custom text theme. + expect(_labelStyle(tester, 'Item 1')!.fontSize, customTextStyle.fontSize); + expect(_labelStyle(tester, 'Item 1')!.fontWeight, customTextStyle.fontWeight); + expect(_labelStyle(tester, 'Item 1')!.fontStyle, customTextStyle.fontStyle); + }); + + testWidgetsWithLeakTracking('CheckedPopupMenuItem.labelTextStyle resolve material states', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + final MaterialStateProperty<TextStyle?> labelTextStyle = MaterialStateProperty.resolveWith( + (Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return const TextStyle(color: Colors.red, fontSize: 24.0); + } + + return const TextStyle(color: Colors.amber, fontSize: 20.0); + }); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + actions: <Widget>[ + PopupMenuButton<void>( + key: popupMenuButtonKey, + itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[ + CheckedPopupMenuItem<void>( + labelTextStyle: labelTextStyle, + child: const Text('Item 1'), + ), + CheckedPopupMenuItem<int>( + checked: true, + labelTextStyle: labelTextStyle, + child: const Text('Item 2'), + ), + ], + ), + ], + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + expect( + _labelStyle(tester, 'Item 1'), + labelTextStyle.resolve(<MaterialState>{}) + ); + expect( + _labelStyle(tester, 'Item 2'), + labelTextStyle.resolve(<MaterialState>{MaterialState.selected}) + ); + }); + + testWidgetsWithLeakTracking('CheckedPopupMenuItem overrides redundant ListTile.contentPadding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + const CheckedPopupMenuItem<String>( + value: '0', + child: Text('Item 0'), + ), + const CheckedPopupMenuItem<String>( + value: '1', + checked: true, + child: Text('Item 1'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + SafeArea getItemSafeArea(String label) { + return tester.widget<SafeArea>(find.ancestor( + of: find.text(label), + matching: find.byType(SafeArea), + )); + } + + expect(getItemSafeArea('Item 0').minimum, EdgeInsets.zero); + expect(getItemSafeArea('Item 1').minimum, EdgeInsets.zero); + }); + + testWidgetsWithLeakTracking('PopupMenuItem overrides redundant ListTile.contentPadding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + const PopupMenuItem<String>( + value: '0', + enabled: false, + child: ListTile(title: Text('Item 0')), + ), + const PopupMenuItem<String>( + value: '1', + child: ListTile(title: Text('Item 1')), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + SafeArea getItemSafeArea(String label) { + return tester.widget<SafeArea>(find.ancestor( + of: find.text(label), + matching: find.byType(SafeArea), + )); + } + + expect(getItemSafeArea('Item 0').minimum, EdgeInsets.zero); + expect(getItemSafeArea('Item 1').minimum, EdgeInsets.zero); + }); + + testWidgetsWithLeakTracking('Material3 - PopupMenuItem overrides ListTile.titleTextStyle', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + ThemeData theme = ThemeData(useMaterial3: true); + + Widget buildMenu() { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + // Popup menu item with a Text widget. + const PopupMenuItem<String>( + value: '0', + child: Text('Item 0'), + ), + // Popup menu item with a ListTile widget. + const PopupMenuItem<String>( + value: '1', + child: ListTile(title: Text('Item 1')), + ), + ]; + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildMenu()); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test popup menu item with a Text widget. + expect(_labelStyle(tester, 'Item 0')!.fontSize, 14.0); + expect(_labelStyle(tester, 'Item 0')!.color, theme.colorScheme.onSurface); + + // Test popup menu item with a ListTile widget. + expect(_labelStyle(tester, 'Item 1')!.fontSize, 14.0); + expect(_labelStyle(tester, 'Item 1')!.color, theme.colorScheme.onSurface); + + // Close the menu. + await tester.tapAt(const Offset(20.0, 20.0)); + await tester.pumpAndSettle(); + + // Test custom text theme. + const TextStyle customTextStyle = TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ); + theme = theme.copyWith( + textTheme: const TextTheme(labelLarge: customTextStyle), + ); + await tester.pumpWidget(buildMenu()); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test popup menu item with a Text widget with custom text theme. + expect(_labelStyle(tester, 'Item 0')!.fontSize, customTextStyle.fontSize); + expect(_labelStyle(tester, 'Item 0')!.fontWeight, customTextStyle.fontWeight); + expect(_labelStyle(tester, 'Item 0')!.fontStyle, customTextStyle.fontStyle); + + // Test popup menu item with a ListTile widget with custom text theme. + expect(_labelStyle(tester, 'Item 1')!.fontSize, customTextStyle.fontSize); + expect(_labelStyle(tester, 'Item 1')!.fontWeight, customTextStyle.fontWeight); + expect(_labelStyle(tester, 'Item 1')!.fontStyle, customTextStyle.fontStyle); + }); + + testWidgetsWithLeakTracking('Material2 - PopupMenuItem overrides ListTile.titleTextStyle', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + ThemeData theme = ThemeData(useMaterial3: false); + + Widget buildMenu() { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Center( + child: PopupMenuButton<String>( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return <PopupMenuEntry<String>>[ + // Popup menu item with a Text widget. + const PopupMenuItem<String>( + value: '0', + child: Text('Item 0'), + ), + // Popup menu item with a ListTile widget. + const PopupMenuItem<String>( + value: '1', + child: ListTile(title: Text('Item 1')), + ), + ]; + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildMenu()); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test popup menu item with a Text widget. + expect(_labelStyle(tester, 'Item 0')!.fontSize, 16.0); + expect(_labelStyle(tester, 'Item 0')!.color, theme.textTheme.subtitle1!.color); + + // Test popup menu item with a ListTile widget. + expect(_labelStyle(tester, 'Item 1')!.fontSize, 16.0); + expect(_labelStyle(tester, 'Item 1')!.color, theme.textTheme.subtitle1!.color); + + // Close the menu. + await tester.tapAt(const Offset(20.0, 20.0)); + await tester.pumpAndSettle(); + + // Test custom text theme. + const TextStyle customTextStyle = TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ); + theme = theme.copyWith( + textTheme: const TextTheme(subtitle1: customTextStyle), + ); + await tester.pumpWidget(buildMenu()); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // Test popup menu item with a Text widget with custom text style. + expect(_labelStyle(tester, 'Item 0')!.fontSize, customTextStyle.fontSize); + expect(_labelStyle(tester, 'Item 0')!.fontWeight, customTextStyle.fontWeight); + expect(_labelStyle(tester, 'Item 0')!.fontStyle, customTextStyle.fontStyle); + + // Test popup menu item with a ListTile widget with custom text style. + expect(_labelStyle(tester, 'Item 1')!.fontSize, customTextStyle.fontSize); + expect(_labelStyle(tester, 'Item 1')!.fontWeight, customTextStyle.fontWeight); + expect(_labelStyle(tester, 'Item 1')!.fontStyle, customTextStyle.fontStyle); + }); + + testWidgetsWithLeakTracking('CheckedPopupMenuItem.onTap callback is called when defined', (WidgetTester tester) async { + int count = 0; + await tester.pumpWidget( + TestApp( + textDirection: TextDirection.ltr, + child: Material( + child: RepaintBoundary( + child: PopupMenuButton<String>( + child: const Text('button'), + itemBuilder: (BuildContext context) { + return <PopupMenuItem<String>>[ + CheckedPopupMenuItem<String>( + onTap: () { + count += 1; + }, + value: 'item1', + child: const Text('Item with onTap'), + ), + const CheckedPopupMenuItem<String>( + value: 'item2', + child: Text('Item without onTap'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Tap a checked menu item with onTap. + await tester.tap(find.text('button')); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(CheckedPopupMenuItem<String>, 'Item with onTap')); + await tester.pumpAndSettle(); + expect(count, 1); + + // Tap a checked menu item without onTap. + await tester.tap(find.text('button')); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(CheckedPopupMenuItem<String>, 'Item without onTap')); + await tester.pumpAndSettle(); + expect(count, 1); + }); } class TestApp extends StatelessWidget { @@ -3377,3 +3855,17 @@ class _ClosureNavigatorObserver extends NavigatorObserver { @override void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!); } + +TextStyle? _labelStyle(WidgetTester tester, String label) { + return tester.widget<RichText>(find.descendant( + of: find.text(label), + matching: find.byType(RichText), + )).text.style; +} + +TextStyle? _iconStyle(WidgetTester tester, IconData icon) { + return tester.widget<RichText>(find.descendant( + of: find.byIcon(icon), + matching: find.byType(RichText), + )).text.style; +} diff --git a/packages/flutter/test/material/popup_menu_theme_test.dart b/packages/flutter/test/material/popup_menu_theme_test.dart index 4b37dedb96a3c..f17caec690199 100644 --- a/packages/flutter/test/material/popup_menu_theme_test.dart +++ b/packages/flutter/test/material/popup_menu_theme_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; PopupMenuThemeData _popupMenuThemeM2() { return PopupMenuThemeData( @@ -41,6 +42,8 @@ PopupMenuThemeData _popupMenuThemeM3() { } return SystemMouseCursors.alias; }), + iconColor: const Color(0xfff12099), + iconSize: 17.0, ); } @@ -69,7 +72,7 @@ void main() { expect(popupMenuTheme.mouseCursor, null); }); - testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const PopupMenuThemeData().debugFillProperties(builder); @@ -81,22 +84,26 @@ void main() { expect(description, <String>[]); }); - testWidgets('PopupMenuThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenuThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); PopupMenuThemeData( - color: const Color(0xFFFFFFFF), + color: const Color(0xfffffff1), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), elevation: 2.0, - shadowColor: const Color(0xff00ff00), - surfaceTintColor: const Color(0xff00ff00), - textStyle: const TextStyle(color: Color(0xffffffff)), + shadowColor: const Color(0xfffffff2), + surfaceTintColor: const Color(0xfffffff3), + textStyle: const TextStyle(color: Color(0xfffffff4)), labelTextStyle: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { - return const TextStyle(color: Color(0xfff99ff0), fontSize: 12.0); + return const TextStyle(color: Color(0xfffffff5), fontSize: 12.0); } - return const TextStyle(color: Color(0xfff12099), fontSize: 17.0); + return const TextStyle(color: Color(0xfffffff6), fontSize: 17.0); }), + enableFeedback: false, mouseCursor: MaterialStateMouseCursor.clickable, + position: PopupMenuPosition.over, + iconColor: const Color(0xfffffff8), + iconSize: 31.0, ).debugFillProperties(builder); final List<String> description = builder.properties @@ -105,18 +112,22 @@ void main() { .toList(); expect(description, <String>[ - 'color: Color(0xffffffff)', + 'color: Color(0xfffffff1)', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'elevation: 2.0', - 'shadowColor: Color(0xff00ff00)', - 'surfaceTintColor: Color(0xff00ff00)', - 'text style: TextStyle(inherit: true, color: Color(0xffffffff))', + 'shadowColor: Color(0xfffffff2)', + 'surfaceTintColor: Color(0xfffffff3)', + 'text style: TextStyle(inherit: true, color: Color(0xfffffff4))', "labelTextStyle: Instance of '_MaterialStatePropertyWith<TextStyle?>'", + 'enableFeedback: false', 'mouseCursor: MaterialStateMouseCursor(clickable)', + 'position: over', + 'iconColor: Color(0xfffffff8)', + 'iconSize: 31.0' ]); }); - testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); final Key enabledPopupItemKey = UniqueKey(); @@ -147,6 +158,13 @@ void main() { enabled: false, child: const Text('Disabled PopupMenuItem'), ), + const CheckedPopupMenuItem<void>( + child: Text('Unchecked item'), + ), + const CheckedPopupMenuItem<void>( + checked: true, + child: Text('Checked item'), + ), ]; }, ), @@ -156,6 +174,9 @@ void main() { ), )); + // Test default button icon color. + expect(_iconStyle(tester, Icons.adaptive.more)?.color, theme.iconTheme.color); + await tester.tap(find.byKey(popupButtonKey)); await tester.pumpAndSettle(); @@ -179,22 +200,23 @@ void main() { /// [PopupMenuItem] specified above, so by finding the last descendent of /// popupItemKey that is of type DefaultTextStyle, this code retrieves the /// built [PopupMenuItem]. - final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>( + DefaultTextStyle popupMenuItemLabel = tester.widget<DefaultTextStyle>( find.descendant( of: find.byKey(enabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); - expect(enabledText.style.fontFamily, 'Roboto'); - expect(enabledText.style.color, theme.colorScheme.onSurface); + expect(popupMenuItemLabel.style.fontFamily, 'Roboto'); + expect(popupMenuItemLabel.style.color, theme.colorScheme.onSurface); + /// Test disabled text color - final DefaultTextStyle disabledText = tester.widget<DefaultTextStyle>( + popupMenuItemLabel = tester.widget<DefaultTextStyle>( find.descendant( of: find.byKey(disabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); - expect(disabledText.style.color, theme.colorScheme.onSurface.withOpacity(0.38)); + expect(popupMenuItemLabel.style.color, theme.colorScheme.onSurface.withOpacity(0.38)); final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>)); final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); @@ -215,9 +237,17 @@ void main() { RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click, ); + + // Test unchecked CheckedPopupMenuItem label. + ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first); + expect(listTile.titleTextStyle?.color, theme.colorScheme.onSurface); + + // Test checked CheckedPopupMenuItem label. + listTile = tester.widget<ListTile>(find.byType(ListTile).last); + expect(listTile.titleTextStyle?.color, theme.colorScheme.onSurface); }); - testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM3(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); @@ -249,6 +279,13 @@ void main() { onTap: () { }, child: const Text('enabled'), ), + const CheckedPopupMenuItem<Object>( + child: Text('Unchecked item'), + ), + const CheckedPopupMenuItem<Object>( + checked: true, + child: Text('Checked item'), + ), ]; }, ), @@ -257,6 +294,9 @@ void main() { ), )); + expect(_iconStyle(tester, Icons.adaptive.more)?.color, popupMenuTheme.iconColor); + expect(tester.getSize(find.byIcon(Icons.adaptive.more)), Size(popupMenuTheme.iconSize!, popupMenuTheme.iconSize!)); + await tester.tap(find.byKey(popupButtonKey)); await tester.pumpAndSettle(); @@ -276,25 +316,25 @@ void main() { expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12)))); expect(button.elevation, 12.0); - final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>( + DefaultTextStyle popupMenuItemLabel = tester.widget<DefaultTextStyle>( find.descendant( of: find.byKey(enabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); expect( - enabledText.style, + popupMenuItemLabel.style, popupMenuTheme.labelTextStyle?.resolve(enabled), ); /// Test disabled text color - final DefaultTextStyle disabledText = tester.widget<DefaultTextStyle>( + popupMenuItemLabel = tester.widget<DefaultTextStyle>( find.descendant( of: find.byKey(disabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); expect( - disabledText.style, + popupMenuItemLabel.style, popupMenuTheme.labelTextStyle?.resolve(disabled), ); @@ -313,23 +353,33 @@ void main() { RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), popupMenuTheme.mouseCursor?.resolve(enabled), ); + + // Test unchecked CheckedPopupMenuItem label. + ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first); + expect(listTile.titleTextStyle, popupMenuTheme.labelTextStyle?.resolve(enabled)); + + // Test checked CheckedPopupMenuItem label. + listTile = tester.widget<ListTile>(find.byType(ListTile).last); + expect(listTile.titleTextStyle, popupMenuTheme.labelTextStyle?.resolve(enabled)); }); - testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu widget properties take priority over theme', (WidgetTester tester) async { final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM3(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); final Key popupItemKey = UniqueKey(); - const Color color = Colors.purple; - const Color surfaceTintColor = Colors.amber; - const Color shadowColor = Colors.green; + const Color color = Color(0xfff11fff); + const Color surfaceTintColor = Color(0xfff12fff); + const Color shadowColor = Color(0xfff13fff); const ShapeBorder shape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(9.0)), ); const double elevation = 7.0; - const TextStyle textStyle = TextStyle(color: Color(0xffffffef), fontSize: 19.0); + const TextStyle textStyle = TextStyle(color: Color(0xfff14fff), fontSize: 19.0); const MouseCursor cursor = SystemMouseCursors.forbidden; + const Color iconColor = Color(0xfff15fff); + const double iconSize = 21.5; await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme), @@ -344,6 +394,8 @@ void main() { surfaceTintColor: surfaceTintColor, color: color, shape: shape, + iconColor: iconColor, + iconSize: iconSize, itemBuilder: (BuildContext context) { return <PopupMenuEntry<void>>[ PopupMenuItem<void>( @@ -352,6 +404,11 @@ void main() { mouseCursor: cursor, child: const Text('Example'), ), + CheckedPopupMenuItem<void>( + checked: true, + labelTextStyle: MaterialStateProperty.all<TextStyle>(textStyle), + child: const Text('Checked item'), + ) ]; }, ), @@ -360,6 +417,9 @@ void main() { ), )); + expect(_iconStyle(tester, Icons.adaptive.more)?.color, iconColor); + expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(iconSize, iconSize)); + await tester.tap(find.byKey(popupButtonKey)); await tester.pumpAndSettle(); @@ -397,6 +457,10 @@ void main() { await gesture.moveTo(tester.getCenter(find.byKey(popupItemKey))); await tester.pumpAndSettle(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), cursor); + + // Test CheckedPopupMenuItem label. + final ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first); + expect(listTile.titleTextStyle, textStyle); }); group('Material 2', () { @@ -404,7 +468,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); final Key enabledPopupItemKey = UniqueKey(); @@ -503,7 +567,7 @@ void main() { ); }); - testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM2(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); @@ -589,7 +653,7 @@ void main() { ); }); - testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popup menu widget properties take priority over theme', (WidgetTester tester) async { final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM2(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); @@ -677,3 +741,10 @@ void main() { Set<MaterialState> enabled = <MaterialState>{}; Set<MaterialState> disabled = <MaterialState>{MaterialState.disabled}; + +TextStyle? _iconStyle(WidgetTester tester, IconData icon) { + return tester.widget<RichText>(find.descendant( + of: find.byIcon(icon), + matching: find.byType(RichText), + )).text.style; +} diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index c68e3507a8975..7537bf97871e1 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -17,8 +17,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final ThemeData theme = ThemeData(); @@ -26,7 +25,7 @@ void main() { // The "can be constructed" tests that follow are primarily to ensure that any // animations started by the progress indicators are stopped at dispose() time. - testWidgets('LinearProgressIndicator(value: 0.0) can be constructed and has empty semantics by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator(value: 0.0) can be constructed and has empty semantics by default', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Theme( @@ -47,7 +46,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator(value: null) can be constructed and has empty semantics by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator(value: null) can be constructed and has empty semantics by default', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Theme( @@ -68,7 +67,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator custom minHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator custom minHeight', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -117,7 +116,7 @@ void main() { ); }); - testWidgets('LinearProgressIndicator paint (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator paint (LTR)', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -143,7 +142,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('LinearProgressIndicator paint (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator paint (RTL)', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -169,7 +168,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('LinearProgressIndicator indeterminate (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator indeterminate (LTR)', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -199,7 +198,7 @@ void main() { expect(tester.binding.transientCallbackCount, 1); }); - testWidgets('LinearProgressIndicator paint (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator paint (RTL)', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -229,7 +228,7 @@ void main() { expect(tester.binding.transientCallbackCount, 1); }); - testWidgets('LinearProgressIndicator with colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with colors', (WidgetTester tester) async { // With valueColor & color provided await tester.pumpWidget( Theme( @@ -349,7 +348,7 @@ void main() { }); - testWidgets('LinearProgressIndicator with animation with null colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with animation with null colors', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -377,7 +376,7 @@ void main() { ); }); - testWidgets('CircularProgressIndicator(value: 0.0) can be constructed and has value semantics by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator(value: 0.0) can be constructed and has value semantics by default', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Theme( @@ -398,7 +397,7 @@ void main() { handle.dispose(); }); - testWidgets('CircularProgressIndicator(value: null) can be constructed and has empty semantics by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator(value: null) can be constructed and has empty semantics by default', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Theme( @@ -413,7 +412,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator causes a repaint when it changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator causes a repaint when it changes', (WidgetTester tester) async { await tester.pumpWidget(Theme( data: theme, child: Directionality( @@ -433,7 +432,7 @@ void main() { expect(layers1, isNot(equals(layers2))); }); - testWidgets('CircularProgressIndicator stroke width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator stroke width', (WidgetTester tester) async { await tester.pumpWidget(Theme(data: theme, child: const CircularProgressIndicator())); expect(find.byType(CircularProgressIndicator), paints..arc(strokeWidth: 4.0)); @@ -443,7 +442,7 @@ void main() { expect(find.byType(CircularProgressIndicator), paints..arc(strokeWidth: 16.0)); }); - testWidgets('CircularProgressIndicator strokeAlign', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator strokeAlign', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -484,7 +483,7 @@ void main() { expect(find.byType(CircularProgressIndicator), paints..arc(rect: const Offset(-4.0, -4.0) & const Size(808.0, 608.0))); }); - testWidgets('CircularProgressIndicator with strokeCap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator with strokeCap', (WidgetTester tester) async { await tester.pumpWidget(const CircularProgressIndicator()); expect(find.byType(CircularProgressIndicator), paints..arc(strokeCap: StrokeCap.square), @@ -506,7 +505,7 @@ void main() { expect(find.byType(CircularProgressIndicator), paints..arc(strokeCap: StrokeCap.round)); }); - testWidgets('LinearProgressIndicator with indicatorBorderRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with indicatorBorderRadius', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -541,7 +540,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('CircularProgressIndicator paint colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator paint colors', (WidgetTester tester) async { const Color green = Color(0xFF00FF00); const Color blue = Color(0xFF0000FF); const Color red = Color(0xFFFF0000); @@ -598,7 +597,7 @@ void main() { expect(find.byType(CircularProgressIndicator), paints..arc(color: blue)..arc(color: green)); }); - testWidgets('RefreshProgressIndicator paint colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshProgressIndicator paint colors', (WidgetTester tester) async { const Color green = Color(0xFF00FF00); const Color blue = Color(0xFF0000FF); const Color red = Color(0xFFFF0000); @@ -667,7 +666,7 @@ void main() { expect(themeBackgroundMaterial.color, blue); }); - testWidgets('RefreshProgressIndicator with a round indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshProgressIndicator with a round indicator', (WidgetTester tester) async { await tester.pumpWidget(const RefreshProgressIndicator()); expect(find.byType(RefreshProgressIndicator), paints..arc(strokeCap: StrokeCap.square), @@ -690,7 +689,7 @@ void main() { expect(find.byType(RefreshProgressIndicator), paints..arc(strokeCap: StrokeCap.round)); }); - testWidgets('Indeterminate RefreshProgressIndicator keeps spinning until end of time (approximate)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Indeterminate RefreshProgressIndicator keeps spinning until end of time (approximate)', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/13782 await tester.pumpWidget( @@ -719,8 +718,9 @@ void main() { expect(tester.hasRunningAnimations, isTrue); }); - testWidgets('RefreshProgressIndicator uses expected animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - RefreshProgressIndicator uses expected animation', (WidgetTester tester) async { final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(50, 50)); + addTearDown(animationSheet.dispose); await tester.pumpFrames(animationSheet.record( Theme( @@ -730,12 +730,29 @@ void main() { ), const Duration(seconds: 3)); await expectLater( - await animationSheet.collate(20), - matchesGoldenFile('material.refresh_progress_indicator.png'), + animationSheet.collate(20), + matchesGoldenFile('m2_material.refresh_progress_indicator.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + testWidgetsWithLeakTracking('Material3 - RefreshProgressIndicator uses expected animation', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(50, 50)); + addTearDown(animationSheet.dispose); + + await tester.pumpFrames(animationSheet.record( + Theme( + data: ThemeData(useMaterial3: true), + child: const _RefreshProgressIndicatorGolden() + ), + ), const Duration(seconds: 3)); + + await expectLater( + animationSheet.collate(20), + matchesGoldenFile('m3_material.refresh_progress_indicator.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 - testWidgets('Determinate CircularProgressIndicator stops the animator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Determinate CircularProgressIndicator stops the animator', (WidgetTester tester) async { double? progressValue; late StateSetter setState; await tester.pumpWidget( @@ -765,7 +782,7 @@ void main() { expect(tester.hasRunningAnimations, isTrue); }); - testWidgets('LinearProgressIndicator with height 12.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with height 12.0', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -790,7 +807,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('LinearProgressIndicator with a height less than the minimum', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with a height less than the minimum', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -815,7 +832,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('LinearProgressIndicator with default height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator with default height', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: theme, @@ -840,7 +857,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('LinearProgressIndicator can be made accessible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator can be made accessible', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const String label = 'Label'; @@ -869,7 +886,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator that is determinate gets default a11y value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator that is determinate gets default a11y value', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const String label = 'Label'; @@ -896,7 +913,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator that is determinate does not default a11y value when label is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator that is determinate does not default a11y value when label is null', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -917,7 +934,7 @@ void main() { handle.dispose(); }); - testWidgets('LinearProgressIndicator that is indeterminate does not default a11y value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinearProgressIndicator that is indeterminate does not default a11y value', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const String label = 'Progress'; @@ -943,7 +960,7 @@ void main() { handle.dispose(); }); - testWidgets('CircularProgressIndicator can be made accessible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CircularProgressIndicator can be made accessible', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const String label = 'Label'; @@ -972,7 +989,7 @@ void main() { handle.dispose(); }); - testWidgets('RefreshProgressIndicator can be made accessible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshProgressIndicator can be made accessible', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey key = GlobalKey(); const String label = 'Label'; @@ -1000,8 +1017,9 @@ void main() { handle.dispose(); }); - testWidgets('Indeterminate CircularProgressIndicator uses expected animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Indeterminate CircularProgressIndicator uses expected animation', (WidgetTester tester) async { final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(40, 40)); + addTearDown(animationSheet.dispose); await tester.pumpFrames(animationSheet.record( Theme( @@ -1017,12 +1035,35 @@ void main() { ), const Duration(seconds: 2)); await expectLater( - await animationSheet.collate(20), - matchesGoldenFile('material.circular_progress_indicator.indeterminate.png'), + animationSheet.collate(20), + matchesGoldenFile('m2_material.circular_progress_indicator.indeterminate.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + testWidgetsWithLeakTracking('Material3 - Indeterminate CircularProgressIndicator uses expected animation', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(40, 40)); + addTearDown(animationSheet.dispose); + + await tester.pumpFrames(animationSheet.record( + Theme( + data: ThemeData(useMaterial3: true), + child: const Directionality( + textDirection: TextDirection.ltr, + child: Padding( + padding: EdgeInsets.all(4), + child: CircularProgressIndicator(), + ), + ), + ), + ), const Duration(seconds: 2)); + + await expectLater( + animationSheet.collate(20), + matchesGoldenFile('m3_material.circular_progress_indicator.indeterminate.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 - testWidgets( + testWidgetsWithLeakTracking( 'Adaptive CircularProgressIndicator displays CupertinoActivityIndicator in iOS', (WidgetTester tester) async { await tester.pumpWidget( @@ -1044,7 +1085,7 @@ void main() { }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Adaptive CircularProgressIndicator can use backgroundColor to change tick color for iOS', (WidgetTester tester) async { await tester.pumpWidget( @@ -1073,7 +1114,7 @@ void main() { }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Adaptive CircularProgressIndicator does not display CupertinoActivityIndicator in non-iOS', (WidgetTester tester) async { await tester.pumpWidget( @@ -1097,7 +1138,7 @@ void main() { }), ); - testWidgets('ProgressIndicatorTheme.wrap() always creates a new ProgressIndicatorTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ProgressIndicatorTheme.wrap() always creates a new ProgressIndicatorTheme', (WidgetTester tester) async { late BuildContext builderContext; @@ -1128,7 +1169,7 @@ void main() { expect((wrappedTheme as ProgressIndicatorTheme).data, themeData); }); - testWidgets('default size of CircularProgressIndicator is 36x36 - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default size of CircularProgressIndicator is 36x36 - M3', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: theme.copyWith(useMaterial3: true), @@ -1142,6 +1183,75 @@ void main() { expect(tester.getSize(find.byType(CircularProgressIndicator)), const Size(36, 36)); }); + + testWidgetsWithLeakTracking('RefreshProgressIndicator using fields correctly', (WidgetTester tester) async { + Future<void> pumpIndicator(RefreshProgressIndicator indicator) { + return tester.pumpWidget(Theme(data: theme, child: indicator)); + } + + // With default values. + await pumpIndicator(const RefreshProgressIndicator()); + Material material = tester.widget( + find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Material), + ), + ); + Container container = tester.widget( + find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Container), + ), + ); + Padding padding = tester.widget( + find.descendant( + of: find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Material), + ), + matching: find.byType(Padding), + ), + ); + expect(material.elevation, 2.0); + expect(container.margin, const EdgeInsets.all(4.0)); + expect(padding.padding, const EdgeInsets.all(12.0)); + + // With values provided. + const double testElevation = 1.0; + const EdgeInsetsGeometry testIndicatorMargin = EdgeInsets.all(6.0); + const EdgeInsetsGeometry testIndicatorPadding = EdgeInsets.all(10.0); + await pumpIndicator( + const RefreshProgressIndicator( + elevation: testElevation, + indicatorMargin: testIndicatorMargin, + indicatorPadding: testIndicatorPadding, + ), + ); + material = tester.widget( + find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Material), + ), + ); + container = tester.widget( + find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Container), + ), + ); + padding = tester.widget( + find.descendant( + of: find.descendant( + of: find.byType(RefreshProgressIndicator), + matching: find.byType(Material), + ), + matching: find.byType(Padding), + ), + ); + expect(material.elevation, testElevation); + expect(container.margin, testIndicatorMargin); + expect(padding.padding, testIndicatorPadding); + }); } class _RefreshProgressIndicatorGolden extends StatefulWidget { diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart index 9e0cba869a568..6793eb6ed8ec5 100644 --- a/packages/flutter/test/material/radio_list_tile_test.dart +++ b/packages/flutter/test/material/radio_list_tile_test.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -24,7 +24,7 @@ Widget wrap({Widget? child}) { } void main() { - testWidgets('RadioListTile should initialize according to groupValue', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile should initialize according to groupValue', (WidgetTester tester) async { final List<int> values = <int>[0, 1, 2]; int? selectedValue; // Constructor parameters are required for [RadioListTile], but they are @@ -84,7 +84,7 @@ void main() { expect(generatedRadioListTiles[2].checked, equals(false)); }); - testWidgets('RadioListTile simple control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile simple control test', (WidgetTester tester) async { final Key key = UniqueKey(); final Key titleKey = UniqueKey(); final List<int?> log = <int?>[]; @@ -156,7 +156,7 @@ void main() { expect(log, equals(<int>[1])); }); - testWidgets('RadioListTile control tests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile control tests', (WidgetTester tester) async { final List<int> values = <int>[0, 1, 2]; int? selectedValue; // Constructor parameters are required for [Radio], but they are irrelevant @@ -223,7 +223,7 @@ void main() { expect(log, equals(<dynamic>[1, '-', 2])); }); - testWidgets('Selected RadioListTile should not trigger onChanged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selected RadioListTile should not trigger onChanged', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/30311 final List<int> values = <int>[0, 1, 2]; int? selectedValue; @@ -275,7 +275,7 @@ void main() { expect(log, equals(<int>[0])); }); - testWidgets('Selected RadioListTile should trigger onChanged when toggleable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selected RadioListTile should trigger onChanged when toggleable', (WidgetTester tester) async { final List<int> values = <int>[0, 1, 2]; int? selectedValue; // Constructor parameters are required for [Radio], but they are irrelevant @@ -329,7 +329,7 @@ void main() { expect(log, equals(<int?>[0, null, 0])); }); - testWidgets('RadioListTile can be toggled when toggleable is set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile can be toggled when toggleable is set', (WidgetTester tester) async { final Key key = UniqueKey(); final List<int?> log = <int?>[]; @@ -384,7 +384,7 @@ void main() { expect(log, equals(<int>[1])); }); - testWidgets('RadioListTile semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -533,7 +533,7 @@ void main() { semantics.dispose(); }); - testWidgets('RadioListTile has semantic events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile has semantic events', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); dynamic semanticEvent; @@ -572,7 +572,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('RadioListTile can autofocus unless disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile can autofocus unless disabled.', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( @@ -606,7 +606,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); }); - testWidgets('RadioListTile contentPadding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile contentPadding test', (WidgetTester tester) async { final Type radioType = const Radio<bool>( groupValue: true, value: true, @@ -646,7 +646,7 @@ void main() { expect(paddingRect.right, titleRect.right + 15); //right padding }); - testWidgets('RadioListTile respects shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects shape', (WidgetTester tester) async { const ShapeBorder shapeBorder = RoundedRectangleBorder( borderRadius: BorderRadius.horizontal(right: Radius.circular(100)), ); @@ -666,7 +666,7 @@ void main() { expect(tester.widget<InkWell>(find.byType(InkWell)).customBorder, shapeBorder); }); - testWidgets('RadioListTile respects tileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects tileColor', (WidgetTester tester) async { final Color tileColor = Colors.red.shade500; await tester.pumpWidget( @@ -686,7 +686,7 @@ void main() { expect(find.byType(Material), paints..rect(color: tileColor)); }); - testWidgets('RadioListTile respects selectedTileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects selectedTileColor', (WidgetTester tester) async { final Color selectedTileColor = Colors.green.shade500; await tester.pumpWidget( @@ -707,7 +707,7 @@ void main() { expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); - testWidgets('RadioListTile selected item text Color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile selected item text Color', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/76906 const Color activeColor = Color(0xff00ff00); @@ -747,7 +747,7 @@ void main() { expect(textColor('title'), activeColor); }); - testWidgets('RadioListTile respects visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects visualDensity', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -772,7 +772,7 @@ void main() { expect(box.size, equals(const Size(800, 56))); }); - testWidgets('RadioListTile respects focusNode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects focusNode', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( wrap( @@ -795,8 +795,10 @@ void main() { expect(tileNode.hasPrimaryFocus, isTrue); }); - testWidgets('RadioListTile onFocusChange callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile onFocusChange callback', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'RadioListTile onFocusChange'); + addTearDown(node.dispose); + bool gotFocus = false; await tester.pumpWidget( MaterialApp( @@ -825,7 +827,7 @@ void main() { expect(node.hasFocus, isFalse); }); - testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio changes mouse cursor when hovered', (WidgetTester tester) async { // Test Radio() constructor await tester.pumpWidget( wrap(child: MouseRegion( @@ -876,7 +878,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('RadioListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async { const Color activeEnabledFillColor = Color(0xFF000001); const Color activeDisabledFillColor = Color(0xFF000002); const Color inactiveEnabledFillColor = Color(0xFF000003); @@ -963,7 +965,7 @@ void main() { ); }); - testWidgets('RadioListTile respects fillColor in hovered state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects fillColor in hovered state', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredFillColor = Color(0xFF000001); @@ -1012,7 +1014,7 @@ void main() { ); }); - testWidgets('RadioListTile respects hoverColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - RadioListTile respects hoverColor', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; final Color? hoverColor = Colors.orange[500]; @@ -1078,7 +1080,7 @@ void main() { ); }); - testWidgets('RadioListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - RadioListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color fillColor = Color(0xFF000000); @@ -1203,7 +1205,7 @@ void main() { ); }); - testWidgets('RadioListTile respects splashRadius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects splashRadius', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { @@ -1235,7 +1237,7 @@ void main() { ); }); - testWidgets('Radio respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio respects materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( wrap(child: RadioListTile<bool>( groupValue: true, @@ -1259,7 +1261,7 @@ void main() { expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0)); }); - testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async { Widget buildApp(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), @@ -1301,7 +1303,7 @@ void main() { feedback.dispose(); }); - testWidgets('RadioListTile respects enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioListTile respects enableFeedback', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(bool enableFeedback) async { return tester.pumpWidget( @@ -1339,7 +1341,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('RadioListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - RadioListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color fillColor = Color(0xFF000000); @@ -1443,7 +1445,7 @@ void main() { ); }); - testWidgets('RadioListTile respects hoverColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - RadioListTile respects hoverColor', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; final Color? hoverColor = Colors.orange[500]; diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index 9f4610c12f9b3..b1a1335664669 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -16,14 +16,13 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/gestures/constants.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { final ThemeData theme = ThemeData(); - testWidgets('Radio control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio control test', (WidgetTester tester) async { final Key key = UniqueKey(); final List<int?> log = <int?>[]; @@ -84,7 +83,7 @@ void main() { expect(log, isEmpty); }); - testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio can be toggled when toggleable is set', (WidgetTester tester) async { final Key key = UniqueKey(); final List<int?> log = <int?>[]; @@ -148,7 +147,7 @@ void main() { expect(log, equals(<int>[1])); }); - testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( Theme( @@ -194,7 +193,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); }); - testWidgets('Radio selected semantics - platform adaptive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio selected semantics - platform adaptive', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Theme( @@ -229,7 +228,7 @@ void main() { semantics.dispose(); }, variant: TargetPlatformVariant.all()); - testWidgets('Radio semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Theme( @@ -360,7 +359,7 @@ void main() { semantics.dispose(); }); - testWidgets('has semantic events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantic events', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); dynamic semanticEvent; @@ -398,7 +397,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('Material2 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const Key radioKey = Key('radio'); @@ -432,7 +431,7 @@ void main() { ); }); - testWidgets('Material3 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const Key radioKey = Key('radio'); @@ -466,7 +465,7 @@ void main() { ); }); - testWidgets('Radio with splash radius set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio with splash radius set', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { @@ -503,7 +502,7 @@ void main() { ); }); - testWidgets('Material2 - Radio is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; @@ -584,9 +583,10 @@ void main() { ..circle(color: const Color(0x61000000)) ..circle(color: const Color(0x61000000)), ); + focusNode.dispose(); }); - testWidgets('Material3 - Radio is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; @@ -662,9 +662,10 @@ void main() { ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)), ); + focusNode.dispose(); }); - testWidgets('Material2 - Radio can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const Key radioKey = Key('radio'); @@ -747,7 +748,7 @@ void main() { ); }); - testWidgets('Material3 - Radio can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const Key radioKey = Key('radio'); @@ -831,7 +832,7 @@ void main() { ); }); - testWidgets('Radio can be controlled by keyboard shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio can be controlled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 1; const Key radioKey0 = Key('radio0'); @@ -908,9 +909,11 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(groupValue, equals(2)); + + focusNode2.dispose(); }); - testWidgets('Radio responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -949,7 +952,7 @@ void main() { expect(box.size, equals(const Size(60, 36))); }); - testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio changes mouse cursor when hovered', (WidgetTester tester) async { const Key key = ValueKey<int>(1); // Test Radio() constructor await tester.pumpWidget( @@ -1032,7 +1035,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('Radio button fill color resolves in enabled/disabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio button fill color resolves in enabled/disabled states', (WidgetTester tester) async { const Color activeEnabledFillColor = Color(0xFF000001); const Color activeDisabledFillColor = Color(0xFF000002); const Color inactiveEnabledFillColor = Color(0xFF000003); @@ -1143,7 +1146,7 @@ void main() { ); }); - testWidgets('Material2 - Radio fill color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio fill color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredFillColor = Color(0xFF000001); @@ -1224,9 +1227,11 @@ void main() { ..circle(color: theme.hoverColor) ..circle(color: hoveredFillColor), ); + + focusNode.dispose(); }); - testWidgets('Material3 - Radio fill color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio fill color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredFillColor = Color(0xFF000001); @@ -1302,9 +1307,11 @@ void main() { ..circle(color: theme.colorScheme.primary.withOpacity(0.08)) ..circle(color: hoveredFillColor), ); + + focusNode.dispose(); }); - testWidgets('Radio overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1447,9 +1454,11 @@ void main() { ), reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor', ); + + focusNode.dispose(); }); - testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { final Key key = UniqueKey(); Widget buildRadio(bool show) { @@ -1475,7 +1484,7 @@ void main() { await gesture.up(); }); - testWidgets('disabled radio shows tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled radio shows tooltip', (WidgetTester tester) async { const String longPressTooltip = 'long press tooltip'; const String tapTooltip = 'tap tooltip'; await tester.pumpWidget( @@ -1531,7 +1540,7 @@ void main() { expect(find.text(tapTooltip), findsOneWidget); }); - testWidgets('Material2 - Radio button default colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio button default colors', (WidgetTester tester) async { Widget buildRadio({bool enabled = true, bool selected = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1577,7 +1586,7 @@ void main() { ); }); - testWidgets('Material3 - Radio button default colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio button default colors', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); Widget buildRadio({bool enabled = true, bool selected = true}) { return MaterialApp( @@ -1625,7 +1634,7 @@ void main() { ); }); - testWidgets('Material2 - Radio button default overlay colors in hover/focus/press states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio button default overlay colors in hover/focus/press states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1710,9 +1719,11 @@ void main() { Material.of(tester.element(find.byType(Radio<bool>))), paints..circle(color: theme.hoverColor)..circle(color: colors.secondary) ); + + focusNode.dispose(); }); - testWidgets('Material3 - Radio button default overlay colors in hover/focus/press states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio button default overlay colors in hover/focus/press states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1796,9 +1807,11 @@ void main() { Material.of(tester.element(find.byType(Radio<bool>))), paints..circle(color: colors.primary.withOpacity(0.08))..circle(color: colors.primary.withOpacity(1)) ); + + focusNode.dispose(); }); - testWidgets('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async { Widget buildApp(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), @@ -1829,7 +1842,7 @@ void main() { } }); - testWidgets('Material2 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(useMaterial3: false); @@ -1892,9 +1905,10 @@ void main() { ..circle(color: theme.focusColor) ..circle(color: theme.colorScheme.secondary) ); + focusNode.dispose(); }); - testWidgets('Material3 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(useMaterial3: true); @@ -1957,5 +1971,6 @@ void main() { ..circle(color: theme.colorScheme.primary.withOpacity(0.12)) ..circle(color: theme.colorScheme.primary) ); + focusNode.dispose(); }); } diff --git a/packages/flutter/test/material/radio_theme_test.dart b/packages/flutter/test/material/radio_theme_test.dart index b263547fb1cf8..5c8c186121629 100644 --- a/packages/flutter/test/material/radio_theme_test.dart +++ b/packages/flutter/test/material/radio_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('RadioThemeData copyWith, ==, hashCode basics', () { @@ -39,7 +38,7 @@ void main() { expect(theme.data.visualDensity, null); }); - testWidgets('Default RadioThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default RadioThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const RadioThemeData().debugFillProperties(builder); @@ -51,7 +50,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('RadioThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RadioThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const RadioThemeData( mouseCursor: MaterialStatePropertyAll<MouseCursor>(SystemMouseCursors.click), @@ -80,7 +79,7 @@ void main() { ); }); - testWidgets('Radio is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio is themeable', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const MouseCursor mouseCursor = SystemMouseCursors.text; @@ -153,7 +152,7 @@ void main() { expect(_getRadioMaterial(tester), paints..circle(color: focusOverlayColor, radius: splashRadius)); }); - testWidgets('Radio properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const MouseCursor themeMouseCursor = SystemMouseCursors.click; @@ -247,7 +246,7 @@ void main() { expect(_getRadioMaterial(tester), paints..circle(color: focusColor, radius: splashRadius)); }); - testWidgets('Radio activeColor property is taken over the theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio activeColor property is taken over the theme', (WidgetTester tester) async { const Color themeDefaultFillColor = Color(0xfffffff0); const Color themeSelectedFillColor = Color(0xfffffff1); @@ -288,7 +287,7 @@ void main() { expect(_getRadioMaterial(tester), paints..circle(color: selectedFillColor)); }); - testWidgets('Radio theme overlay color resolves in active/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Radio theme overlay color resolves in active/pressed states', (WidgetTester tester) async { const Color activePressedOverlayColor = Color(0xFF000001); const Color inactivePressedOverlayColor = Color(0xFF000002); @@ -350,7 +349,7 @@ void main() { ); }); - testWidgets('Local RadioTheme can override global RadioTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Local RadioTheme can override global RadioTheme', (WidgetTester tester) async { const Color globalThemeFillColor = Color(0xfffffff1); const Color localThemeFillColor = Color(0xffff0000); diff --git a/packages/flutter/test/material/range_slider_test.dart b/packages/flutter/test/material/range_slider_test.dart index 12c5eabdbb667..bd1c528065239 100644 --- a/packages/flutter/test/material/range_slider_test.dart +++ b/packages/flutter/test/material/range_slider_test.dart @@ -8,12 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/physics/utils.dart' show nearEqual; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/105833 - testWidgets('Drag gesture uses provided gesture settings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag gesture uses provided gesture settings', (WidgetTester tester) async { RangeValues values = const RangeValues(0.1, 0.5); bool dragStarted = false; final Key sliderKey = UniqueKey(); @@ -119,7 +118,7 @@ void main() { expect(dragStarted, false); }); - testWidgets('Range Slider can move when tapped (continuous LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can move when tapped (continuous LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -173,7 +172,7 @@ void main() { expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); }); - testWidgets('Range Slider can move when tapped (continuous RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can move when tapped (continuous RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -227,7 +226,7 @@ void main() { expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); }); - testWidgets('Range Slider can move when tapped (discrete LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can move when tapped (discrete LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -285,7 +284,7 @@ void main() { expect(values.end.round(), equals(90)); }); - testWidgets('Range Slider can move when tapped (discrete RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can move when tapped (discrete RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -343,7 +342,7 @@ void main() { expect(values.end.round(), equals(90)); }); - testWidgets('Range Slider thumbs can be dragged to the min and max (continuous LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged to the min and max (continuous LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -387,7 +386,7 @@ void main() { expect(values.end, equals(1)); }); - testWidgets('Range Slider thumbs can be dragged to the min and max (continuous RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged to the min and max (continuous RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -431,7 +430,7 @@ void main() { expect(values.start, equals(0)); }); - testWidgets('Range Slider thumbs can be dragged to the min and max (discrete LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged to the min and max (discrete LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -477,7 +476,7 @@ void main() { expect(values.end, equals(100)); }); - testWidgets('Range Slider thumbs can be dragged to the min and max (discrete RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged to the min and max (discrete RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -523,7 +522,7 @@ void main() { expect(values.start, equals(0)); }); - testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -573,7 +572,7 @@ void main() { expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05)); }); - testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -623,7 +622,7 @@ void main() { expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05)); }); - testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -675,7 +674,7 @@ void main() { expect(values.start, moreOrLessEquals(20, epsilon: 0.01)); }); - testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -727,7 +726,7 @@ void main() { expect(values.start, moreOrLessEquals(20, epsilon: 0.01)); }); - testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -777,7 +776,7 @@ void main() { expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05)); }); - testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); await tester.pumpWidget( @@ -827,7 +826,7 @@ void main() { expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05)); }); - testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -879,7 +878,7 @@ void main() { expect(values.end, moreOrLessEquals(80, epsilon: 0.01)); }); - testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); await tester.pumpWidget( @@ -931,7 +930,7 @@ void main() { expect(values.end, moreOrLessEquals(80, epsilon: 0.01)); }); - testWidgets('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by tap', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); RangeValues? startValues; RangeValues? endValues; @@ -985,7 +984,7 @@ void main() { expect(endValues!.end, moreOrLessEquals(70, epsilon: 1)); }); - testWidgets('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by drag', (WidgetTester tester) async { RangeValues values = const RangeValues(30, 70); late RangeValues startValues; late RangeValues endValues; @@ -1100,7 +1099,7 @@ void main() { ); } - testWidgets('Range Slider uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async { final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -1129,7 +1128,7 @@ void main() { expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes when setting the active color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes when setting the active color', (WidgetTester tester) async { const Color activeColor = Color(0xcafefeed); final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -1157,7 +1156,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async { const Color inactiveColor = Color(0xdeadbeef); final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -1184,7 +1183,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async { const Color activeColor = Color(0xcafefeed); const Color inactiveColor = Color(0xdeadbeef); final ThemeData theme = buildTheme(); @@ -1217,7 +1216,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async { final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -1247,7 +1246,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async { const Color activeColor = Color(0xcafefeed); const Color inactiveColor = Color(0xdeadbeef); final ThemeData theme = buildTheme(); @@ -1288,7 +1287,7 @@ void main() { expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes for a default disabled slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes for a default disabled slider', (WidgetTester tester) async { final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -1308,7 +1307,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes for a disabled slider with active and inactive colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes for a disabled slider with active and inactive colors', (WidgetTester tester) async { const Color activeColor = Color(0xcafefeed); const Color inactiveColor = Color(0xdeadbeef); final ThemeData theme = buildTheme(); @@ -1335,7 +1334,7 @@ void main() { expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); }); - testWidgets('Range Slider uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async { final ThemeData theme = buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; RangeValues values = const RangeValues(0.5, 0.75); @@ -1393,7 +1392,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Range Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async { RangeValues values = const RangeValues(0.5, 0.75); const Color fillColor = Color(0xf55f5f5f); @@ -1497,7 +1496,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); final ThemeData theme = ThemeData( @@ -1564,7 +1563,7 @@ void main() { ); }); - testWidgets('Range Slider top value indicator gets stroked when overlapping', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider top value indicator gets stroked when overlapping', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); final ThemeData theme = ThemeData( @@ -1638,7 +1637,7 @@ void main() { await gesture.up(); }); - testWidgets('Range Slider top value indicator gets stroked when overlapping with large text scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider top value indicator gets stroked when overlapping with large text scale', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); final ThemeData theme = ThemeData( @@ -1715,7 +1714,7 @@ void main() { await gesture.up(); }); - testWidgets('Range Slider thumb gets stroked when overlapping', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider thumb gets stroked when overlapping', (WidgetTester tester) async { RangeValues values = const RangeValues(0.3, 0.7); final ThemeData theme = ThemeData( @@ -1796,7 +1795,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/101868 - testWidgets('RangeSlider.label info should not write to semantic node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider.label info should not write to semantic node', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Theme( @@ -1854,7 +1853,7 @@ void main() { ); }); - testWidgets('Range Slider Semantics - ltr', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider Semantics - ltr', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Theme( @@ -1938,7 +1937,7 @@ void main() { ]); }); - testWidgets('Range Slider Semantics - rtl', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider Semantics - rtl', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Theme( @@ -2020,7 +2019,7 @@ void main() { ]); }); - testWidgets('Range Slider implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); RangeSlider( @@ -2051,7 +2050,7 @@ void main() { ]); }); - testWidgets('Range Slider can be painted in a narrower constraint when track shape is RoundedRectRange', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can be painted in a narrower constraint when track shape is RoundedRectRange', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( @@ -2090,7 +2089,7 @@ void main() { ); }); - testWidgets('Range Slider can be painted in a narrower constraint when track shape is Rectangular', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Range Slider can be painted in a narrower constraint when track shape is Rectangular', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -2135,7 +2134,7 @@ void main() { ); }); - testWidgets('Update the divisions and values at the same time for RangeSlider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update the divisions and values at the same time for RangeSlider', (WidgetTester tester) async { // Regress test for https://github.com/flutter/flutter/issues/65943 Widget buildFrame(double maxValue) { return MaterialApp( @@ -2179,7 +2178,7 @@ void main() { expect(nearEqual(activeTrackRect.right, (800.0 - 24.0 - 24.0) * (8 / 15) + 24.0, 0.01), true); }); - testWidgets('RangeSlider changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider changes mouse cursor when hovered', (WidgetTester tester) async { const RangeValues values = RangeValues(50, 70); // Test default cursor. @@ -2234,7 +2233,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('RangeSlider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async { RangeValues values = const RangeValues(50, 70); const MouseCursor disabledCursor = SystemMouseCursors.basic; const MouseCursor hoveredCursor = SystemMouseCursors.grab; @@ -2308,7 +2307,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), draggedCursor); }); - testWidgets('RangeSlider can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; RangeValues values = const RangeValues(50, 70); final ThemeData theme = ThemeData(); @@ -2371,7 +2370,7 @@ void main() { ); }); - testWidgets('RangeSlider is draggable and has correct dragged color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider is draggable and has correct dragged color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; RangeValues values = const RangeValues(50, 70); final ThemeData theme = ThemeData(); @@ -2427,7 +2426,7 @@ void main() { ); }); - testWidgets('RangeSlider overlayColor supports hovered and dragged states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider overlayColor supports hovered and dragged states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; RangeValues values = const RangeValues(50, 70); const Color hoverColor = Color(0xffff0000); @@ -2542,7 +2541,7 @@ void main() { ); }); - testWidgets('RangeSlider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RangeSlider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/128433 int startFired = 0; @@ -2582,4 +2581,34 @@ void main() { expect(startFired, equals(1)); expect(endFired, equals(1)); }); + + testWidgetsWithLeakTracking('RangeSlider in a ListView does not throw an exception', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/126648 + + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: ListView( + children: <Widget>[ + const SizedBox( + height: 600, + child: Placeholder(), + ), + RangeSlider( + values: const RangeValues(40, 80), + max: 100, + onChanged: (RangeValues newValue) { }, + ), + ], + ), + ), + ), + ), + ); + + // No exception should be thrown. + expect(tester.takeException(), null); + }); } diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart index da69556af2132..3736ab1a31c3f 100644 --- a/packages/flutter/test/material/raw_material_button_test.dart +++ b/packages/flutter/test/material/raw_material_button_test.dart @@ -8,12 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('RawMaterialButton responds when tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton responds when tapped', (WidgetTester tester) async { bool pressed = false; const Color splashColor = Color(0xff00ff00); await tester.pumpWidget( @@ -43,7 +42,7 @@ void main() { expect(pressed, isTrue); }); - testWidgets('RawMaterialButton responds to shortcut when activated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton responds to shortcut when activated', (WidgetTester tester) async { bool pressed = false; final FocusNode focusNode = FocusNode(debugLabel: 'Test Button'); const Color splashColor = Color(0xff00ff00); @@ -108,9 +107,10 @@ void main() { await tester.pumpAndSettle(); expect(pressed, isTrue); + focusNode.dispose(); }); - testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async { int pressed = 0; await tester.pumpWidget( @@ -132,7 +132,7 @@ void main() { expect(pressed, 1); }); - testWidgets('materialTapTargetSize.padded expands semantics area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('materialTapTargetSize.padded expands semantics area', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -175,7 +175,7 @@ void main() { semantics.dispose(); }); - testWidgets('Ink splash from center tap originates in correct location', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ink splash from center tap originates in correct location', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xAA0000FF); const Color fillColor = Color(0xFFEF5350); @@ -210,7 +210,7 @@ void main() { await gesture.up(); }); - testWidgets('Ink splash from tap above material originates in correct location', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ink splash from tap above material originates in correct location', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xAA0000FF); const Color fillColor = Color(0xFFEF5350); @@ -244,7 +244,7 @@ void main() { await gesture.up(); }); - testWidgets('off-center child is hit testable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('off-center child is hit testable', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Column( @@ -274,7 +274,7 @@ void main() { expect(find.text('Material').hitTestable(), findsOneWidget); }); - testWidgets('smaller child is hit testable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('smaller child is hit testable', (WidgetTester tester) async { const Key key = Key('test'); await tester.pumpWidget( MaterialApp( @@ -299,7 +299,7 @@ void main() { expect(find.byKey(key).hitTestable(), findsOneWidget); }); - testWidgets('RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async { const Key key = Key('test'); await tester.pumpWidget( MaterialApp( @@ -319,7 +319,7 @@ void main() { expect(tester.getSize(find.byKey(key)), const Size(800.0, 48.0)); }); - testWidgets('RawMaterialButton handles focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton handles focus', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Button Focus'); const Key key = Key('test'); const Color focusColor = Color(0xff00ff00); @@ -345,9 +345,10 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 1)); expect(box, paints..rect(color: focusColor)); + focusNode.dispose(); }); - testWidgets('RawMaterialButton loses focus when disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton loses focus when disabled.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'RawMaterialButton'); await tester.pumpWidget( MaterialApp( @@ -379,9 +380,10 @@ void main() { await tester.pump(); expect(focusNode.hasPrimaryFocus, isFalse); + focusNode.dispose(); }); - testWidgets("Disabled RawMaterialButton can't be traversed to.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Disabled RawMaterialButton can't be traversed to.", (WidgetTester tester) async { final FocusNode focusNode1 = FocusNode(debugLabel: '$RawMaterialButton 1'); final FocusNode focusNode2 = FocusNode(debugLabel: '$RawMaterialButton 2'); @@ -414,14 +416,17 @@ void main() { expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); - expect(focusNode1.nextFocus(), isTrue); + expect(focusNode1.nextFocus(), isFalse); await tester.pump(); expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); + + focusNode1.dispose(); + focusNode2.dispose(); }); - testWidgets('RawMaterialButton handles hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton handles hover', (WidgetTester tester) async { const Key key = Key('test'); const Color hoverColor = Color(0xff00ff00); @@ -450,7 +455,7 @@ void main() { expect(box, paints..rect(color: hoverColor)); }); - testWidgets('RawMaterialButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder rawMaterialButton; @@ -494,7 +499,7 @@ void main() { expect(tester.widget<RawMaterialButton>(rawMaterialButton).enabled, false); }); - testWidgets('RawMaterialButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -525,7 +530,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets('RawMaterialButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -586,7 +591,7 @@ void main() { expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); }); - testWidgets('RawMaterialButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawMaterialButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/material/refresh_indicator_test.dart b/packages/flutter/test/material/refresh_indicator_test.dart index 9a2248204d94f..e3c1bd8933337 100644 --- a/packages/flutter/test/material/refresh_indicator_test.dart +++ b/packages/flutter/test/material/refresh_indicator_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; bool refreshCalled = false; @@ -22,7 +23,7 @@ Future<void> holdRefresh() { } void main() { - testWidgets('RefreshIndicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator', (WidgetTester tester) async { refreshCalled = false; final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( @@ -56,7 +57,7 @@ void main() { handle.dispose(); }); - testWidgets('Refresh Indicator - nested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Refresh Indicator - nested', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -97,7 +98,7 @@ void main() { expect(refreshCalled, true); }); - testWidgets('RefreshIndicator - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - reverse', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -125,7 +126,7 @@ void main() { expect(refreshCalled, true); }); - testWidgets('RefreshIndicator - top - position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - top - position', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -151,7 +152,7 @@ void main() { expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0)); }); - testWidgets('RefreshIndicator - reverse - position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - reverse - position', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -178,7 +179,7 @@ void main() { expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0)); }); - testWidgets('RefreshIndicator - no movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - no movement', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -206,7 +207,7 @@ void main() { expect(refreshCalled, false); }); - testWidgets('RefreshIndicator - not enough', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - not enough', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -233,7 +234,7 @@ void main() { expect(refreshCalled, false); }); - testWidgets('RefreshIndicator - just enough', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - just enough', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -260,7 +261,7 @@ void main() { expect(refreshCalled, true); }); - testWidgets('RefreshIndicator - show - slow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - show - slow', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -303,7 +304,7 @@ void main() { expect(refreshCalled, false); }); - testWidgets('RefreshIndicator - show - fast', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - show - fast', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -347,7 +348,7 @@ void main() { expect(completed, true); }); - testWidgets('RefreshIndicator - show - fast - twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - show - fast - twice', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -385,7 +386,7 @@ void main() { expect(completed2, true); }); - testWidgets('Refresh starts while scroll view moves back to 0.0 after overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Refresh starts while scroll view moves back to 0.0 after overscroll', (WidgetTester tester) async { refreshCalled = false; double lastScrollOffset; final ScrollController controller = ScrollController(); @@ -422,9 +423,11 @@ void main() { expect(controller.offset, greaterThan(lastScrollOffset)); expect(controller.offset, lessThan(0.0)); expect(refreshCalled, isTrue); + + controller.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('RefreshIndicator does not force child to relayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator does not force child to relayout', (WidgetTester tester) async { int layoutCount = 0; Widget layoutCallback(BuildContext context, BoxConstraints constraints) { @@ -459,7 +462,7 @@ void main() { expect(layoutCount, 1); }); - testWidgets('RefreshIndicator responds to strokeWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator responds to strokeWidth', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: RefreshIndicator( @@ -507,7 +510,7 @@ void main() { ); }); - testWidgets('RefreshIndicator responds to edgeOffset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator responds to edgeOffset', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: RefreshIndicator( @@ -555,7 +558,7 @@ void main() { ); }); - testWidgets('RefreshIndicator appears at edgeOffset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator appears at edgeOffset', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: RefreshIndicator( edgeOffset: kToolbarHeight, @@ -584,7 +587,7 @@ void main() { ); }); - testWidgets('Top RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Top RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -617,9 +620,11 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0)); + + scrollController.dispose(); }); - testWidgets('Reverse RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reverse RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -653,10 +658,12 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0)); + + scrollController.dispose(); }); // Regression test for https://github.com/flutter/flutter/issues/71936 - testWidgets('RefreshIndicator(anywhere mode) should not be shown when overscroll occurs due to inertia', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator(anywhere mode) should not be shown when overscroll occurs due to inertia', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -690,9 +697,11 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation expect(find.byType(RefreshProgressIndicator), findsNothing); + + scrollController.dispose(); }); - testWidgets('Top RefreshIndicator(onEdge mode) should not be shown when dragging from non-zero scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Top RefreshIndicator(onEdge mode) should not be shown when dragging from non-zero scroll position', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -724,9 +733,11 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation expect(find.byType(RefreshProgressIndicator), findsNothing); + + scrollController.dispose(); }); - testWidgets('Reverse RefreshIndicator(onEdge mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reverse RefreshIndicator(onEdge mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -759,9 +770,11 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation expect(find.byType(RefreshProgressIndicator), findsNothing); + + scrollController.dispose(); }); - testWidgets('ScrollController.jumpTo should not trigger the refresh indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollController.jumpTo should not trigger the refresh indicator', (WidgetTester tester) async { refreshCalled = false; final ScrollController scrollController = ScrollController(initialScrollOffset: 500.0); await tester.pumpWidget( @@ -791,9 +804,11 @@ void main() { await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation expect(refreshCalled, false); + + scrollController.dispose(); }); - testWidgets('RefreshIndicator.adaptive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator.adaptive', (WidgetTester tester) async { Widget buildFrame(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), @@ -835,7 +850,7 @@ void main() { } }); - testWidgets('RefreshIndicator color defaults to ColorScheme.primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator color defaults to ColorScheme.primary', (WidgetTester tester) async { const Color primaryColor = Color(0xff4caf50); final ThemeData theme = ThemeData.from(colorScheme: const ColorScheme.light().copyWith(primary: primaryColor)); await tester.pumpWidget( @@ -871,7 +886,7 @@ void main() { expect(tester.widget<RefreshProgressIndicator>(find.byType(RefreshProgressIndicator)).valueColor!.value, primaryColor); }); - testWidgets('RefreshIndicator.color can be updated at runtime', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator.color can be updated at runtime', (WidgetTester tester) async { refreshCalled = false; Color refreshIndicatorColor = Colors.green; const Color red = Colors.red; @@ -918,7 +933,7 @@ void main() { expect(tester.widget<RefreshProgressIndicator>(find.byType(RefreshProgressIndicator)).valueColor!.value, red.withOpacity(1.0)); }); - testWidgets('RefreshIndicator - reverse - BouncingScrollPhysics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator - reverse - BouncingScrollPhysics', (WidgetTester tester) async { refreshCalled = false; await tester.pumpWidget( MaterialApp( @@ -952,7 +967,7 @@ void main() { expect(refreshCalled, true); }); - testWidgets('RefreshIndicator disallows indicator - glow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator disallows indicator - glow', (WidgetTester tester) async { refreshCalled = false; bool glowAccepted = true; ScrollNotification? lastNotification; @@ -1004,7 +1019,7 @@ void main() { expect(glowAccepted, false); }); - testWidgets('RefreshIndicator disallows indicator - stretch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RefreshIndicator disallows indicator - stretch', (WidgetTester tester) async { refreshCalled = false; bool stretchAccepted = true; ScrollNotification? lastNotification; @@ -1055,4 +1070,67 @@ void main() { expect(refreshCalled, true); expect(stretchAccepted, false); }); + + testWidgetsWithLeakTracking('RefreshIndicator manipulates value color opacity correctly', (WidgetTester tester) async { + final List<Color> colors = <Color>[ + Colors.black, + Colors.black54, + Colors.white, + Colors.white54, + Colors.transparent, + ]; + const List<double> positions = <double>[50.0, 100.0, 150.0]; + + Future<void> testColor(Color color) async { + final AnimationController positionController = AnimationController(vsync: const TestVSync()); + // Correspond to [_setupColorTween]. + final Animation<Color?> valueColorAnimation = positionController.drive( + ColorTween( + begin: color.withAlpha(0), + end: color.withAlpha(color.alpha), + ).chain( + CurveTween( + // Correspond to [_kDragSizeFactorLimit]. + curve: const Interval(0.0, 1.0 / 1.5), + ), + ), + ); + await tester.pumpWidget( + MaterialApp( + home: RefreshIndicator( + onRefresh: refresh, + color: color, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const <Widget>[Text('X')], + ), + ), + ), + ); + + RefreshProgressIndicator getIndicator() { + return tester.widget<RefreshProgressIndicator>( + find.byType(RefreshProgressIndicator), + ); + } + + // Correspond to [_kDragContainerExtentPercentage]. + final double maxPosition = tester.view.physicalSize.height / tester.view.devicePixelRatio * 0.25; + for (final double position in positions) { + await tester.fling(find.text('X'), Offset(0.0, position), 1.0); + await tester.pump(); + positionController.value = position / maxPosition; + expect( + getIndicator().valueColor!.value!.alpha, + valueColorAnimation.value!.alpha, + ); + // Wait until the fling finishes before starting the next fling. + await tester.pumpAndSettle(); + } + } + + for (final Color color in colors) { + await testColor(color); + } + }); } diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart index 6bcf56fe5f378..e13dbf0437319 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -11,6 +11,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('$ReorderableListView', () { @@ -71,7 +72,7 @@ void main() { }); group('in vertical mode', () { - testWidgets('reorder is not triggered when children length is less or equals to 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reorder is not triggered when children length is less or equals to 1', (WidgetTester tester) async { bool onReorderWasCalled = false; final List<String> currentListItems = listItems.take(1).toList(); final ReorderableListView reorderableListView = ReorderableListView( @@ -97,7 +98,7 @@ void main() { expect(currentListItems, orderedEquals(<String>['Item 1'])); }); - testWidgets('reorders its contents only when a drag finishes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reorders its contents only when a drag finishes', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(listItems, orderedEquals(originalListItems)); final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('Item 1'))); @@ -110,7 +111,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 1', 'Item 4'])); }); - testWidgets('allows reordering from the very top to the very bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering from the very top to the very bottom', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -122,7 +123,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1'])); }); - testWidgets('allows reordering from the very bottom to the very top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering from the very bottom to the very top', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -134,7 +135,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3'])); }); - testWidgets('allows reordering inside the middle of the widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering inside the middle of the widget', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -146,7 +147,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 2', 'Item 4'])); }); - testWidgets('properly reorders with a header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly reorders with a header', (WidgetTester tester) async { await tester.pumpWidget(build(header: const Text('Header Text'))); expect(find.text('Header Text'), findsOneWidget); expect(listItems, orderedEquals(originalListItems)); @@ -160,7 +161,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1'])); }); - testWidgets('properly reorders with a footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly reorders with a footer', (WidgetTester tester) async { await tester.pumpWidget(build(footer: const Text('Footer Text'))); expect(find.text('Footer Text'), findsOneWidget); expect(listItems, orderedEquals(originalListItems)); @@ -174,7 +175,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1'])); }); - testWidgets('properly determines the vertical drop area extents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly determines the vertical drop area extents', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView( onReorder: (int oldIndex, int newIndex) { }, children: const <Widget>[ @@ -241,7 +242,7 @@ void main() { expect(getListHeight(), kDraggingListHeight); }); - testWidgets('Vertical drag in progress golden image', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical drag in progress golden image', (WidgetTester tester) async { debugDisableShadows = false; final Widget reorderableListView = ReorderableListView( children: <Widget>[ @@ -266,6 +267,10 @@ void main() { ], onReorder: (int oldIndex, int newIndex) { }, ); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget(MaterialApp( home: Container( color: Colors.white, @@ -273,7 +278,7 @@ void main() { // Wrap in an overlay so that the golden image includes the dragged item. child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + entry = OverlayEntry(builder: (BuildContext context) { // Wrap the list in padding to test that the positioning // is correct when the origin of the overlay is different // from the list. @@ -305,7 +310,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preserves children states when the list parent changes the order', (WidgetTester tester) async { _StatefulState findState(Key key) { return find.byElementPredicate((Element element) => element.findAncestorWidgetOfExactType<_Stateful>()?.key == key) .evaluate() @@ -345,7 +350,7 @@ void main() { expect(findState(const Key('A')).checked, true); }); - testWidgets('Preserves children states when rebuilt', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preserves children states when rebuilt', (WidgetTester tester) async { const Key firstBox = Key('key'); Widget build() { return MaterialApp( @@ -373,8 +378,9 @@ void main() { expect(e0, equals(e1)); }); - testWidgets('Uses the PrimaryScrollController when available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uses the PrimaryScrollController when available', (WidgetTester tester) async { final ScrollController primary = ScrollController(); + addTearDown(primary.dispose); final Widget reorderableList = ReorderableListView( children: const <Widget>[ SizedBox(width: 100.0, height: 100.0, key: Key('C'), child: Text('C')), @@ -405,6 +411,8 @@ void main() { // Now try changing the primary scroll controller and checking that the scroll view gets updated. final ScrollController primary2 = ScrollController(); + addTearDown(primary2.dispose); + await tester.pumpWidget(buildWithScrollController(primary2)); scrollView = tester.widget( find.byType(Scrollable), @@ -412,11 +420,13 @@ void main() { expect(scrollView.controller, primary2); }); - testWidgets('Test custom ScrollController behavior when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test custom ScrollController behavior when set', (WidgetTester tester) async { const Key firstBox = Key('C'); const Key secondBox = Key('B'); const Key thirdBox = Key('A'); final ScrollController customController = ScrollController(); + addTearDown(customController.dispose); + await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -474,9 +484,11 @@ void main() { expect(customController.offset, 120.0); }); - testWidgets('ReorderableList auto scrolling is fast enough', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableList auto scrolling is fast enough', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/121603. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -513,7 +525,7 @@ void main() { expect(controller.offset, greaterThan(kMinimumAllowedAutoScrollDistancePer5ms * 4)); }); - testWidgets('Still builds when no PrimaryScrollController is available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Still builds when no PrimaryScrollController is available', (WidgetTester tester) async { final Widget reorderableList = ReorderableListView( children: const <Widget>[ SizedBox(width: 100.0, height: 100.0, key: Key('C'), child: Text('C')), @@ -522,9 +534,13 @@ void main() { ], onReorder: (int oldIndex, int newIndex) { }, ); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + final Widget overlay = Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) => reorderableList), + entry = OverlayEntry(builder: (BuildContext context) => reorderableList), ], ); final Widget boilerplate = Localizations( @@ -562,7 +578,7 @@ void main() { const CustomSemanticsAction moveUp = CustomSemanticsAction(label: 'Move up'); const CustomSemanticsAction moveDown = CustomSemanticsAction(label: 'Move down'); - testWidgets('Provides the correct accessibility actions in LTR and RTL modes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Provides the correct accessibility actions in LTR and RTL modes', (WidgetTester tester) async { // The a11y actions for a vertical list are the same in LTR and RTL modes. final SemanticsHandle handle = tester.ensureSemantics(); for (final TextDirection direction in TextDirection.values) { @@ -597,7 +613,7 @@ void main() { handle.dispose(); }); - testWidgets('First item accessibility (a11y) actions work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('First item accessibility (a11y) actions work', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -618,7 +634,7 @@ void main() { handle.dispose(); }); - testWidgets('Middle item accessibility (a11y) actions work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Middle item accessibility (a11y) actions work', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -653,7 +669,7 @@ void main() { handle.dispose(); }); - testWidgets('Last item accessibility (a11y) actions work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Last item accessibility (a11y) actions work', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -674,7 +690,7 @@ void main() { handle.dispose(); }); - testWidgets("Doesn't hide accessibility when a child declares its own semantics", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Doesn't hide accessibility when a child declares its own semantics", (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final Widget reorderableListView = ReorderableListView( onReorder: (int oldIndex, int newIndex) { }, @@ -743,7 +759,7 @@ void main() { }); group('in horizontal mode', () { - testWidgets('reorder is not triggered when children length is less or equals to 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reorder is not triggered when children length is less or equals to 1', (WidgetTester tester) async { bool onReorderWasCalled = false; final List<String> currentListItems = listItems.take(1).toList(); final ReorderableListView reorderableListView = ReorderableListView( @@ -770,7 +786,7 @@ void main() { expect(currentListItems, orderedEquals(<String>['Item 1'])); }); - testWidgets('allows reordering from the very top to the very bottom', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering from the very top to the very bottom', (WidgetTester tester) async { await tester.pumpWidget(build(scrollDirection: Axis.horizontal)); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -782,7 +798,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1'])); }); - testWidgets('allows reordering from the very bottom to the very top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering from the very bottom to the very top', (WidgetTester tester) async { await tester.pumpWidget(build(scrollDirection: Axis.horizontal)); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -794,7 +810,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3'])); }); - testWidgets('allows reordering inside the middle of the widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows reordering inside the middle of the widget', (WidgetTester tester) async { await tester.pumpWidget(build(scrollDirection: Axis.horizontal)); expect(listItems, orderedEquals(originalListItems)); await longPressDrag( @@ -806,7 +822,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 2', 'Item 4'])); }); - testWidgets('properly reorders with a header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly reorders with a header', (WidgetTester tester) async { await tester.pumpWidget(build(header: const Text('Header Text'), scrollDirection: Axis.horizontal)); expect(find.text('Header Text'), findsOneWidget); expect(listItems, orderedEquals(originalListItems)); @@ -829,7 +845,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 4', 'Item 3', 'Item 1'])); }); - testWidgets('properly reorders with a footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly reorders with a footer', (WidgetTester tester) async { await tester.pumpWidget(build(footer: const Text('Footer Text'), scrollDirection: Axis.horizontal)); expect(find.text('Footer Text'), findsOneWidget); expect(listItems, orderedEquals(originalListItems)); @@ -852,7 +868,7 @@ void main() { expect(listItems, orderedEquals(<String>['Item 2', 'Item 4', 'Item 3', 'Item 1'])); }); - testWidgets('properly determines the horizontal drop area extents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properly determines the horizontal drop area extents', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView( scrollDirection: Axis.horizontal, onReorder: (int oldIndex, int newIndex) { }, @@ -920,7 +936,7 @@ void main() { expect(getListWidth(), kDraggingListWidth); }); - testWidgets('Horizontal drag in progress golden image', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal drag in progress golden image', (WidgetTester tester) async { debugDisableShadows = false; final Widget reorderableListView = ReorderableListView( scrollDirection: Axis.horizontal, @@ -946,6 +962,10 @@ void main() { ), ], ); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget(MaterialApp( home: Container( color: Colors.white, @@ -953,7 +973,7 @@ void main() { // Wrap in an overlay so that the golden image includes the dragged item. child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + entry = OverlayEntry(builder: (BuildContext context) { // Wrap the list in padding to test that the positioning // is correct when the origin of the overlay is different // from the list. @@ -985,7 +1005,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preserves children states when the list parent changes the order', (WidgetTester tester) async { _StatefulState findState(Key key) { return find.byElementPredicate((Element element) => element.findAncestorWidgetOfExactType<_Stateful>()?.key == key) .evaluate() @@ -1027,7 +1047,7 @@ void main() { expect(findState(const Key('A')).checked, true); }); - testWidgets('Preserves children states when rebuilt', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preserves children states when rebuilt', (WidgetTester tester) async { const Key firstBox = Key('key'); Widget build() { return MaterialApp( @@ -1070,7 +1090,7 @@ void main() { const CustomSemanticsAction moveLeft = CustomSemanticsAction(label: 'Move left'); const CustomSemanticsAction moveRight = CustomSemanticsAction(label: 'Move right'); - testWidgets('Provides the correct accessibility actions in LTR mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Provides the correct accessibility actions in LTR mode', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(build(scrollDirection: Axis.horizontal)); @@ -1103,7 +1123,7 @@ void main() { handle.dispose(); }); - testWidgets('Provides the correct accessibility actions in Right-To-Left directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Provides the correct accessibility actions in Right-To-Left directionality', (WidgetTester tester) async { // In RTL mode, the right is the start and the left is the end. // The array representation is unchanged (LTR), but the direction of the motion actions is reversed. final SemanticsHandle handle = tester.ensureSemantics(); @@ -1138,7 +1158,7 @@ void main() { handle.dispose(); }); - testWidgets('First item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('First item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -1159,7 +1179,7 @@ void main() { handle.dispose(); }); - testWidgets('First item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('First item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { // In RTL mode, the right is the start and the left is the end. // The array representation is unchanged (LTR), but the direction of the motion actions is reversed. final SemanticsHandle handle = tester.ensureSemantics(); @@ -1182,7 +1202,7 @@ void main() { handle.dispose(); }); - testWidgets('Middle item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Middle item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -1217,7 +1237,7 @@ void main() { handle.dispose(); }); - testWidgets('Middle item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Middle item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { // In RTL mode, the right is the start and the left is the end. // The array representation is unchanged (LTR), but the direction of the motion actions is reversed. final SemanticsHandle handle = tester.ensureSemantics(); @@ -1254,7 +1274,7 @@ void main() { handle.dispose(); }); - testWidgets('Last item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Last item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); expect(listItems, orderedEquals(originalListItems)); @@ -1275,7 +1295,7 @@ void main() { handle.dispose(); }); - testWidgets('Last item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Last item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async { // In RTL mode, the right is the start and the left is the end. // The array representation is unchanged (LTR), but the direction of the motion actions is reversed. final SemanticsHandle handle = tester.ensureSemantics(); @@ -1302,7 +1322,7 @@ void main() { }); - testWidgets('ReorderableListView.builder asserts on negative childCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView.builder asserts on negative childCount', (WidgetTester tester) async { expect(() => ReorderableListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); @@ -1312,7 +1332,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ReorderableListView.builder only creates the children it needs', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView.builder only creates the children it needs', (WidgetTester tester) async { final Set<int> itemsCreated = <int>{}; await tester.pumpWidget(MaterialApp( home: ReorderableListView.builder( @@ -1330,7 +1350,7 @@ void main() { }); group('Padding', () { - testWidgets('Padding with no header & footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding with no header & footer', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(10, 20, 30, 40); // Vertical @@ -1344,7 +1364,7 @@ void main() { expect(tester.getRect(find.byKey(const Key('Item 4'))), const Rect.fromLTRB(154, 20, 202, 560)); }); - testWidgets('Padding with header or footer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding with header or footer', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(10, 20, 30, 40); const Key headerKey = Key('Header'); const Key footerKey = Key('Footer'); @@ -1403,7 +1423,7 @@ void main() { }); }); - testWidgets('ReorderableListView can be reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView can be reversed', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView( reverse: true, onReorder: (int oldIndex, int newIndex) { }, @@ -1428,7 +1448,7 @@ void main() { expect(tester.getCenter(find.text('A')).dy, greaterThan(tester.getCenter(find.text('B')).dy)); }); - testWidgets('Animation test when placing an item in place', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animation test when placing an item in place', (WidgetTester tester) async { const Key testItemKey = Key('Test item'); final Widget reorderableListView = ReorderableListView( onReorder: (int oldIndex, int newIndex) { }, @@ -1480,21 +1500,21 @@ void main() { }); // TODO(djshuckerow): figure out how to write a test for scrolling the list. - testWidgets('ReorderableListView on desktop platforms should have drag handles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView on desktop platforms should have drag handles', (WidgetTester tester) async { await tester.pumpWidget(build()); // All four items should have drag handles and not delayed listeners. expect(find.byIcon(Icons.drag_handle), findsNWidgets(4)); expect(find.byType(ReorderableDelayedDragStartListener), findsNothing); }, variant: TargetPlatformVariant.desktop()); - testWidgets('ReorderableListView on mobile platforms should not have drag handles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView on mobile platforms should not have drag handles', (WidgetTester tester) async { await tester.pumpWidget(build()); // All four items should have delayed listeners and not drag handles. expect(find.byType(ReorderableDelayedDragStartListener), findsNWidgets(4)); expect(find.byIcon(Icons.drag_handle), findsNothing); }, variant: TargetPlatformVariant.mobile()); - testWidgets('Vertical list renders drag handle in correct position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical list renders drag handle in correct position', (WidgetTester tester) async { await tester.pumpWidget(build(platform: TargetPlatform.macOS)); final Finder listView = find.byType(ReorderableListView); final Finder item1 = find.byKey(const Key('Item 1')); @@ -1505,7 +1525,7 @@ void main() { expect(tester.getTopRight(dragHandle).dx, tester.getSize(listView).width - 8); }); - testWidgets('Horizontal list renders drag handle in correct position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal list renders drag handle in correct position', (WidgetTester tester) async { await tester.pumpWidget(build(scrollDirection: Axis.horizontal, platform: TargetPlatform.macOS)); final Finder listView = find.byType(ReorderableListView); final Finder item1 = find.byKey(const Key('Item 1')); @@ -1517,7 +1537,7 @@ void main() { }); }); - testWidgets('ReorderableListView, can deal with the dragged item getting unmounted and rebuilt during drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView, can deal with the dragged item getting unmounted and rebuilt during drag', (WidgetTester tester) async { // See https://github.com/flutter/flutter/issues/74840 for more details. final List<int> items = List<int>.generate(100, (int index) => index); @@ -1585,7 +1605,7 @@ void main() { expect(items.take(8), orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7])); }); - testWidgets('ReorderableListView calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { final List<int> items = List<int>.generate(8, (int index) => index); int? startIndex, endIndex; final Finder item0 = find.textContaining('item 0'); @@ -1661,7 +1681,7 @@ void main() { expect(endIndex, equals(0)); }); - testWidgets('ReorderableListView throws an error when key is not passed to its children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView throws an error when key is not passed to its children', (WidgetTester tester) async { final Widget reorderableListView = ReorderableListView.builder( itemBuilder: (BuildContext context, int index) { return SizedBox(child: Text('Item $index')); @@ -1677,7 +1697,7 @@ void main() { expect(exception.toString(), contains('Every item of ReorderableListView must have a key.')); }); - testWidgets('Throws an error if no overlay present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws an error if no overlay present', (WidgetTester tester) async { final Widget reorderableList = ReorderableListView( children: const <Widget>[ SizedBox(width: 100.0, height: 100.0, key: Key('C'), child: Text('C')), @@ -1709,7 +1729,7 @@ void main() { expect(exception.toString(), contains('ReorderableListView widgets require an Overlay widget ancestor')); }); - testWidgets('ReorderableListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ReorderableListView( itemExtent: 30, prototypeItem: const SizedBox(), @@ -1718,7 +1738,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ReorderableListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; expect(() => ReorderableListView.builder( itemBuilder: (BuildContext context, int index) { @@ -1738,7 +1758,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( @@ -1777,7 +1797,7 @@ void main() { expect(item2Height, 30.0); }); - testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( @@ -1819,7 +1839,7 @@ void main() { expect(item2Height, 30.0); }); - testWidgets('ReorderableListView auto scrolls speed is configurable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableListView auto scrolls speed is configurable', (WidgetTester tester) async { Future<void> pumpFor({ required Duration duration, Duration interval = const Duration(milliseconds: 50), @@ -1837,6 +1857,7 @@ void main() { Future<double> pumpListAndDrag({required double autoScrollerVelocityScalar}) async { final List<int> items = List<int>.generate(10, (int index) => index); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 55b38c18c86b2..b883025a5d207 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; @@ -18,8 +19,10 @@ const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); void main() { // Regression test for https://github.com/flutter/flutter/issues/103741 - testWidgets('extendBodyBehindAppBar change should not cause the body widget lose state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extendBodyBehindAppBar change should not cause the body widget lose state', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + Widget buildFrame({required bool extendBodyBehindAppBar}) { return MediaQuery( data: const MediaQueryData(), @@ -50,7 +53,7 @@ void main() { expect(controller.position.pixels, 100.0); }); - testWidgets('Scaffold drawer callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold drawer callback test', (WidgetTester tester) async { bool isDrawerOpen = false; bool isEndDrawerOpen = false; @@ -89,7 +92,7 @@ void main() { expect(isEndDrawerOpen, false); }); - testWidgets('Scaffold drawer callback test - only call when changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold drawer callback test - only call when changed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/87914 bool onDrawerChangedCalled = false; bool onEndDrawerChangedCalled = false; @@ -123,7 +126,7 @@ void main() { expect(onEndDrawerChangedCalled, false); }); - testWidgets('Scaffold control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold control test', (WidgetTester tester) async { final Key bodyKey = UniqueKey(); Widget boilerplate(Widget child) { return Localizations( @@ -173,7 +176,7 @@ void main() { expect(bodyBox.size, equals(const Size(800.0, 544.0))); }); - testWidgets('Scaffold large bottom padding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold large bottom padding test', (WidgetTester tester) async { final Key bodyKey = UniqueKey(); Widget boilerplate(Widget child) { @@ -230,7 +233,7 @@ void main() { expect(bodyBox.size, equals(const Size(800.0, 0.0))); }); - testWidgets('Floating action entrance/exit animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating action entrance/exit animation', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Scaffold( floatingActionButton: FloatingActionButton( key: Key('one'), @@ -268,7 +271,7 @@ void main() { expect(tester.binding.transientCallbackCount, greaterThan(0)); }); - testWidgets('Floating action button shrinks when bottom sheet becomes dominant', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating action button shrinks when bottom sheet becomes dominant', (WidgetTester tester) async { final DraggableScrollableController draggableController = DraggableScrollableController(); const double kBottomSheetDominatesPercentage = 0.3; @@ -307,7 +310,7 @@ void main() { } }); - testWidgets('Scaffold shows scrim when bottom sheet becomes dominant', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold shows scrim when bottom sheet becomes dominant', (WidgetTester tester) async { final DraggableScrollableController draggableController = DraggableScrollableController(); const double kBottomSheetDominatesPercentage = 0.3; const double kMinBottomSheetScrimOpacity = 0.1; @@ -346,7 +349,7 @@ void main() { } }); - testWidgets('Floating action button directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating action button directionality', (WidgetTester tester) async { Widget build(TextDirection textDirection) { return Directionality( textDirection: textDirection, @@ -374,7 +377,7 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 356.0)); }); - testWidgets('Floating Action Button bottom padding not consumed by viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating Action Button bottom padding not consumed by viewInsets', (WidgetTester tester) async { final Widget child = Directionality( textDirection: TextDirection.ltr, child: Scaffold( @@ -412,7 +415,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('viewPadding change should trigger _ScaffoldLayout re-layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('viewPadding change should trigger _ScaffoldLayout re-layout', (WidgetTester tester) async { Widget buildFrame(EdgeInsets viewPadding) { return MediaQuery( data: MediaQueryData( @@ -443,11 +446,12 @@ void main() { expect(renderBox.debugNeedsLayout, true); }); - testWidgets('Drawer scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer scrolling', (WidgetTester tester) async { final Key drawerKey = UniqueKey(); const double appBarHeight = 256.0; final ScrollController scrollOffset = ScrollController(); + addTearDown(scrollOffset.dispose); await tester.pumpWidget( MaterialApp( @@ -526,7 +530,7 @@ void main() { ); } - testWidgets('Tapping the status bar scrolls to top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping the status bar scrolls to top', (WidgetTester tester) async { await tester.pumpWidget(buildStatusBarTestApp(debugDefaultTargetPlatformOverride)); final ScrollableState scrollable = tester.state(find.byType(Scrollable)); scrollable.position.jumpTo(500.0); @@ -536,7 +540,7 @@ void main() { expect(scrollable.position.pixels, equals(0.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Tapping the status bar scrolls to top with ease out curve animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping the status bar scrolls to top with ease out curve animation', (WidgetTester tester) async { const int duration = 1000; final List<double> stops = <double>[0.842, 0.959, 0.993, 1.0]; const double scrollOffset = 1000; @@ -565,7 +569,7 @@ void main() { expect(scrollable.position.pixels, equals(0.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Tapping the status bar does not scroll to top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping the status bar does not scroll to top', (WidgetTester tester) async { await tester.pumpWidget(buildStatusBarTestApp(TargetPlatform.android)); final ScrollableState scrollable = tester.state(find.byType(Scrollable)); scrollable.position.jumpTo(500.0); @@ -576,7 +580,7 @@ void main() { expect(scrollable.position.pixels, equals(500.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); - testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bottom sheet cannot overlap app bar', (WidgetTester tester) async { final Key sheetKey = UniqueKey(); await tester.pumpWidget( @@ -617,7 +621,7 @@ void main() { expect(appBarBottomRight, equals(sheetTopRight)); }); - testWidgets('BottomSheet bottom padding is not consumed by viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BottomSheet bottom padding is not consumed by viewInsets', (WidgetTester tester) async { final Widget child = Directionality( textDirection: TextDirection.ltr, child: Scaffold( @@ -650,7 +654,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('Persistent bottom buttons are persistent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent bottom buttons are persistent', (WidgetTester tester) async { bool didPressButton = false; await tester.pumpWidget( MaterialApp( @@ -680,7 +684,7 @@ void main() { expect(didPressButton, isTrue); }); - testWidgets('Persistent bottom buttons alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent bottom buttons alignment', (WidgetTester tester) async { Widget buildApp(AlignmentDirectional persistentAlignment) { return MaterialApp( home: Scaffold( @@ -715,7 +719,7 @@ void main() { expect(tester.getTopLeft(footerButton).dx, 8.0); }); - testWidgets('Persistent bottom buttons apply media padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent bottom buttons apply media padding', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -742,7 +746,7 @@ void main() { expect(tester.getBottomRight(buttonsBar), const Offset(770.0, 560.0)); }); - testWidgets('persistentFooterButtons with bottomNavigationBar apply SafeArea properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('persistentFooterButtons with bottomNavigationBar apply SafeArea properly', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/92039 await tester.pumpWidget( MaterialApp( @@ -792,7 +796,7 @@ void main() { expect(tester.getTopLeft(buttonsBar), const Offset(0.0, 488.0)); }); - testWidgets('Persistent bottom buttons bottom padding is not consumed by viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent bottom buttons bottom padding is not consumed by viewInsets', (WidgetTester tester) async { final Widget child = Directionality( textDirection: TextDirection.ltr, child: Scaffold( @@ -845,11 +849,11 @@ void main() { expect(icon.icon, expectedIcon); } - testWidgets('Back arrow uses correct default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Back arrow uses correct default', (WidgetTester tester) async { await expectBackIcon(tester, Icons.arrow_back); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia })); - testWidgets('Back arrow uses correct default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Back arrow uses correct default', (WidgetTester tester) async { await expectBackIcon(tester, kIsWeb ? Icons.arrow_back : Icons.arrow_back_ios); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }); @@ -900,21 +904,21 @@ void main() { ); } - testWidgets('Close button shows correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Close button shows correctly', (WidgetTester tester) async { await expectCloseIcon(tester, materialRouteBuilder, 'materialRouteBuilder'); }, variant: TargetPlatformVariant.all()); - testWidgets('Close button shows correctly with PageRouteBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Close button shows correctly with PageRouteBuilder', (WidgetTester tester) async { await expectCloseIcon(tester, pageRouteBuilder, 'pageRouteBuilder'); }, variant: TargetPlatformVariant.all()); - testWidgets('Close button shows correctly with custom page route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Close button shows correctly with custom page route', (WidgetTester tester) async { await expectCloseIcon(tester, customPageRouteBuilder, 'customPageRouteBuilder'); }, variant: TargetPlatformVariant.all()); }); group('body size', () { - testWidgets('body size with container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with container', (WidgetTester tester) async { final Key testKey = UniqueKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -931,7 +935,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), Offset.zero); }); - testWidgets('body size with sized container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with sized container', (WidgetTester tester) async { final Key testKey = UniqueKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -949,7 +953,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), Offset.zero); }); - testWidgets('body size with centered container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with centered container', (WidgetTester tester) async { final Key testKey = UniqueKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -968,7 +972,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), Offset.zero); }); - testWidgets('body size with button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with button', (WidgetTester tester) async { final Key testKey = UniqueKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -987,7 +991,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), Offset.zero); }); - testWidgets('body size with extendBody', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with extendBody', (WidgetTester tester) async { final Key bodyKey = UniqueKey(); late double mediaQueryBottom; @@ -1044,7 +1048,7 @@ void main() { expect(mediaQueryBottom, 0.0); }); - testWidgets('body size with extendBodyBehindAppBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('body size with extendBodyBehindAppBar', (WidgetTester tester) async { final Key appBarKey = UniqueKey(); final Key bodyKey = UniqueKey(); @@ -1141,7 +1145,7 @@ void main() { }); }); - testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Open drawer hides underlying semantics tree', (WidgetTester tester) async { const String bodyLabel = 'I am the body'; const String persistentFooterButtonLabel = 'a button on the bottom'; const String bottomNavigationBarLabel = 'a bar in an app'; @@ -1177,7 +1181,7 @@ void main() { semantics.dispose(); }); - testWidgets('Scaffold and extreme window padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold and extreme window padding', (WidgetTester tester) async { final Key appBar = UniqueKey(); final Key body = UniqueKey(); final Key floatingActionButton = UniqueKey(); @@ -1282,7 +1286,7 @@ void main() { expect(tester.getRect(find.byKey(insideBottomNavigationBar)), const Rect.fromLTRB(20.0, 515.0, 750.0, 540.0)); }); - testWidgets('Scaffold and extreme window padding - persistent footer buttons only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold and extreme window padding - persistent footer buttons only', (WidgetTester tester) async { final Key appBar = UniqueKey(); final Key body = UniqueKey(); final Key floatingActionButton = UniqueKey(); @@ -1377,7 +1381,7 @@ void main() { group('ScaffoldGeometry', () { - testWidgets('bottomNavigationBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bottomNavigationBar', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp(home: Scaffold( body: Container(), @@ -1399,7 +1403,7 @@ void main() { ); }); - testWidgets('no bottomNavigationBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no bottomNavigationBar', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: Scaffold( body: ConstrainedBox( constraints: const BoxConstraints.expand(height: 80.0), @@ -1416,7 +1420,7 @@ void main() { ); }); - testWidgets('Scaffold BottomNavigationBar bottom padding is not consumed by viewInsets.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scaffold BottomNavigationBar bottom padding is not consumed by viewInsets.', (WidgetTester tester) async { Widget boilerplate(Widget child) { return Localizations( locale: const Locale('en', 'us'), @@ -1479,7 +1483,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('floatingActionButton', (WidgetTester tester) async { + testWidgetsWithLeakTracking('floatingActionButton', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp(home: Scaffold( body: Container(), @@ -1502,7 +1506,7 @@ void main() { ); }); - testWidgets('no floatingActionButton', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no floatingActionButton', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: Scaffold( body: ConstrainedBox( constraints: const BoxConstraints.expand(height: 80.0), @@ -1519,7 +1523,7 @@ void main() { ); }); - testWidgets('floatingActionButton entrance/exit animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('floatingActionButton entrance/exit animation', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp(home: Scaffold( body: ConstrainedBox( @@ -1581,7 +1585,7 @@ void main() { ); }); - testWidgets('change notifications', (WidgetTester tester) async { + testWidgetsWithLeakTracking('change notifications', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); int numNotificationsAtLastFrame = 0; await tester.pumpWidget(MaterialApp(home: Scaffold( @@ -1619,7 +1623,7 @@ void main() { numNotificationsAtLastFrame = listenerState.numNotifications; }); - testWidgets('Simultaneous drawers on either side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simultaneous drawers on either side', (WidgetTester tester) async { const String bodyLabel = 'I am the body'; const String drawerLabel = 'I am the label on start side'; const String endDrawerLabel = 'I am the label on end side'; @@ -1653,7 +1657,7 @@ void main() { semantics.dispose(); }); - testWidgets('Drawer state query correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer state query correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SafeArea( @@ -1709,7 +1713,7 @@ void main() { expect(scaffoldState.isDrawerOpen, true); }); - testWidgets('Dual Drawer Opening', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dual Drawer Opening', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SafeArea( @@ -1761,7 +1765,7 @@ void main() { expect(find.text('drawer'), findsOneWidget); }); - testWidgets('Drawer opens correctly with padding from MediaQuery (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer opens correctly with padding from MediaQuery (LTR)', (WidgetTester tester) async { const double simulatedNotchSize = 40.0; await tester.pumpWidget( MaterialApp( @@ -1815,7 +1819,7 @@ void main() { expect(scaffoldState.isDrawerOpen, true); }); - testWidgets('Drawer opens correctly with padding from MediaQuery (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer opens correctly with padding from MediaQuery (RTL)', (WidgetTester tester) async { const double simulatedNotchSize = 40.0; await tester.pumpWidget( MaterialApp( @@ -1879,7 +1883,7 @@ void main() { }); }); - testWidgets('Drawer opens correctly with custom edgeDragWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer opens correctly with custom edgeDragWidth', (WidgetTester tester) async { // The default edge drag width is 20.0. await tester.pumpWidget( MaterialApp( @@ -1925,7 +1929,7 @@ void main() { expect(scaffoldState.isDrawerOpen, true); }); - testWidgets('Drawer does not open with a drag gesture when it is disabled on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer does not open with a drag gesture when it is disabled on mobile', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1989,7 +1993,7 @@ void main() { expect(scaffoldState.isDrawerOpen, false); }, variant: TargetPlatformVariant.mobile()); - testWidgets('Drawer does not open with a drag gesture on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer does not open with a drag gesture on desktop', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2029,7 +2033,7 @@ void main() { expect(scaffoldState.isDrawerOpen, false); }, variant: TargetPlatformVariant.desktop()); - testWidgets('End drawer does not open with a drag gesture when it is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('End drawer does not open with a drag gesture when it is disabled', (WidgetTester tester) async { late double screenWidth; await tester.pumpWidget( MaterialApp( @@ -2099,7 +2103,7 @@ void main() { expect(scaffoldState.isEndDrawerOpen, false); }); - testWidgets('Nested scaffold body insets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested scaffold body insets', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/20295 final Key bodyKey = UniqueKey(); @@ -2151,7 +2155,7 @@ void main() { }); group('FlutterError control test', () { - testWidgets('showBottomSheet() while Scaffold has bottom sheet', + testWidgetsWithLeakTracking('showBottomSheet() while Scaffold has bottom sheet', (WidgetTester tester) async { final GlobalKey<ScaffoldState> key = GlobalKey<ScaffoldState>(); await tester.pumpWidget( @@ -2200,7 +2204,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'didUpdate bottomSheet while a previous bottom sheet is still displayed', (WidgetTester tester) async { final GlobalKey<ScaffoldState> key = GlobalKey<ScaffoldState>(); @@ -2255,7 +2259,7 @@ void main() { }, ); - testWidgets('Call to Scaffold.of() without context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Call to Scaffold.of() without context', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Builder( @@ -2328,7 +2332,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Call to Scaffold.geometryOf() without context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Call to Scaffold.geometryOf() without context', (WidgetTester tester) async { ValueListenable<ScaffoldGeometry>? geometry; await tester.pumpWidget( MaterialApp( @@ -2394,7 +2398,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('FloatingActionButton always keeps the same position regardless of extendBodyBehindAppBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FloatingActionButton always keeps the same position regardless of extendBodyBehindAppBar', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( appBar: AppBar(), @@ -2424,7 +2428,7 @@ void main() { }); }); - testWidgets('ScaffoldMessenger.maybeOf can return null if not found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger.maybeOf can return null if not found', (WidgetTester tester) async { ScaffoldMessengerState? scaffoldMessenger; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(Directionality( @@ -2455,7 +2459,7 @@ void main() { expect(scaffoldMessenger, isNull); }); - testWidgets('ScaffoldMessenger.of will assert if not found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger.of will assert if not found', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); final List<dynamic> exceptions = <dynamic>[]; @@ -2518,7 +2522,7 @@ void main() { )); }); - testWidgets('ScaffoldMessenger checks for nesting when a new Scaffold is registered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger checks for nesting when a new Scaffold is registered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/77251 const String snackBarContent = 'SnackBar Content'; await tester.pumpWidget(MaterialApp( @@ -2589,7 +2593,7 @@ void main() { expect(find.text(snackBarContent), findsNothing); }); - testWidgets('Drawer can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/106131 bool isDrawerOpen = false; bool isEndDrawerOpen = false; @@ -2637,7 +2641,7 @@ void main() { expect(isEndDrawerOpen, false); }); - testWidgets('ScaffoldMessenger showSnackBar throws an intuitive error message if called during build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger showSnackBar throws an intuitive error message if called during build', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Builder( @@ -2654,7 +2658,7 @@ void main() { expect(summary.toString(), 'The showSnackBar() method cannot be called during build.'); }); - testWidgets('Persistent BottomSheet is not dismissible via a11y means', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Persistent BottomSheet is not dismissible via a11y means', (WidgetTester tester) async { final Key bottomSheetKey = UniqueKey(); await tester.pumpWidget(MaterialApp( @@ -2677,7 +2681,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/117004 - testWidgets('can rebuild and remove bottomSheet at the same time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can rebuild and remove bottomSheet at the same time', (WidgetTester tester) async { bool themeIsLight = true; bool? defaultBottomSheet = true; final GlobalKey bottomSheetKey1 = GlobalKey(); @@ -2756,7 +2760,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('showBottomSheet removes scrim when draggable sheet is dismissed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showBottomSheet removes scrim when draggable sheet is dismissed', (WidgetTester tester) async { final DraggableScrollableController draggableController = DraggableScrollableController(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); PersistentBottomSheetController<void>? sheetController; @@ -2802,7 +2806,7 @@ void main() { expect(findModalBarrier(), findsNothing); }); - testWidgets("Closing bottom sheet & removing FAB at the same time doesn't throw assertion", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Closing bottom sheet & removing FAB at the same time doesn't throw assertion", (WidgetTester tester) async { final Key bottomSheetKey = UniqueKey(); PersistentBottomSheetController<void>? controller; bool show = true; diff --git a/packages/flutter/test/material/scrollbar_paint_test.dart b/packages/flutter/test/material/scrollbar_paint_test.dart index 42ce23133f54f..09be56932fa0f 100644 --- a/packages/flutter/test/material/scrollbar_paint_test.dart +++ b/packages/flutter/test/material/scrollbar_paint_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color _kAndroidThumbIdleColor = Color(0xffbcbcbc); @@ -26,7 +25,7 @@ Widget _buildSingleChildScrollViewWithScrollbar({ } void main() { - testWidgets('Viewport basic test (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport basic test (LTR)', (WidgetTester tester) async { await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar( child: const SizedBox(width: 4000.0, height: 4000.0), )); @@ -52,7 +51,7 @@ void main() { ); }); - testWidgets('Viewport basic test (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport basic test (RTL)', (WidgetTester tester) async { await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar( textDirection: TextDirection.rtl, child: const SizedBox(width: 4000.0, height: 4000.0), @@ -79,7 +78,7 @@ void main() { ); }); - testWidgets('works with MaterialApp and Scaffold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with MaterialApp and Scaffold', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: MediaQuery( data: const MediaQueryData( @@ -123,7 +122,7 @@ void main() { ); }); - testWidgets("should not paint when there isn't enough space", (WidgetTester tester) async { + testWidgetsWithLeakTracking("should not paint when there isn't enough space", (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: MediaQuery( data: const MediaQueryData( diff --git a/packages/flutter/test/material/scrollbar_test.dart b/packages/flutter/test/material/scrollbar_test.dart index 44addd4fc5c47..25e2fdf4e00b6 100644 --- a/packages/flutter/test/material/scrollbar_test.dart +++ b/packages/flutter/test/material/scrollbar_test.dart @@ -16,8 +16,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300); const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); @@ -72,7 +71,7 @@ class NoScrollbarBehavior extends MaterialScrollBehavior { } void main() { - testWidgets("Scrollbar doesn't show when tapping list", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Scrollbar doesn't show when tapping list", (WidgetTester tester) async { await tester.pumpWidget( _buildBoilerplate( child: Center( @@ -116,7 +115,7 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); }); - testWidgets('ScrollbarPainter does not divide by zero', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarPainter does not divide by zero', (WidgetTester tester) async { await tester.pumpWidget( _buildBoilerplate(child: SizedBox( height: 200.0, @@ -158,7 +157,7 @@ void main() { expect(canvas.invocations.isEmpty, isTrue); }); - testWidgets('When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async { Widget viewWithScroll() { return _buildBoilerplate( child: Theme( @@ -181,7 +180,7 @@ void main() { expect(exception, isAssertionError); }); - testWidgets('When thumbVisibility is true, must pass a controller that is attached to a scroll view or find PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When thumbVisibility is true, must pass a controller that is attached to a scroll view or find PrimaryScrollController', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -204,9 +203,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); final AssertionError exception = tester.takeException() as AssertionError; expect(exception, isAssertionError); + + controller.dispose(); }); - testWidgets('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -230,9 +231,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); await tester.pumpAndSettle(); expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }); - testWidgets('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -262,9 +265,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); await tester.pumpAndSettle(); expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async { Widget viewWithScroll() { @@ -290,7 +295,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'When thumbVisibility is true, must pass a controller that is attached to a scroll view or find PrimaryScrollController', (WidgetTester tester) async { final ScrollController controller = ScrollController(); @@ -315,10 +320,12 @@ void main() { await tester.pumpWidget(viewWithScroll()); final AssertionError exception = tester.takeException() as AssertionError; expect(exception, isAssertionError); + + controller.dispose(); }, ); - testWidgets('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -342,9 +349,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); await tester.pumpAndSettle(); expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }); - testWidgets('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -374,9 +383,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); await tester.pumpAndSettle(); expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }); - testWidgets('On first render with thumbVisibility: false, the thumb is hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On first render with thumbVisibility: false, the thumb is hidden', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll() { return _buildBoilerplate( @@ -400,9 +411,11 @@ void main() { await tester.pumpWidget(viewWithScroll()); await tester.pumpAndSettle(); expect(find.byType(Scrollbar), isNot(paints..rect())); + + controller.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'With thumbVisibility: true, fling a scroll. While it is still scrolling, set thumbVisibility: false. The thumb should not fade out until the scrolling stops.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); @@ -453,10 +466,12 @@ void main() { await tester.pumpAndSettle(); // Scrollbar is not showing after scroll finishes expect(find.byType(Scrollbar), isNot(paints..rect())); + + controller.dispose(); }, ); - testWidgets( + testWidgetsWithLeakTracking( 'With thumbVisibility: false, set thumbVisibility: true. The thumb should be always shown directly', (WidgetTester tester) async { final ScrollController controller = ScrollController(); @@ -502,10 +517,12 @@ void main() { await tester.pumpAndSettle(); // Scrollbar is not showing after scroll finishes expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }, ); - testWidgets( + testWidgetsWithLeakTracking( 'With thumbVisibility: false, fling a scroll. While it is still scrolling, set thumbVisibility: true. The thumb should not fade even after the scrolling stops', (WidgetTester tester) async { final ScrollController controller = ScrollController(); @@ -562,10 +579,12 @@ void main() { await tester.pumpAndSettle(); // Scrollbar thumb is showing after scroll finishes and timer ends. expect(find.byType(Scrollbar), paints..rect()); + + controller.dispose(); }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Toggling thumbVisibility while not scrolling fades the thumb in/out. This works even when you have never scrolled at all yet', (WidgetTester tester) async { final ScrollController controller = ScrollController(); @@ -611,10 +630,12 @@ void main() { await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); expect(materialScrollbar, isNot(paints..rect())); + + controller.dispose(); }, ); - testWidgets('Scrollbar respects thickness and radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar respects thickness and radius', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll({Radius? radius}) { return _buildBoilerplate( @@ -674,9 +695,11 @@ void main() { )); await tester.pumpAndSettle(); + + controller.dispose(); }); - testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping the track area pages the Scroll View', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( Directionality( @@ -764,9 +787,11 @@ void main() { color: _kAndroidThumbIdleColor, ), ); + + scrollController.dispose(); }); - testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar never goes away until finger lift', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scrollbar( @@ -847,7 +872,7 @@ void main() { ); }); - testWidgets('Scrollbar thumb can be dragged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( MaterialApp( @@ -937,9 +962,11 @@ void main() { color: _kAndroidThumbIdleColor, ), ); + + scrollController.dispose(); }); - testWidgets('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -987,7 +1014,7 @@ void main() { }), ); - testWidgets('Hover animation is not triggered by tap gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hover animation is not triggered by tap gestures', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1062,7 +1089,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux }), ); - testWidgets('ScrollbarThemeData.thickness replaces hoverThickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarThemeData.thickness replaces hoverThickness', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1134,7 +1161,7 @@ void main() { }), ); - testWidgets('ScrollbarThemeData.trackVisibility replaces showTrackOnHover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarThemeData.trackVisibility replaces showTrackOnHover', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1202,7 +1229,7 @@ void main() { }), ); - testWidgets('Scrollbar showTrackOnHover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar showTrackOnHover', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1265,7 +1292,7 @@ void main() { }), ); - testWidgets('Adaptive scrollbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adaptive scrollbar', (WidgetTester tester) async { Widget viewWithScroll(TargetPlatform platform) { return _buildBoilerplate( child: Theme( @@ -1302,7 +1329,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scrollbar passes controller to CupertinoScrollbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar passes controller to CupertinoScrollbar', (WidgetTester tester) async { final ScrollController controller = ScrollController(); Widget viewWithScroll(TargetPlatform? platform) { return _buildBoilerplate( @@ -1332,9 +1359,11 @@ void main() { expect(find.byType(CupertinoScrollbar), paints..rrect()); final CupertinoScrollbar scrollbar = tester.widget<CupertinoScrollbar>(find.byType(CupertinoScrollbar)); expect(scrollbar.controller, isNotNull); + + controller.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets("Scrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Scrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final GlobalKey outerKey = GlobalKey(); @@ -1397,7 +1426,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('Scrollbar dragging can be disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar dragging can be disabled', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( MaterialApp( @@ -1464,9 +1493,11 @@ void main() { await tester.pumpAndSettle(); // The offset should not have changed. expect(scrollController.offset, scrollAmount); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia })); - testWidgets('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async { int tapCount = 0; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -1558,9 +1589,11 @@ void main() { // The offset should not have changed. expect(scrollController.offset, scrollAmount * 2); expect(tapCount, 2); + + scrollController.dispose(); }); - testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/70105 final ScrollController scrollController = ScrollController(); await tester.pumpWidget( @@ -1730,9 +1763,11 @@ void main() { color: const Color(0xffbcbcbc), ), ); + + scrollController.dispose(); }); - testWidgets('Scrollbar.thumbVisibility triggers assertion when multiple ScrollPositions are attached.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar.thumbVisibility triggers assertion when multiple ScrollPositions are attached.', (WidgetTester tester) async { Widget getTabContent({ ScrollController? scrollController }) { return Scrollbar( thumbVisibility: true, @@ -1797,9 +1832,11 @@ void main() { error.message, contains('The provided ScrollController is currently attached to more than one ScrollPosition.'), ); + + scrollController.dispose(); }); - testWidgets('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); Widget buildScrollWithOrientation(ScrollbarOrientation orientation) { @@ -1845,5 +1882,7 @@ void main() { color: _kAndroidThumbIdleColor, ), ); + + scrollController.dispose(); }); } diff --git a/packages/flutter/test/material/scrollbar_theme_test.dart b/packages/flutter/test/material/scrollbar_theme_test.dart index 50c6ba74f6ad7..c1e616629c11a 100644 --- a/packages/flutter/test/material/scrollbar_theme_test.dart +++ b/packages/flutter/test/material/scrollbar_theme_test.dart @@ -8,8 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // The const represents the starting position of the scrollbar thumb for // the below tests. The thumb is 90 pixels long, and 8 pixels wide, with a 2 @@ -31,7 +30,7 @@ void main() { expect(identical(ScrollbarThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Passing no ScrollbarTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passing no ScrollbarTheme returns defaults', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( MaterialApp( @@ -114,6 +113,8 @@ void main() { color: const Color(0x80000000), ), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.macOS, @@ -122,7 +123,7 @@ void main() { }), ); - testWidgets('Scrollbar uses values from ScrollbarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar uses values from ScrollbarTheme', (WidgetTester tester) async { final ScrollbarThemeData scrollbarTheme = _scrollbarTheme(); final ScrollController scrollController = ScrollController(); await tester.pumpWidget(MaterialApp( @@ -205,6 +206,8 @@ void main() { color: const Color(0xff2196f3), ), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.macOS, @@ -213,7 +216,7 @@ void main() { }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Scrollbar uses values from ScrollbarTheme if exists instead of values from Theme', (WidgetTester tester) async { final ScrollbarThemeData scrollbarTheme = _scrollbarTheme(); @@ -254,10 +257,12 @@ void main() { color: const Color(0xFF000000), ), ); + + scrollController.dispose(); }, ); - testWidgets('ScrollbarTheme can disable gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarTheme can disable gestures', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false, scrollbarTheme: const ScrollbarThemeData(interactive: false)), @@ -302,9 +307,11 @@ void main() { color: _kDefaultIdleThumbColor, ), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia })); - testWidgets('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false, scrollbarTheme: const ScrollbarThemeData(interactive: false)), @@ -350,9 +357,11 @@ void main() { color: _kDefaultIdleThumbColor, ), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia })); - testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar widget properties take priority over theme', (WidgetTester tester) async { const double thickness = 4.0; const bool showTrackOnHover = true; const Radius radius = Radius.circular(3.0); @@ -443,6 +452,8 @@ void main() { color: const Color(0x80000000), ), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.macOS, @@ -451,31 +462,35 @@ void main() { }), ); - testWidgets('ThemeData colorScheme is used when no ScrollbarTheme is set', (WidgetTester tester) async { - Widget buildFrame(ThemeData appTheme) { + testWidgetsWithLeakTracking('ThemeData colorScheme is used when no ScrollbarTheme is set', (WidgetTester tester) async { + (ScrollController, Widget) buildFrame(ThemeData appTheme) { final ScrollController scrollController = ScrollController(); - return MaterialApp( - theme: appTheme, - home: ScrollConfiguration( - behavior: const NoScrollbarBehavior(), - child: Scrollbar( - thumbVisibility: true, - showTrackOnHover: true, - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: const SizedBox(width: 4000.0, height: 4000.0), + return ( + scrollController, + MaterialApp( + theme: appTheme, + home: ScrollConfiguration( + behavior: const NoScrollbarBehavior(), + child: Scrollbar( + thumbVisibility: true, + showTrackOnHover: true, + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: const SizedBox(width: 4000.0, height: 4000.0), + ), + ), ), ), - ), - ); + ); } // Scrollbar defaults for light themes: // - coloring based on ColorScheme.onSurface - await tester.pumpWidget(buildFrame(ThemeData( + final (ScrollController controller1, Widget frame1) = buildFrame(ThemeData( colorScheme: const ColorScheme.light(), - ))); + )); + await tester.pumpWidget(frame1); await tester.pumpAndSettle(); // Idle scrollbar behavior expect( @@ -546,9 +561,10 @@ void main() { // Scrollbar defaults for dark themes: // - coloring slightly different based on ColorScheme.onSurface - await tester.pumpWidget(buildFrame(ThemeData( + final (ScrollController controller2, Widget frame2) = buildFrame(ThemeData( colorScheme: const ColorScheme.dark(), - ))); + )); + await tester.pumpWidget(frame2); await tester.pumpAndSettle(); // Theme change animation // Idle scrollbar behavior @@ -611,6 +627,9 @@ void main() { color: const Color(0xa6ffffff), ), ); + + controller1.dispose(); + controller2.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.macOS, @@ -619,7 +638,7 @@ void main() { }), ); - testWidgets('ScrollbarThemeData.trackVisibility test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarThemeData.trackVisibility test', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); bool? getTrackVisibility(Set<MaterialState> states) { return true; @@ -657,6 +676,8 @@ void main() { ) ..rrect(color: const Color(0xff4caf50)), ); + + scrollController.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.macOS, @@ -665,7 +686,7 @@ void main() { }), ); - testWidgets('Default ScrollbarTheme debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ScrollbarTheme debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ScrollbarThemeData().debugFillProperties(builder); @@ -677,7 +698,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ScrollbarTheme implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarTheme implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); ScrollbarThemeData( thickness: MaterialStateProperty.resolveWith(_getThickness), diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index d30cd8e0a67d6..3398560998b7a 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -6,11 +6,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SearchBar defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SearchBar defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colorScheme = theme.colorScheme; @@ -693,6 +692,108 @@ void main() { expect(inputText.style.color, focusedColor); }); + testWidgets('SearchBar respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchBar(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchBar( + textCapitalization: textCapitalization, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchBar(TextCapitalization.characters)); + await tester.pump(); + TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.sentences)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.sentences); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.words)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.words); + + await tester.pumpWidget(buildSearchBar(TextCapitalization.none)); + await tester.pump(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.none); + }); + + testWidgets('SearchAnchor respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchAnchor(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchAnchor( + textCapitalization: textCapitalization, + builder: (BuildContext context, SearchController controller) { + return IconButton( + icon: const Icon(Icons.ac_unit), + onPressed: () { + controller.openView(); + }, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return <Widget>[]; + }, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); + await tester.pump(); + await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); + await tester.pumpAndSettle(); + TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); + await tester.pump(); + + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.none)); + await tester.pump(); + await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); + await tester.pumpAndSettle(); + textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.none); + }); + + testWidgets('SearchAnchor.bar respects textCapitalization property', (WidgetTester tester) async { + Widget buildSearchAnchor(TextCapitalization textCapitalization) { + return MaterialApp( + home: Center( + child: Material( + child: SearchAnchor.bar( + textCapitalization: textCapitalization, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return <Widget>[]; + }, + ), + ), + ), + ); + } + await tester.pumpWidget(buildSearchAnchor(TextCapitalization.characters)); + await tester.pump(); + await tester.tap(find.byType(SearchBar)); // Open search view. + await tester.pumpAndSettle(); + final Finder textFieldFinder = find.descendant(of: findViewContent(), matching: find.byType(TextField)); + final TextField textFieldInView = tester.widget<TextField>(textFieldFinder); + expect(textFieldInView.textCapitalization, TextCapitalization.characters); + // Close search view. + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back)); + await tester.pumpAndSettle(); + final TextField textField = tester.widget(find.byType(TextField)); + expect(textField.textCapitalization, TextCapitalization.characters); + }); + testWidgets('hintStyle can override textStyle for hintText', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1954,6 +2055,117 @@ void main() { expect(inputText.style.color, theme.colorScheme.onSurface); }); }); + + testWidgets('SearchAnchor view respects theme brightness', (WidgetTester tester) async { + Widget buildSearchAnchor(ThemeData theme) { + return MaterialApp( + theme: theme, + home: Center( + child: Material( + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return IconButton( + icon: const Icon(Icons.ac_unit), + onPressed: () { + controller.openView(); + }, + ); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return <Widget>[]; + }, + ), + ), + ), + ); + } + + ThemeData theme = ThemeData(brightness: Brightness.light); + await tester.pumpWidget(buildSearchAnchor(theme)); + + // Open the search view. + await tester.tap(find.widgetWithIcon(IconButton, Icons.ac_unit)); + await tester.pumpAndSettle(); + + // Test the search view background color. + Material material = getSearchViewMaterial(tester); + expect(material.color, theme.colorScheme.surface); + + // Change the theme brightness. + theme = ThemeData(brightness: Brightness.dark); + await tester.pumpWidget(buildSearchAnchor(theme)); + await tester.pumpAndSettle(); + + // Test the search view background color. + material = getSearchViewMaterial(tester); + expect(material.color, theme.colorScheme.surface); + }); + + testWidgets('Search view widgets can inherit local themes', (WidgetTester tester) async { + final ThemeData globalTheme = ThemeData(colorSchemeSeed: Colors.red); + final ThemeData localTheme = ThemeData( + colorSchemeSeed: Colors.green, + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + backgroundColor: const Color(0xffffff00) + ), + ), + cardTheme: const CardTheme(color: Color(0xff00ffff)), + ); + Widget buildSearchAnchor() { + return MaterialApp( + theme: globalTheme, + home: Center( + child: Builder( + builder: (BuildContext context) { + return Theme( + data: localTheme, + child: Material( + child: SearchAnchor.bar( + suggestionsBuilder: (BuildContext context, SearchController controller) { + return <Widget>[ + Card( + child: ListTile( + onTap: () {}, + title: const Text('Item 1'), + ), + ), + ]; + }, + ), + ), + ); + } + ), + ), + ); + } + + await tester.pumpWidget(buildSearchAnchor()); + + // Open the search view. + await tester.tap(find.byType(SearchBar)); + await tester.pumpAndSettle(); + + // Test the search view background color. + final Material searchViewMaterial = getSearchViewMaterial(tester); + expect(searchViewMaterial.color, localTheme.colorScheme.surface); + + // Test the search view icons background color. + final Material iconButtonMaterial = tester.widget<Material>(find.descendant( + of: find.byType(IconButton), + matching: find.byType(Material), + ).first); + expect(find.byWidget(iconButtonMaterial), findsOneWidget); + expect(iconButtonMaterial.color, localTheme.iconButtonTheme.style?.backgroundColor?.resolve(<MaterialState>{})); + + // Test the suggestion card color. + final Material suggestionMaterial = tester.widget<Material>(find.descendant( + of: find.byType(Card), + matching: find.byType(Material), + ).first); + expect(suggestionMaterial.color, localTheme.cardTheme.color); + }); } Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async { diff --git a/packages/flutter/test/material/search_bar_theme_test.dart b/packages/flutter/test/material/search_bar_theme_test.dart index 7a6ef96865896..358c7ddb79e36 100644 --- a/packages/flutter/test/material/search_bar_theme_test.dart +++ b/packages/flutter/test/material/search_bar_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('SearchBarThemeData copyWith, ==, hashCode basics', () { @@ -33,6 +34,7 @@ void main() { expect(themeData.textStyle, null); expect(themeData.hintStyle, null); expect(themeData.constraints, null); + expect(themeData.textCapitalization, null); const SearchBarTheme theme = SearchBarTheme(data: SearchBarThemeData(), child: SizedBox()); expect(theme.data.elevation, null); @@ -46,9 +48,10 @@ void main() { expect(theme.data.textStyle, null); expect(theme.data.hintStyle, null); expect(theme.data.constraints, null); + expect(theme.data.textCapitalization, null); }); - testWidgets('Default SearchBarThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SearchBarThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SearchBarThemeData().debugFillProperties(builder); @@ -60,7 +63,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('SearchBarThemeData implements debugFillProperties', ( + testWidgetsWithLeakTracking('SearchBarThemeData implements debugFillProperties', ( WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SearchBarThemeData( @@ -75,6 +78,7 @@ void main() { textStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 24.0)), hintStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 16.0)), constraints: BoxConstraints(minWidth: 350, maxWidth: 850), + textCapitalization: TextCapitalization.characters, ).debugFillProperties(builder); final List<String> description = builder.properties @@ -93,6 +97,7 @@ void main() { expect(description[8], 'textStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 24.0))'); expect(description[9], 'hintStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 16.0))'); expect(description[10], 'constraints: BoxConstraints(350.0<=w<=850.0, 0.0<=h<=Infinity)'); + expect(description[11], 'textCapitalization: TextCapitalization.characters'); }); group('[Theme, SearchBarTheme, SearchBar properties overrides]', () { @@ -118,6 +123,7 @@ void main() { const MaterialStateProperty<TextStyle?> textStyle = MaterialStatePropertyAll<TextStyle>(textStyleValue); const MaterialStateProperty<TextStyle?> hintStyle = MaterialStatePropertyAll<TextStyle>(hintStyleValue); const BoxConstraints constraints = BoxConstraints(minWidth: 250.0, maxWidth: 300.0, minHeight: 80.0); + const TextCapitalization textCapitalization = TextCapitalization.words; const SearchBarThemeData searchBarTheme = SearchBarThemeData( elevation: elevation, @@ -131,6 +137,7 @@ void main() { textStyle: textStyle, hintStyle: hintStyle, constraints: constraints, + textCapitalization: textCapitalization, ); Widget buildFrame({ @@ -162,6 +169,7 @@ void main() { textStyle: textStyle, hintStyle: hintStyle, constraints: constraints, + textCapitalization: textCapitalization, ); }, ); @@ -221,6 +229,7 @@ void main() { final EditableText inputText = tester.widget(find.text('input')); expect(inputText.style.color, textStyleValue.color); expect(inputText.style.fontSize, textStyleValue.fontSize); + expect(inputText.textCapitalization, textCapitalization); final Rect barRect = tester.getRect(find.byType(SearchBar)); final Rect leadingRect = tester.getRect(find.byIcon(Icons.search)); @@ -233,19 +242,19 @@ void main() { expect(trailingRect.right, barRect.right - 16.0); } - testWidgets('SearchBar properties overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SearchBar properties overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(useSearchBarProperties: true)); await tester.pumpAndSettle(); // allow the animations to finish checkSearchBar(tester); }); - testWidgets('SearchBar theme data overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SearchBar theme data overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(searchBarThemeData: searchBarTheme)); await tester.pumpAndSettle(); checkSearchBar(tester); }); - testWidgets('Overall Theme SearchBar theme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme SearchBar theme overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallTheme: searchBarTheme)); await tester.pumpAndSettle(); checkSearchBar(tester); @@ -253,7 +262,7 @@ void main() { // Same as the previous tests with empty SearchBarThemeData's instead of null. - testWidgets('SearchBar properties overrides defaults, empty theme and overall theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SearchBar properties overrides defaults, empty theme and overall theme', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(useSearchBarProperties: true, searchBarThemeData: const SearchBarThemeData(), overallTheme: const SearchBarThemeData())); @@ -261,14 +270,14 @@ void main() { checkSearchBar(tester); }); - testWidgets('SearchBar theme overrides defaults and overall theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SearchBar theme overrides defaults and overall theme', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(searchBarThemeData: searchBarTheme, overallTheme: const SearchBarThemeData())); await tester.pumpAndSettle(); // allow the animations to finish checkSearchBar(tester); }); - testWidgets('Overall Theme SearchBar theme overrides defaults and null theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme SearchBar theme overrides defaults and null theme', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallTheme: searchBarTheme)); await tester.pumpAndSettle(); // allow the animations to finish checkSearchBar(tester); diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart index be9a1d6a2498e..4e1e07954172d 100644 --- a/packages/flutter/test/material/search_test.dart +++ b/packages/flutter/test/material/search_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/semantics_tester.dart'; @@ -25,8 +26,9 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); }); - testWidgets('Changing query moves cursor to the end of query', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing query moves cursor to the end of query', (WidgetTester tester) async { final _TestSearchDelegate delegate = _TestSearchDelegate(); + addTearDown(() => delegate.dispose()); await tester.pumpWidget(TestHomePage(delegate: delegate)); await tester.tap(find.byTooltip('Search')); @@ -605,6 +607,9 @@ void main() { const Widget flexibleSpace = Text('FlexibleSpace'); TestSemantics buildExpected({ required String routeName }) { + final bool isDesktop = debugDefaultTargetPlatformOverride == TargetPlatform.macOS || + debugDefaultTargetPlatformOverride == TargetPlatform.windows || + debugDefaultTargetPlatformOverride == TargetPlatform.linux; return TestSemantics.root( children: <TestSemantics>[ TestSemantics( @@ -651,9 +656,10 @@ void main() { debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: <SemanticsAction>[ - if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS || - debugDefaultTargetPlatformOverride == TargetPlatform.windows) + if (isDesktop) SemanticsAction.didGainAccessibilityFocus, + if (isDesktop) + SemanticsAction.didLoseAccessibilityFocus, SemanticsAction.tap, SemanticsAction.setSelection, SemanticsAction.setText, @@ -748,6 +754,9 @@ void main() { group('contributes semantics', () { TestSemantics buildExpected({ required String routeName }) { + final bool isDesktop = debugDefaultTargetPlatformOverride == TargetPlatform.macOS || + debugDefaultTargetPlatformOverride == TargetPlatform.windows || + debugDefaultTargetPlatformOverride == TargetPlatform.linux; return TestSemantics.root( children: <TestSemantics>[ TestSemantics( @@ -791,9 +800,10 @@ void main() { debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: <SemanticsAction>[ - if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS || - debugDefaultTargetPlatformOverride == TargetPlatform.windows) + if (isDesktop) SemanticsAction.didGainAccessibilityFocus, + if (isDesktop) + SemanticsAction.didLoseAccessibilityFocus, SemanticsAction.tap, SemanticsAction.setSelection, SemanticsAction.setText, diff --git a/packages/flutter/test/material/search_view_theme_test.dart b/packages/flutter/test/material/search_view_theme_test.dart index b85c61ef57cd1..647012c09f458 100644 --- a/packages/flutter/test/material/search_view_theme_test.dart +++ b/packages/flutter/test/material/search_view_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('SearchViewThemeData copyWith, ==, hashCode basics', () { @@ -44,7 +45,7 @@ void main() { expect(theme.data.dividerColor, null); }); - testWidgets('Default SearchViewThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SearchViewThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SearchViewThemeData().debugFillProperties(builder); @@ -56,7 +57,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('SearchViewThemeData implements debugFillProperties', ( + testWidgetsWithLeakTracking('SearchViewThemeData implements debugFillProperties', ( WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SearchViewThemeData( diff --git a/packages/flutter/test/material/segmented_button_test.dart b/packages/flutter/test/material/segmented_button_test.dart index cb785b3217e85..3f8053b940fa8 100644 --- a/packages/flutter/test/material/segmented_button_test.dart +++ b/packages/flutter/test/material/segmented_button_test.dart @@ -9,8 +9,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; Widget boilerplate({required Widget child}) { @@ -21,8 +21,54 @@ Widget boilerplate({required Widget child}) { } void main() { + testWidgetsWithLeakTracking('SegmentedButton releases state controllers for deleted segments', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + final Key key = UniqueKey(); + + Widget buildApp(Widget button) { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Center( + child: button, + ), + ), + ); + } + + await tester.pumpWidget( + buildApp( + SegmentedButton<int>( + key: key, + segments: const <ButtonSegment<int>>[ + ButtonSegment<int>(value: 1, label: Text('1')), + ButtonSegment<int>(value: 2, label: Text('2')), + ], + selected: const <int>{2}, + ), + ), + ); + + await tester.pumpWidget( + buildApp( + SegmentedButton<int>( + key: key, + segments: const <ButtonSegment<int>>[ + ButtonSegment<int>(value: 2, label: Text('2')), + ButtonSegment<int>(value: 3, label: Text('3')), + ], + selected: const <int>{2}, + ), + ), + ); + + final SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>)); + expect(state.statesControllers, hasLength(2)); + expect(state.statesControllers.keys.first.value, 2); + expect(state.statesControllers.keys.last.value, 3); + }); - testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( @@ -51,7 +97,7 @@ void main() { expect(material.type, MaterialType.transparency); }); - testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async { int callbackCount = 0; int selectedSegment = 2; @@ -101,7 +147,7 @@ void main() { expect(selectedSegment, 3); }); - testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton supports multiple selected segments', (WidgetTester tester) async { int callbackCount = 0; Set<int> selection = <int>{1}; @@ -156,7 +202,7 @@ void main() { expect(selection, <int>{2, 3}); }); -testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async { +testWidgetsWithLeakTracking('SegmentedButton allows for empty selection', (WidgetTester tester) async { int callbackCount = 0; int? selectedSegment = 1; @@ -209,7 +255,7 @@ testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) expect(selectedSegment, 3); }); -testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTester tester) async { +testWidgetsWithLeakTracking('SegmentedButton shows checkboxes for selected segments', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( @@ -246,7 +292,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes expect(find.byIcon(Icons.check), findsOneWidget); }); - testWidgets('SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( @@ -289,7 +335,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes expect(find.byIcon(Icons.add_alarm), findsNothing); }); - testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton shows selected checkboxes next to icon if there is no label', (WidgetTester tester) async { Widget frameWithSelection(int selected) { return Material( child: boilerplate( @@ -330,7 +376,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes }); - testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButtons have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -409,7 +455,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes }); - testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -486,7 +532,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes semantics.dispose(); }); - testWidgets('SegmentedButton default overlayColor and foregroundColor resolve pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton default overlayColor and foregroundColor resolve pressed state', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -534,7 +580,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes expect(material.textStyle?.color, theme.colorScheme.onSurface); }); - testWidgets('SegmentedButton has no tooltips by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton has no tooltips by default', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( @@ -558,7 +604,7 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes expect(find.byType(Tooltip), findsNothing); }); - testWidgets('SegmentedButton has correct tooltips', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButton has correct tooltips', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/segmented_button_theme_test.dart b/packages/flutter/test/material/segmented_button_theme_test.dart index f978b4bb4a768..5efee131502ef 100644 --- a/packages/flutter/test/material/segmented_button_theme_test.dart +++ b/packages/flutter/test/material/segmented_button_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { @@ -29,7 +30,7 @@ void main() { expect(identical(SegmentedButtonThemeData.lerp(theme, theme, 0.5), theme), true); }); - testWidgets('Default SegmentedButtonThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SegmentedButtonThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SegmentedButtonThemeData().debugFillProperties(builder); @@ -41,7 +42,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('With no other configuration, defaults are used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('With no other configuration, defaults are used', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( @@ -108,7 +109,7 @@ void main() { } }); - testWidgets('ThemeData.segmentedButtonTheme overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.segmentedButtonTheme overrides defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: true, segmentedButtonTheme: SegmentedButtonThemeData( @@ -201,7 +202,7 @@ void main() { } }); - testWidgets('SegmentedButtonTheme overrides ThemeData and defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SegmentedButtonTheme overrides ThemeData and defaults', (WidgetTester tester) async { final SegmentedButtonThemeData global = SegmentedButtonThemeData( style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { @@ -328,7 +329,7 @@ void main() { } }); - testWidgets('Widget parameters overrides SegmentedTheme, ThemeData and defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget parameters overrides SegmentedTheme, ThemeData and defaults', (WidgetTester tester) async { final SegmentedButtonThemeData global = SegmentedButtonThemeData( style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { diff --git a/packages/flutter/test/material/selection_area_test.dart b/packages/flutter/test/material/selection_area_test.dart index b9fb37c582d6e..521a3ff66eca0 100644 --- a/packages/flutter/test/material/selection_area_test.dart +++ b/packages/flutter/test/material/selection_area_test.dart @@ -8,6 +8,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + Offset textOffsetToPosition(RenderParagraph paragraph, int offset) { const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0); @@ -16,7 +18,7 @@ Offset textOffsetToPosition(RenderParagraph paragraph, int offset) { } void main() { - testWidgets('SelectionArea uses correct selection controls', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectionArea uses correct selection controls', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: SelectionArea( child: Text('abc'), @@ -38,7 +40,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('Does not crash when long pressing on padding after dragging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not crash when long pressing on padding after dragging', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/123378 await tester.pumpWidget( const MaterialApp( @@ -68,12 +70,72 @@ void main() { expect(tester.takeException(), isNull); }); + // Regression test for https://github.com/flutter/flutter/issues/111370 + testWidgetsWithLeakTracking('Handle is correctly transformed when the text is inside of a FittedBox ',(WidgetTester tester) async { + final Key textKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + color: const Color(0xFF2196F3), + home: Scaffold( + body: SelectionArea( + child: SizedBox( + height: 100, + child: FittedBox( + fit: BoxFit.fill, + child: Text('test', key: textKey), + ), + ), + ), + ), + ), + ); + + final TestGesture longpress = await tester.startGesture(const Offset(10, 10)); + addTearDown(longpress.removePointer); + await tester.pump(const Duration(milliseconds: 500)); + await longpress.up(); + + // Text box is scaled by 5. + final RenderBox textBox = tester.firstRenderObject(find.byKey(textKey)); + expect(textBox.size.height, 20.0); + final Offset textPoint = textBox.localToGlobal(const Offset(0, 20)); + expect(textPoint, equals(const Offset(0, 100))); + + // Find handles and verify their sizes. + expect(find.byType(Overlay), findsOneWidget); + expect(find.descendant(of: find.byType(Overlay),matching: find.byType(CustomPaint),),findsNWidgets(2)); + final Iterable<RenderBox> handles = tester.renderObjectList(find.descendant( + of: find.byType(Overlay), + matching: find.byType(CustomPaint), + )); + + // The handle height is determined by the formula: + // textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap . + // The text line height will be the value of the fontSize. + // The constant _kSelectionHandleRadius has the value of 6. + // The constant _kSelectionHandleOverlap has the value of 1.5. + // The handle height before scaling is 20.0 + 6 * 2 - 1.5 = 30.5. + + final double handleHeightBeforeScaling = handles.first.size.height; + expect(handleHeightBeforeScaling, 30.5); + + final Offset handleHeightAfterScaling = handles.first.localToGlobal(const Offset(0, 30.5)) - handles.first.localToGlobal(Offset.zero); + + // The handle height after scaling is 30.5 * 5 = 152.5 + expect(handleHeightAfterScaling, equals(const Offset(0.0, 152.5))); + }, + skip: isBrowser, // [intended] + variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), + ); + + testWidgetsWithLeakTracking('builds the default context menu by default', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); - testWidgets('builds the default context menu by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SelectionArea( - focusNode: FocusNode(), + focusNode: focusNode, child: const Text('How are you?'), ), ), @@ -89,6 +151,8 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); // `are` is selected. expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); + + await gesture.up(); await tester.pumpAndSettle(); expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); @@ -96,12 +160,15 @@ void main() { skip: kIsWeb, // [intended] ); - testWidgets('builds a custom context menu if provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('builds a custom context menu if provided', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectionArea( - focusNode: FocusNode(), + focusNode: focusNode, contextMenuBuilder: ( BuildContext context, SelectableRegionState selectableRegionState, @@ -124,6 +191,8 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); // `are` is selected. expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); + + await gesture.up(); await tester.pumpAndSettle(); expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); @@ -132,7 +201,7 @@ void main() { skip: kIsWeb, // [intended] ); - testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSelectionChange is called when the selection changes', (WidgetTester tester) async { SelectedContent? content; await tester.pumpWidget(MaterialApp( @@ -155,7 +224,14 @@ void main() { // Backwards selection. await gesture.down(textOffsetToPosition(paragraph, 3)); - expect(content, isNull); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(content, isNotNull); + expect(content!.plainText, ''); + + await gesture.down(textOffsetToPosition(paragraph, 3)); + await tester.pump(); await gesture.moveTo(textOffsetToPosition(paragraph, 0)); await gesture.up(); await tester.pump(); @@ -163,7 +239,10 @@ void main() { expect(content!.plainText, 'How'); }); - testWidgets('stopping drag of end handle will show the toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('stopping drag of end handle will show the toolbar', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + // Regression test for https://github.com/flutter/flutter/issues/119314 await tester.pumpWidget( MaterialApp( @@ -175,7 +254,7 @@ void main() { children: <Widget>[ const Text('How are you?'), SelectionArea( - focusNode: FocusNode(), + focusNode: focusNode, child: const Text('Good, and you?'), ), const Text('Fine, thank you.'), @@ -193,6 +272,7 @@ void main() { await gesture.up(); final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]); expect(boxes.length, 1); + await tester.pumpAndSettle(); // There is a selection now. // We check the presence of the copy button to make sure the selection toolbar // is showing. diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index a8bf69325ccea..cdc043fa31514 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -13,8 +13,8 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/physics/utils.dart' show nearEqual; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; // A thumb shape that also logs its repaint center. @@ -101,7 +101,7 @@ class _StateDependentMouseCursor extends MaterialStateMouseCursor { } void main() { - testWidgets('The initial value should respect the discrete value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The initial value should respect the discrete value', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.20; final List<Offset> log = <Offset>[]; @@ -141,7 +141,7 @@ void main() { expect(log[0], const Offset(212.0, 300.0)); }); - testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can move when tapped (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; double? startValue; @@ -200,7 +200,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider can move when tapped (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can move when tapped (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; @@ -245,7 +245,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; late double startValue; @@ -302,7 +302,7 @@ void main() { expect(endValueUpdates, equals(2)); }); - testWidgets('Value indicator shows for a bit after being tapped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Value indicator shows for a bit after being tapped', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; @@ -352,7 +352,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; final List<Offset> log = <Offset>[]; @@ -420,7 +420,7 @@ void main() { await gesture.up(); }); - testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; int updates = 0; @@ -461,7 +461,7 @@ void main() { expect(updates, equals(1)); }); - testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('discrete Slider repaints when dragged', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; final List<Offset> log = <Offset>[]; @@ -529,7 +529,7 @@ void main() { await gesture.up(); }); - testWidgets('Slider take on discrete values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider take on discrete values', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; @@ -581,7 +581,7 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); - testWidgets('Slider can be given zero values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be given zero values', (WidgetTester tester) async { final List<double> log = <double>[]; await tester.pumpWidget( MaterialApp( @@ -625,7 +625,7 @@ void main() { log.clear(); }); - testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can tap in vertical scroller', (WidgetTester tester) async { double value = 0.0; await tester.pumpWidget( MaterialApp( @@ -654,7 +654,7 @@ void main() { expect(value, equals(0.5)); }); - testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider drags immediately (LTR)', (WidgetTester tester) async { double value = 0.0; await tester.pumpWidget( MaterialApp( @@ -686,7 +686,7 @@ void main() { await gesture.up(); }); - testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider drags immediately (RTL)', (WidgetTester tester) async { double value = 0.0; await tester.pumpWidget( MaterialApp( @@ -718,7 +718,7 @@ void main() { await gesture.up(); }); - testWidgets('Slider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28115 int startFired = 0; @@ -758,7 +758,7 @@ void main() { expect(endFired, equals(1)); }); - testWidgets('Slider sizing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider sizing', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Directionality( @@ -817,7 +817,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0)); }); - testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider respects textScaleFactor', (WidgetTester tester) async { debugDisableShadows = false; try { final Key sliderKey = UniqueKey(); @@ -972,7 +972,7 @@ void main() { } }); - testWidgets('Tick marks are skipped when they are too dense', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tick marks are skipped when they are too dense', (WidgetTester tester) async { Widget buildSlider({ required int divisions, }) { @@ -1019,7 +1019,7 @@ void main() { expect(material, paintsExactlyCountTimes(#drawCircle, 1)); }); - testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider has correct animations when reparented', (WidgetTester tester) async { final Key sliderKey = GlobalKey(debugLabel: 'A'); double value = 0.0; @@ -1148,7 +1148,7 @@ void main() { await testReparenting(true); }); - testWidgets('Slider Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -1310,7 +1310,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux })); - testWidgets('Slider Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1421,7 +1421,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Slider Semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -1590,7 +1590,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })); - testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider semantics with custom formatter', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -1649,7 +1649,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/101868 - testWidgets('Slider.label info should not write to semantic node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider.label info should not write to semantic node', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -1708,8 +1708,9 @@ void main() { semantics.dispose(); }); - testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(useMaterial3: true); double value = 0.5; @@ -1756,8 +1757,9 @@ void main() { ); }); - testWidgets('Slider has correct focus color from overlayColor property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider has correct focus color from overlayColor property', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; Widget buildApp({bool enabled = true}) { @@ -1809,7 +1811,7 @@ void main() { ); }); - testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(useMaterial3: true); double value = 0.5; @@ -1879,7 +1881,7 @@ void main() { ); }); - testWidgets('Slider has correct hovered color from overlayColor property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider has correct hovered color from overlayColor property', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; Widget buildApp({bool enabled = true}) { @@ -1940,12 +1942,13 @@ void main() { ); }); - testWidgets('Slider is draggable and has correct dragged color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider is draggable and has correct dragged color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; final ThemeData theme = ThemeData(useMaterial3: true); final Key sliderKey = UniqueKey(); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -2014,11 +2017,12 @@ void main() { ); }); - testWidgets('Slider has correct dragged color from overlayColor property', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider has correct dragged color from overlayColor property', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; final Key sliderKey = UniqueKey(); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -2083,8 +2087,9 @@ void main() { ); }); - testWidgets('OverlayColor property is correctly applied when activeColor is also provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverlayColor property is correctly applied when activeColor is also provided', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; const Color activeColor = Color(0xffff0000); @@ -2140,7 +2145,7 @@ void main() { ); }); - testWidgets('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double startValue = 0.0; double currentValue = 0.5; @@ -2201,7 +2206,7 @@ void main() { expect(endValue, 0.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double startValue = 0.0; double currentValue = 0.5; @@ -2262,7 +2267,7 @@ void main() { expect(endValue, 0.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double startValue = 0.0; double currentValue = 0.5; @@ -2326,7 +2331,7 @@ void main() { expect(endValue, 0.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double startValue = 0.0; double currentValue = 0.5; @@ -2390,7 +2395,7 @@ void main() { expect(endValue, 0.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('In directional nav, Slider can be navigated out of by using up and down arrows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('In directional nav, Slider can be navigated out of by using up and down arrows', (WidgetTester tester) async { const Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left), SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right), @@ -2486,10 +2491,11 @@ void main() { expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight'); }); - testWidgets('Slider gains keyboard focus when it gains semantics focus on Windows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider gains keyboard focus when it gains semantics focus on Windows', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -2553,7 +2559,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })); - testWidgets('Value indicator appears when it should', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Value indicator appears when it should', (WidgetTester tester) async { final ThemeData baseTheme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -2637,7 +2643,7 @@ void main() { await expectValueIndicator(isVisible: false, theme: theme, enabled: false); }); - testWidgets("Slider doesn't start any animations after dispose", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Slider doesn't start any animations after dispose", (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; await tester.pumpWidget( @@ -2677,7 +2683,7 @@ void main() { await gesture.up(); }); - testWidgets('Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); const Color fillColor = Color(0xf55f5f5f); double value = 0.0; @@ -2774,7 +2780,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Slider.adaptive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider.adaptive', (WidgetTester tester) async { double value = 0.5; Widget buildFrame(TargetPlatform platform) { @@ -2829,7 +2835,7 @@ void main() { } }); - testWidgets('Slider respects height from theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider respects height from theme', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; await tester.pumpWidget( @@ -2868,7 +2874,7 @@ void main() { expect(renderObject.size.height, 200); }); - testWidgets('Slider changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider changes mouse cursor when hovered', (WidgetTester tester) async { // Test Slider() constructor await tester.pumpWidget( MaterialApp( @@ -2943,7 +2949,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('Slider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async { const MouseCursor disabledCursor = SystemMouseCursors.basic; const MouseCursor hoveredCursor = SystemMouseCursors.grab; const MouseCursor draggedCursor = SystemMouseCursors.move; @@ -2993,7 +2999,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move); }); - testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Slider( @@ -3026,7 +3032,7 @@ void main() { ]); }); - testWidgets('Slider track paints correctly when the shape is rectangular', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider track paints correctly when the shape is rectangular', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -3061,7 +3067,7 @@ void main() { ); }); - testWidgets('SliderTheme change should trigger re-layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderTheme change should trigger re-layout', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/118955 double sliderValue = 0.0; Widget buildFrame(ThemeMode themeMode) { @@ -3106,7 +3112,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Slider can be painted in a narrower constraint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be painted in a narrower constraint', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Directionality( @@ -3142,7 +3148,7 @@ void main() { ); }); - testWidgets('Update the divisions and value at the same time for Slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update the divisions and value at the same time for Slider', (WidgetTester tester) async { // Regress test for https://github.com/flutter/flutter/issues/65943 Widget buildFrame(double maxValue) { return MaterialApp( @@ -3183,7 +3189,7 @@ void main() { expect(nearEqual(activeTrackRRect.right, (800.0 - 24.0 - 24.0) * (5 / 15) + 24.0, 0.01), true); }); - testWidgets('Slider paints thumbColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider paints thumbColor', (WidgetTester tester) async { const Color color = Color(0xffffc107); final Widget sliderAdaptive = MaterialApp( @@ -3205,7 +3211,7 @@ void main() { expect(material, paints..circle(color: color)); }); - testWidgets('Slider.adaptive paints thumbColor on Android', + testWidgetsWithLeakTracking('Slider.adaptive paints thumbColor on Android', (WidgetTester tester) async { const Color color = Color(0xffffc107); @@ -3228,7 +3234,7 @@ void main() { expect(material, paints..circle(color: color)); }); - testWidgets('If thumbColor is null, it defaults to CupertinoColors.white', + testWidgetsWithLeakTracking('If thumbColor is null, it defaults to CupertinoColors.white', (WidgetTester tester) async { final Widget sliderAdaptive = MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), @@ -3257,7 +3263,7 @@ void main() { ); }); - testWidgets('Slider.adaptive passes thumbColor to CupertinoSlider', + testWidgetsWithLeakTracking('Slider.adaptive passes thumbColor to CupertinoSlider', (WidgetTester tester) async { const Color color = Color(0xffffc107); @@ -3284,7 +3290,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/103566 - testWidgets('Drag gesture uses provided gesture settings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag gesture uses provided gesture settings', (WidgetTester tester) async { double value = 0.5; bool dragStarted = false; final Key sliderKey = UniqueKey(); @@ -3390,7 +3396,7 @@ void main() { expect(dragStarted, false); }); - testWidgets('Overlay appear only when hovered on the thumb on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overlay appear only when hovered on the thumb on desktop', (WidgetTester tester) async { double value = 0.5; const Color overlayColor = Color(0xffff0000); @@ -3453,10 +3459,11 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Overlay remains when Slider is in focus on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overlay remains when Slider is in focus on desktop', (WidgetTester tester) async { double value = 0.5; const Color overlayColor = Color(0xffff0000); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -3518,7 +3525,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Value indicator disappears after adjusting the slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Value indicator disappears after adjusting the slider', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/123313. final ThemeData theme = ThemeData(useMaterial3: true); const double currentValue = 0.5; @@ -3568,9 +3575,10 @@ void main() { ); }); - testWidgets('Value indicator remains when Slider is in focus on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Value indicator remains when Slider is in focus on desktop', (WidgetTester tester) async { double value = 0.5; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -3637,7 +3645,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Event on Slider should perform no-op if already unmounted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Event on Slider should perform no-op if already unmounted', (WidgetTester tester) async { // Test covering crashing found in Google internal issue b/192329942. double value = 0.0; final ValueNotifier<bool> shouldShowSliderListenable = @@ -3654,16 +3662,22 @@ void main() { child: ValueListenableBuilder<bool>( valueListenable: shouldShowSliderListenable, builder: (BuildContext context, bool shouldShowSlider, _) { - return shouldShowSlider - ? Slider( - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, - ) - : const SizedBox.shrink(); + return GestureDetector( + behavior: HitTestBehavior.translucent, + // Note: it is important that `onTap` is non-null so + // [GestureDetector] will register tap events. + onTap: () {}, + child: shouldShowSlider + ? Slider( + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ) + : const SizedBox.expand(), + ); }, ), ), @@ -3674,20 +3688,19 @@ void main() { ), ); + // Move Slider. final TestGesture gesture = await tester - .startGesture(tester.getRect(find.byType(Slider)).centerLeft); + .startGesture(tester.getRect(find.byType(Slider)).center); + await gesture.moveBy(const Offset(1.0, 0.0)); + await tester.pumpAndSettle(); - // Intentionally not calling `await tester.pumpAndSettle()` to allow drag - // event performed on `Slider` before it is about to get unmounted. + // Hide Slider. Slider will dispose and unmount. shouldShowSliderListenable.value = false; - - await tester.drag(find.byType(Slider), const Offset(1.0, 0.0)); await tester.pumpAndSettle(); - expect(value, equals(0.0)); - - // This is supposed to trigger animation on `Slider` if it is mounted. - await gesture.up(); + // Move Slider after unmounted. + await gesture.moveBy(const Offset(1.0, 0.0)); + await tester.pumpAndSettle(); expect(tester.takeException(), null); }); @@ -3697,7 +3710,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(useMaterial3: false); double value = 0.5; @@ -3753,8 +3766,9 @@ void main() { ); }); - testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final ThemeData theme = ThemeData(); double value = 0.5; @@ -3800,12 +3814,13 @@ void main() { ); }); - testWidgets('Slider is draggable and has correct dragged color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider is draggable and has correct dragged color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; final ThemeData theme = ThemeData(); final Key sliderKey = UniqueKey(); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( @@ -3865,7 +3880,7 @@ void main() { }); group('Slider.allowedInteraction', () { - testWidgets('SliderInteraction.tapOnly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderInteraction.tapOnly', (WidgetTester tester) async { double value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) @@ -3907,7 +3922,7 @@ void main() { expect(value, 0.5); }); - testWidgets('SliderInteraction.tapAndSlide', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderInteraction.tapAndSlide', (WidgetTester tester) async { double value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) @@ -3953,7 +3968,7 @@ void main() { expect(value, 1.0); }); - testWidgets('SliderInteraction.slideOnly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderInteraction.slideOnly', (WidgetTester tester) async { double value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) @@ -4001,7 +4016,7 @@ void main() { expect(value, 1.0); }); - testWidgets('SliderInteraction.slideThumb', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderInteraction.slideThumb', (WidgetTester tester) async { double value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 4f3a41e1f9fce..180cc2b4802ec 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('SliderThemeData copyWith, ==, hashCode basics', () { @@ -20,7 +19,7 @@ void main() { expect(identical(SliderThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Default SliderThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SliderThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SliderThemeData().debugFillProperties(builder); @@ -32,7 +31,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('SliderThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SliderThemeData( trackHeight: 7.0, @@ -104,7 +103,7 @@ void main() { ]); }); - testWidgets('Slider defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider defaults', (WidgetTester tester) async { debugDisableShadows = false; final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colorScheme = theme.colorScheme; @@ -235,7 +234,7 @@ void main() { } }); - testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider uses the right theme colors for the right components', (WidgetTester tester) async { debugDisableShadows = false; try { const Color customColor1 = Color(0xcafefeed); @@ -502,7 +501,7 @@ void main() { } }); - testWidgets('Slider parameters overrides theme properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider parameters overrides theme properties', (WidgetTester tester) async { debugDisableShadows = false; const Color activeTrackColor = Color(0xffff0001); const Color inactiveTrackColor = Color(0xffff0002); @@ -556,7 +555,7 @@ void main() { } }); - testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider uses ThemeData slider theme if present', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.red, @@ -580,7 +579,7 @@ void main() { ); }); - testWidgets('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.red, @@ -604,7 +603,7 @@ void main() { ); }); - testWidgets('SliderThemeData generates correct opacities for fromPrimaryColors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderThemeData generates correct opacities for fromPrimaryColors', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); const Color customColor3 = Color(0xdecaface); @@ -634,7 +633,7 @@ void main() { expect(sliderTheme.valueIndicatorTextStyle!.color, equals(customColor4)); }); - testWidgets('SliderThemeData generates correct shapes for fromPrimaryColors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderThemeData generates correct shapes for fromPrimaryColors', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); const Color customColor3 = Color(0xdecaface); @@ -658,7 +657,7 @@ void main() { expect(sliderTheme.rangeValueIndicatorShape, const PaddleRangeSliderValueIndicatorShape()); }); - testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderThemeData lerps correctly', (WidgetTester tester) async { final SliderThemeData sliderThemeBlack = SliderThemeData.fromPrimaryColors( primaryColor: Colors.black, primaryColorDark: Colors.black, @@ -692,7 +691,7 @@ void main() { expect(lerp.valueIndicatorTextStyle!.color, equals(middleGrey.withAlpha(0xff))); }); - testWidgets('Default slider track draws correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default slider track draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -728,7 +727,7 @@ void main() { ); }); - testWidgets('Default slider overlay draws correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default slider overlay draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -789,7 +788,7 @@ void main() { ); }); - testWidgets('Slider can use theme overlay with material states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider can use theme overlay with material states', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -804,6 +803,7 @@ void main() { }), ); final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); + addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; double value = 0.5; @@ -848,7 +848,7 @@ void main() { ); }); - testWidgets('Default slider ticker and thumb shape draw correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default slider ticker and thumb shape draw correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -892,7 +892,7 @@ void main() { ); }); - testWidgets('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { debugDisableShadows = false; try { final ThemeData theme = ThemeData( @@ -1076,7 +1076,7 @@ void main() { } }); - testWidgets('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { debugDisableShadows = false; try { final ThemeData theme = ThemeData( @@ -1260,7 +1260,7 @@ void main() { } }); - testWidgets('The slider track height can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider track height can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(trackHeight: 16); const Radius radius = Radius.circular(8); const Radius activatedRadius = Radius.circular(9); @@ -1290,7 +1290,7 @@ void main() { ); }); - testWidgets('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 7, @@ -1315,7 +1315,7 @@ void main() { ); }); - testWidgets('The default slider thumb shape disabled size can be inferred from the enabled size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The default slider thumb shape disabled size can be inferred from the enabled size', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 9, @@ -1338,7 +1338,7 @@ void main() { ); }); - testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The default slider tick mark shape size can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5), activeTickMarkColor: const Color(0xfadedead), @@ -1371,7 +1371,7 @@ void main() { ); }); - testWidgets('The default slider overlay shape size can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The default slider overlay shape size can be overridden', (WidgetTester tester) async { const double uniqueOverlayRadius = 23; final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( overlayShape: const RoundSliderOverlayShape( @@ -1398,7 +1398,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/74503 - testWidgets('The slider track layout correctly when the overlay size is smaller than the thumb size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider track layout correctly when the overlay size is smaller than the thumb size', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( overlayShape: SliderComponentShape.noOverlay, ); @@ -1439,7 +1439,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/125467 - testWidgets('The RangeSlider track layout correctly when the overlay size is smaller than the thumb size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The RangeSlider track layout correctly when the overlay size is smaller than the thumb size', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( overlayShape: SliderComponentShape.noOverlay, ); @@ -1490,7 +1490,7 @@ void main() { // // The value indicator can be skipped by passing the appropriate // [ShowValueIndicator]. - testWidgets('The slider can skip all of its component painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all of its component painting', (WidgetTester tester) async { // Pump a slider with all shapes skipped. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1511,7 +1511,7 @@ void main() { expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); - testWidgets('The slider can skip all component painting except the track', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all component painting except the track', (WidgetTester tester) async { // Pump a slider with just a track. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1532,7 +1532,7 @@ void main() { expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); - testWidgets('The slider can skip all component painting except the tick marks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all component painting except the tick marks', (WidgetTester tester) async { // Pump a slider with just tick marks. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1556,7 +1556,7 @@ void main() { expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); - testWidgets('The slider can skip all component painting except the thumb', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all component painting except the thumb', (WidgetTester tester) async { debugDisableShadows = false; try { // Pump a slider with just a thumb. @@ -1582,7 +1582,7 @@ void main() { } }); - testWidgets('The slider can skip all component painting except the overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all component painting except the overlay', (WidgetTester tester) async { // Pump a slider with just an overlay. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1610,7 +1610,7 @@ void main() { await gesture.up(); }); - testWidgets('The slider can skip all component painting except the value indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The slider can skip all component painting except the value indicator', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1640,7 +1640,7 @@ void main() { await gesture.up(); }); - testWidgets('PaddleSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaddleSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1675,7 +1675,7 @@ void main() { await gesture.up(); }); - testWidgets('Default slider value indicator shape skips all painting at zero scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default slider value indicator shape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -1707,7 +1707,7 @@ void main() { }); - testWidgets('Default paddle range slider value indicator shape draws correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default paddle range slider value indicator shape draws correctly', (WidgetTester tester) async { debugDisableShadows = false; try { final ThemeData theme = ThemeData( @@ -1757,7 +1757,7 @@ void main() { } }); - testWidgets('Default paddle range slider value indicator shape draws correctly with debugDisableShadows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default paddle range slider value indicator shape draws correctly with debugDisableShadows', (WidgetTester tester) async { debugDisableShadows = true; final ThemeData theme = ThemeData( platform: TargetPlatform.android, @@ -1803,7 +1803,7 @@ void main() { await gesture.up(); }); - testWidgets('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { debugDisableShadows = false; try { // Pump a slider with just a value indicator. @@ -1837,7 +1837,7 @@ void main() { } }); - testWidgets('Default range indicator shape skips all painting at zero scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default range indicator shape skips all painting at zero scale', (WidgetTester tester) async { debugDisableShadows = false; try { // Pump a slider with just a value indicator. @@ -1873,7 +1873,7 @@ void main() { } }); - testWidgets('activeTrackRadius is taken into account when painting the border of the active track', (WidgetTester tester) async { + testWidgetsWithLeakTracking('activeTrackRadius is taken into account when painting the border of the active track', (WidgetTester tester) async { await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackShape: const RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight( @@ -1900,7 +1900,7 @@ void main() { ); }); - testWidgets('The mouse cursor is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The mouse cursor is themeable', (WidgetTester tester) async { await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( mouseCursor: const MaterialStatePropertyAll<MouseCursor>(SystemMouseCursors.text), @@ -1915,7 +1915,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('SliderTheme.allowedInteraction is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliderTheme.allowedInteraction is themeable', (WidgetTester tester) async { double value = 0.0; Widget buildApp({ @@ -2022,7 +2022,7 @@ void main() { await gesture.up(); }); - testWidgets('Default value indicator color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default value indicator color', (WidgetTester tester) async { debugDisableShadows = false; try { final ThemeData theme = ThemeData( @@ -2085,7 +2085,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Slider defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider defaults', (WidgetTester tester) async { debugDisableShadows = false; final ThemeData theme = ThemeData(useMaterial3: false); const double trackHeight = 4.0; @@ -2235,7 +2235,7 @@ void main() { } }); - testWidgets('Default value indicator color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default value indicator color', (WidgetTester tester) async { debugDisableShadows = false; try { final ThemeData theme = ThemeData( diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index a31753412f845..4f32a0790771d 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -7,14 +7,17 @@ @Tags(<String>['reduced-test-set']) library; +import 'dart:async'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SnackBar control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar control test', (WidgetTester tester) async { const String helloSnackBar = 'Hello SnackBar'; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -59,7 +62,7 @@ void main() { expect(find.text(helloSnackBar), findsNothing); }); - testWidgets('SnackBar twice test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar twice test', (WidgetTester tester) async { int snackBarCount = 0; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -134,7 +137,7 @@ void main() { expect(find.text('bar2'), findsNothing); }); - testWidgets('SnackBar cancel test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar cancel test', (WidgetTester tester) async { int snackBarCount = 0; const Key tapTarget = Key('tap-target'); late int time; @@ -220,7 +223,7 @@ void main() { expect(find.text('bar2'), findsNothing); }); - testWidgets('SnackBar dismiss test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar dismiss test', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); late DismissDirection dismissDirection; late double width; @@ -262,7 +265,7 @@ void main() { ); }); - testWidgets('SnackBar cannot be tapped twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar cannot be tapped twice', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -301,7 +304,7 @@ void main() { expect(tapCount, equals(1)); }); - testWidgets('Light theme SnackBar has dark background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Light theme SnackBar has dark background', (WidgetTester tester) async { final ThemeData lightTheme = ThemeData.light(useMaterial3: false); await tester.pumpWidget( MaterialApp( @@ -343,7 +346,46 @@ void main() { expect(renderModel.color, equals(const Color(0xFF333333))); }); - testWidgets('Dark theme SnackBar has light background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Light theme SnackBar has dark background', (WidgetTester tester) async { + final ThemeData lightTheme = ThemeData.light(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: lightTheme, + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + label: 'ACTION', + onPressed: () { }, + ), + ), + ); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); + + final Finder material = find.widgetWithText(Material, 'I am a snack bar.').first; + final RenderPhysicalModel renderModel = tester.renderObject(material); + + expect(renderModel.color, equals(lightTheme.colorScheme.inverseSurface)); + }); + + testWidgetsWithLeakTracking('Dark theme SnackBar has light background', (WidgetTester tester) async { final ThemeData darkTheme = ThemeData.dark(); await tester.pumpWidget( MaterialApp( @@ -382,7 +424,7 @@ void main() { expect(renderModel.color, equals(darkTheme.colorScheme.onSurface)); }); - testWidgets('Dark theme SnackBar has primary text buttons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Dark theme SnackBar has primary text buttons', (WidgetTester tester) async { final ThemeData darkTheme = ThemeData.dark(useMaterial3: false); await tester.pumpWidget( MaterialApp( @@ -421,7 +463,46 @@ void main() { expect(buttonTextStyle.color, equals(darkTheme.colorScheme.primary)); }); - testWidgets('SnackBar should inherit theme data from its ancestor.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Dark theme SnackBar has primary text buttons', (WidgetTester tester) async { + final ThemeData darkTheme = ThemeData.dark(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: darkTheme, + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + label: 'ACTION', + onPressed: () { }, + ), + ), + ); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); + + final TextStyle buttonTextStyle = tester.widget<RichText>( + find.descendant(of: find.text('ACTION'), matching: find.byType(RichText)) + ).text.style!; + expect(buttonTextStyle.color, equals(darkTheme.colorScheme.inversePrimary)); + }); + + testWidgetsWithLeakTracking('SnackBar should inherit theme data from its ancestor.', (WidgetTester tester) async { final SliderThemeData sliderTheme = SliderThemeData.fromPrimaryColors( primaryColor: Colors.black, primaryColorDark: Colors.black, @@ -555,7 +636,7 @@ void main() { expect(comparedTheme, themeAfterSnackBar); }); - testWidgets('Snackbar margin can be customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar margin can be customized', (WidgetTester tester) async { const double padding = 20.0; await tester.pumpWidget( MaterialApp( @@ -595,7 +676,7 @@ void main() { expect(snackBarBottomRight.dx, 800 - padding); // Device width is 800. }); - testWidgets('SnackbarBehavior.floating is positioned within safe area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackbarBehavior.floating is positioned within safe area', (WidgetTester tester) async { const double viewPadding = 50.0; const double floatingSnackBarDefaultBottomMargin = 10.0; await tester.pumpWidget( @@ -642,7 +723,7 @@ void main() { ); }); - testWidgets('Snackbar padding can be customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar padding can be customized', (WidgetTester tester) async { const double padding = 20.0; await tester.pumpWidget( MaterialApp( @@ -685,7 +766,7 @@ void main() { expect(textTopRight.dy - snackBarTopRight.dy, padding); }); - testWidgets('Snackbar width can be customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar width can be customized', (WidgetTester tester) async { const double width = 200.0; await tester.pumpWidget( MaterialApp( @@ -724,7 +805,7 @@ void main() { expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800. }); - testWidgets('Snackbar width can be customized from ThemeData', + testWidgetsWithLeakTracking('Snackbar width can be customized from ThemeData', (WidgetTester tester) async { const double width = 200.0; await tester.pumpWidget( @@ -766,9 +847,7 @@ void main() { expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800. }); - testWidgets( - 'Snackbar width customization takes preference of widget over theme', - (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar width customization takes preference of widget over theme', (WidgetTester tester) async { const double themeWidth = 200.0; const double widgetWidth = 400.0; await tester.pumpWidget( @@ -811,9 +890,10 @@ void main() { expect(snackBarBottomRight.dx, (800 + widgetWidth) / 2); // Device width is 800. }); - testWidgets('Snackbar labels can be colored as MaterialColor (Material 2)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Snackbar labels can be colored as MaterialColor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: false), home: Scaffold( body: Builder( builder: (BuildContext context) { @@ -855,8 +935,7 @@ void main() { } }); - testWidgets('Snackbar labels can be colored as MaterialColor (Material 3)', - (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Snackbar labels can be colored as MaterialColor', (WidgetTester tester) async { const MaterialColor usedColor = Colors.teal; await tester.pumpWidget( @@ -907,7 +986,7 @@ void main() { } }); - testWidgets('Snackbar labels can be colored as MaterialStateColor (Material 3)', + testWidgetsWithLeakTracking('Snackbar labels can be colored as MaterialStateColor (Material 3)', (WidgetTester tester) async { const _TestMaterialStateColor usedColor = _TestMaterialStateColor(); @@ -959,7 +1038,7 @@ void main() { } }); - testWidgets('SnackBar button text alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SnackBar button text alignment', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: MediaQuery( @@ -1007,8 +1086,56 @@ void main() { expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 17.0 + 40.0); // margin + bottom padding }); + testWidgetsWithLeakTracking('Material3 - SnackBar button text alignment', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + left: 10.0, + top: 20.0, + right: 30.0, + bottom: 40.0, + ), + ), + child: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () { }), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + )); + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); // Animation last frame. + + final Offset textBottomLeft = tester.getBottomLeft(find.text('I am a snack bar.')); + final Offset textBottomRight = tester.getBottomRight(find.text('I am a snack bar.')); + final Offset actionTextBottomLeft = tester.getBottomLeft(find.text('ACTION')); + final Offset actionTextBottomRight = tester.getBottomRight(find.text('ACTION')); + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + final Offset snackBarBottomRight = tester.getBottomRight(find.byType(SnackBar)); + + expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 14.0 + 40.0); // margin + bottom padding + expect(actionTextBottomLeft.dx - textBottomRight.dx, 24.0 + 12.0); // action padding + margin + expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 12.0 + 30.0); // action (padding + margin) + right padding + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 14.0 + 40.0); // margin + bottom padding + }); + testWidgets( - 'Custom padding between SnackBar and its contents when set to SnackBarBehavior.fixed', + 'Material2 - Custom padding between SnackBar and its contents when set to SnackBarBehavior.fixed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1064,7 +1191,64 @@ void main() { }, ); - testWidgets('SnackBar should push FloatingActionButton above', (WidgetTester tester) async { + testWidgets( + 'Material3 - Custom padding between SnackBar and its contents when set to SnackBarBehavior.fixed', + (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + left: 10.0, + top: 20.0, + right: 30.0, + bottom: 40.0, + ), + ), + child: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const <BottomNavigationBarItem>[ + BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Animutation'), + BottomNavigationBarItem(icon: Icon(Icons.block), label: 'Zombo.com'), + ], + ), + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + )); + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); // Animation last frame. + + final Offset textBottomLeft = tester.getBottomLeft(find.text('I am a snack bar.')); + final Offset textBottomRight = tester.getBottomRight(find.text('I am a snack bar.')); + final Offset actionTextBottomLeft = tester.getBottomLeft(find.text('ACTION')); + final Offset actionTextBottomRight = tester.getBottomRight(find.text('ACTION')); + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + final Offset snackBarBottomRight = tester.getBottomRight(find.byType(SnackBar)); + + expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 14.0); // margin (with no bottom padding) + expect(actionTextBottomLeft.dx - textBottomRight.dx, 24.0 + 12.0); // action padding + margin + expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 12.0 + 30.0); // action (padding + margin) + right padding + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 14.0); // margin (with no bottom padding) + }, + ); + + testWidgetsWithLeakTracking('SnackBar should push FloatingActionButton above', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: MediaQuery( data: const MediaQueryData( @@ -1119,7 +1303,7 @@ void main() { expect(fabRect.bottomRight.dy, snackBarTopRight.dy - defaultFabPadding); }); - testWidgets('Floating SnackBar button text alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Floating SnackBar button text alignment', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( useMaterial3: false, @@ -1170,8 +1354,59 @@ void main() { expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 27.0); // margin (with no bottom padding) }); + testWidgetsWithLeakTracking('Material3 - Floating SnackBar button text alignment', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + useMaterial3: true, + snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating), + ), + home: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + left: 10.0, + top: 20.0, + right: 30.0, + bottom: 40.0, + ), + ), + child: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + )); + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); // Animation last frame. + + final Offset textBottomLeft = tester.getBottomLeft(find.text('I am a snack bar.')); + final Offset textBottomRight = tester.getBottomRight(find.text('I am a snack bar.')); + final Offset actionTextBottomLeft = tester.getBottomLeft(find.text('ACTION')); + final Offset actionTextBottomRight = tester.getBottomRight(find.text('ACTION')); + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + final Offset snackBarBottomRight = tester.getBottomRight(find.byType(SnackBar)); + + expect(textBottomLeft.dx - snackBarBottomLeft.dx, 31.0 + 10.0); // margin + left padding + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 24.0); // margin (with no bottom padding) + expect(actionTextBottomLeft.dx - textBottomRight.dx, 16.0 + 8.0); // action padding + margin + expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 31.0 + 30.0 + 8.0); // margin + right (padding + margin) + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 24.0); // margin (with no bottom padding) + }); + testWidgets( - 'Custom padding between SnackBar and its contents when set to SnackBarBehavior.floating', + 'Material2 - Custom padding between SnackBar and its contents when set to SnackBarBehavior.floating', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -1230,40 +1465,100 @@ void main() { }, ); - testWidgets('SnackBarClosedReason', (WidgetTester tester) async { - final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); - bool actionPressed = false; - SnackBarClosedReason? closedReason; - - await tester.pumpWidget(MaterialApp( - scaffoldMessengerKey: scaffoldMessengerKey, - home: Scaffold( - body: Builder( - builder: (BuildContext context) { - return GestureDetector( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: const Text('snack'), - duration: const Duration(seconds: 2), - action: SnackBarAction( - label: 'ACTION', - onPressed: () { - actionPressed = true; - }, - ), - )).closed.then<void>((SnackBarClosedReason reason) { - closedReason = reason; - }); - }, - child: const Text('X'), - ); - }, + testWidgets( + 'Material3 - Custom padding between SnackBar and its contents when set to SnackBarBehavior.floating', + (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + useMaterial3: true, + snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating), ), - ), - )); - - // Pop up the snack bar and then press its action button. - await tester.tap(find.text('X')); + home: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + left: 10.0, + top: 20.0, + right: 30.0, + bottom: 40.0, + ), + ), + child: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const <BottomNavigationBarItem>[ + BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Animutation'), + BottomNavigationBarItem(icon: Icon(Icons.block), label: 'Zombo.com'), + ], + ), + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + ), + )); + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); // Animation last frame. + + final Offset textBottomLeft = tester.getBottomLeft(find.text('I am a snack bar.')); + final Offset textBottomRight = tester.getBottomRight(find.text('I am a snack bar.')); + final Offset actionTextBottomLeft = tester.getBottomLeft(find.text('ACTION')); + final Offset actionTextBottomRight = tester.getBottomRight(find.text('ACTION')); + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + final Offset snackBarBottomRight = tester.getBottomRight(find.byType(SnackBar)); + + expect(textBottomLeft.dx - snackBarBottomLeft.dx, 31.0 + 10.0); // margin + left padding + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 24.0); // margin (with no bottom padding) + expect(actionTextBottomLeft.dx - textBottomRight.dx, 16.0 + 8.0); // action (margin + padding) + expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 31.0 + 30.0 + 8.0); // margin + right (padding + margin) + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 24.0); // margin (with no bottom padding) + }, + ); + + testWidgetsWithLeakTracking('SnackBarClosedReason', (WidgetTester tester) async { + final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); + bool actionPressed = false; + SnackBarClosedReason? closedReason; + + await tester.pumpWidget(MaterialApp( + scaffoldMessengerKey: scaffoldMessengerKey, + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('snack'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + label: 'ACTION', + onPressed: () { + actionPressed = true; + }, + ), + )).closed.then<void>((SnackBarClosedReason reason) { + closedReason = reason; + }); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + // Pop up the snack bar and then press its action button. + await tester.tap(find.text('X')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); expect(actionPressed, isFalse); @@ -1308,7 +1603,7 @@ void main() { expect(closedReason, equals(SnackBarClosedReason.timeout)); }); - testWidgets('accessible navigation behavior with action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('accessible navigation behavior with action', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget(MaterialApp( @@ -1351,7 +1646,7 @@ void main() { expect(find.text('ACTION'), findsNothing); }); - testWidgets('contributes dismiss semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contributes dismiss semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -1394,7 +1689,7 @@ void main() { handle.dispose(); }); - testWidgets('SnackBar default display duration test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar default display duration test', (WidgetTester tester) async { const String helloSnackBar = 'Hello SnackBar'; const Key tapTarget = Key('tap-target'); await tester.pumpWidget(MaterialApp( @@ -1442,7 +1737,7 @@ void main() { expect(find.text(helloSnackBar), findsNothing); }); - testWidgets('SnackBar handles updates to accessibleNavigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar handles updates to accessibleNavigation', (WidgetTester tester) async { Future<void> boilerplate({ required bool accessibleNavigation }) { return tester.pumpWidget(MaterialApp( home: MediaQuery( @@ -1490,7 +1785,7 @@ void main() { expect(find.text('test'), findsNothing); }); - testWidgets('Snackbar calls onVisible once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar calls onVisible once', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); int called = 0; await tester.pumpWidget(MaterialApp( @@ -1527,7 +1822,7 @@ void main() { expect(called, 1); }); - testWidgets('Snackbar does not call onVisible when it is queued', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar does not call onVisible when it is queued', (WidgetTester tester) async { const Key tapTarget = Key('tap-target'); int called = 0; await tester.pumpWidget(MaterialApp( @@ -1777,6 +2072,127 @@ void main() { }, ); + testWidgets( + '${SnackBarBehavior.floating} should not align SnackBar with the top of FloatingActionButton ' + 'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is set to a top position', + (WidgetTester tester) async { + Future<void> pumpApp({required FloatingActionButtonLocation fabLocation}) async { + return tester.pumpWidget(MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.send), + onPressed: () {}, + ), + floatingActionButtonLocation: fabLocation, + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + behavior: SnackBarBehavior.floating, + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + } + + const List<FloatingActionButtonLocation> topLocations = <FloatingActionButtonLocation>[ + FloatingActionButtonLocation.startTop, + FloatingActionButtonLocation.centerTop, + FloatingActionButtonLocation.endTop, + FloatingActionButtonLocation.miniStartTop, + FloatingActionButtonLocation.miniCenterTop, + FloatingActionButtonLocation.miniEndTop, + ]; + + for (final FloatingActionButtonLocation location in topLocations) { + await pumpApp(fabLocation: location); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + + expect(snackBarBottomLeft.dy, 600); // Device height is 600. + } + }, + ); + + testWidgets( + '${SnackBarBehavior.floating} should align SnackBar with the top of FloatingActionButton ' + 'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is not set to a top position', + (WidgetTester tester) async { + Future<void> pumpApp({required FloatingActionButtonLocation fabLocation}) async { + return tester.pumpWidget(MaterialApp( + home: Scaffold( + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.send), + onPressed: () {}, + ), + floatingActionButtonLocation: fabLocation, + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + behavior: SnackBarBehavior.floating, + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + } + + const List<FloatingActionButtonLocation> nonTopLocations = <FloatingActionButtonLocation>[ + FloatingActionButtonLocation.startDocked, + FloatingActionButtonLocation.startFloat, + FloatingActionButtonLocation.centerDocked, + FloatingActionButtonLocation.centerFloat, + FloatingActionButtonLocation.endContained, + FloatingActionButtonLocation.endDocked, + FloatingActionButtonLocation.endFloat, + FloatingActionButtonLocation.miniStartDocked, + FloatingActionButtonLocation.miniStartFloat, + FloatingActionButtonLocation.miniCenterDocked, + FloatingActionButtonLocation.miniCenterFloat, + FloatingActionButtonLocation.miniEndDocked, + FloatingActionButtonLocation.miniEndFloat, + // Regression test related to https://github.com/flutter/flutter/pull/131303. + _CustomFloatingActionButtonLocation(), + ]; + + + for (final FloatingActionButtonLocation location in nonTopLocations) { + await pumpApp(fabLocation: location); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar)); + final Offset floatingActionButtonTopLeft = tester.getTopLeft( + find.byType(FloatingActionButton), + ); + + // Since padding between the SnackBar and the FAB is created by the SnackBar, + // the bottom offset of the SnackBar should be equal to the top offset of the FAB + expect(snackBarBottomLeft.dy, floatingActionButtonTopLeft.dy); + } + }, + ); + testWidgets( '${SnackBarBehavior.fixed} should align SnackBar with the top of BottomNavigationBar ' 'when Scaffold has a BottomNavigationBar and FloatingActionButton', @@ -1857,18 +2273,14 @@ void main() { await tester.pumpAndSettle(); // Have the SnackBar fully animate out. } - void expectSnackBarNotVisibleError(WidgetTester tester) { - final AssertionError exception = tester.takeException() as AssertionError; - const String message = 'Floating SnackBar presented off screen.\n' - 'A SnackBar with behavior property set to SnackBarBehavior.floating is fully ' - 'or partially off screen because some or all the widgets provided to ' - 'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and ' - 'Scaffold.bottomNavigationBar take up too much vertical space.\n' - 'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.'; - expect(exception.message, message); - } + const String offScreenMessage = 'Floating SnackBar presented off screen.\n' + 'A SnackBar with behavior property set to SnackBarBehavior.floating is fully ' + 'or partially off screen because some or all the widgets provided to ' + 'Scaffold.floatingActionButton, Scaffold.persistentFooterButtons and ' + 'Scaffold.bottomNavigationBar take up too much vertical space.\n' + 'Consider constraining the size of these widgets to allow room for the SnackBar to be visible.'; - testWidgets('Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.floatingActionButton', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.floatingActionButton', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84263 Future<void> boilerplate({required double? fabHeight}) { return tester.pumpWidget( @@ -1890,12 +2302,14 @@ void main() { // Run with the Snackbar fully off screen. await boilerplate(fabHeight: spaceAboveSnackBar + mediumFabHeight * 2); await openFloatingSnackBar(tester); - expectSnackBarNotVisibleError(tester); + AssertionError exception = tester.takeException() as AssertionError; + expect(exception.message, offScreenMessage); // Run with the Snackbar partially off screen. await boilerplate(fabHeight: spaceAboveSnackBar + mediumFabHeight + 10); await openFloatingSnackBar(tester); - expectSnackBarNotVisibleError(tester); + exception = tester.takeException() as AssertionError; + expect(exception.message, offScreenMessage); // Run with the Snackbar fully visible right on the top of the screen. await boilerplate(fabHeight: spaceAboveSnackBar + mediumFabHeight); @@ -1903,7 +2317,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.persistentFooterButtons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.persistentFooterButtons', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84263 await tester.pumpWidget( MaterialApp( @@ -1916,10 +2330,36 @@ void main() { await openFloatingSnackBar(tester); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - expectSnackBarNotVisibleError(tester); + + final AssertionError exception = tester.takeException() as AssertionError; + expect(exception.message, offScreenMessage); + }); + + testWidgetsWithLeakTracking('Material3 - Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.persistentFooterButtons', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/84263 + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + persistentFooterButtons: <Widget>[SizedBox(height: 1000)], + ), + ), + ); + + final FlutterExceptionHandler? handler = FlutterError.onError; + final List<String> errorMessages = <String>[]; + FlutterError.onError = (FlutterErrorDetails details) { + errorMessages.add(details.exceptionAsString()); + }; + addTearDown(() => FlutterError.onError = handler); + + await openFloatingSnackBar(tester); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + expect(errorMessages.contains(offScreenMessage), isTrue); }); - testWidgets('Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.bottomNavigationBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.bottomNavigationBar', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84263 await tester.pumpWidget( MaterialApp( @@ -1932,7 +2372,32 @@ void main() { await openFloatingSnackBar(tester); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - expectSnackBarNotVisibleError(tester); + final AssertionError exception = tester.takeException() as AssertionError; + expect(exception.message, offScreenMessage); + }); + + testWidgetsWithLeakTracking('Material3 - Snackbar with SnackBarBehavior.floating will assert when offset too high by a large Scaffold.bottomNavigationBar', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/84263 + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomNavigationBar: SizedBox(height: 1000), + ), + ), + ); + + final FlutterExceptionHandler? handler = FlutterError.onError; + final List<String> errorMessages = <String>[]; + FlutterError.onError = (FlutterErrorDetails details) { + errorMessages.add(details.exceptionAsString()); + }; + addTearDown(() => FlutterError.onError = handler); + + await openFloatingSnackBar(tester); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + expect(errorMessages.contains(offScreenMessage), isTrue); }); testWidgets( @@ -2009,7 +2474,7 @@ void main() { ); }); - testWidgets('SnackBars hero across transitions when using ScaffoldMessenger', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBars hero across transitions when using ScaffoldMessenger', (WidgetTester tester) async { const String snackBarText = 'hello snackbar'; const String firstHeader = 'home'; const String secondHeader = 'second'; @@ -2078,7 +2543,8 @@ void main() { expect(find.text(secondHeader), findsOneWidget); }); - testWidgets('Should have only one SnackBar during back swipe navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Should have only one SnackBar during back swipe navigation', + (WidgetTester tester) async { const String snackBarText = 'hello snackbar'; const Key snackTarget = Key('snack-target'); const Key transitionTarget = Key('transition-target'); @@ -2154,7 +2620,7 @@ void main() { expect(find.text(snackBarText), findsOneWidget); }); - testWidgets('SnackBars should be shown above the bottomSheet', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SnackBars should be shown above the bottomSheet', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2177,10 +2643,36 @@ void main() { )); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.workWithBottomSheet.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.workWithBottomSheet.png')); + }); + + testWidgetsWithLeakTracking('Material3 - SnackBars should be shown above the bottomSheet', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar(SnackBar( + content: const Text('I love Flutter!'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + behavior: SnackBarBehavior.floating, + )); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.workWithBottomSheet.png')); }); - testWidgets('ScaffoldMessenger does not duplicate a SnackBar when presenting a MaterialBanner.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScaffoldMessenger does not duplicate a SnackBar when presenting a MaterialBanner.', (WidgetTester tester) async { const Key materialBannerTapTarget = Key('materialbanner-tap-target'); const Key snackBarTapTarget = Key('snackbar-tap-target'); const String snackBarText = 'SnackBar'; @@ -2237,7 +2729,7 @@ void main() { expect(find.text(materialBannerText), findsOneWidget); }); - testWidgets('ScaffoldMessenger presents SnackBars to only the root Scaffold when Scaffolds are nested.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - ScaffoldMessenger presents SnackBars to only the root Scaffold when Scaffolds are nested.', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -2264,13 +2756,47 @@ void main() { // overlapping the FAB. await expectLater( find.byType(MaterialApp), - matchesGoldenFile('snack_bar.scaffold.nested.png'), + matchesGoldenFile('m2_snack_bar.scaffold.nested.png'), ); final Offset snackBarTopRight = tester.getTopRight(find.byType(SnackBar)); expect(snackBarTopRight.dy, 465.0); }); - testWidgets('ScaffoldMessengerState clearSnackBars works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - ScaffoldMessenger presents SnackBars to only the root Scaffold when Scaffolds are nested.', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: const Scaffold(), + floatingActionButton: FloatingActionButton(onPressed: () {}), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state<ScaffoldMessengerState>( + find.byType(ScaffoldMessenger), + ); + scaffoldMessengerState.showSnackBar(SnackBar( + content: const Text('ScaffoldMessenger'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + behavior: SnackBarBehavior.floating, + )); + await tester.pumpAndSettle(); + + expect(find.byType(SnackBar), findsOneWidget); + // The FloatingActionButton helps us identify which Scaffold has the + // SnackBar here. Since the outer Scaffold contains a FAB, the SnackBar + // should be above it. If the inner Scaffold had the SnackBar, it would be + // overlapping the FAB. + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.scaffold.nested.png')); + final Offset snackBarTopRight = tester.getTopRight(find.byType(SnackBar)); + + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(snackBarTopRight.dy, 465.0); + } + }); + + + testWidgetsWithLeakTracking('ScaffoldMessengerState clearSnackBars works as expected', (WidgetTester tester) async { final List<String> snackBars = <String>['Hello Snackbar', 'Hi Snackbar', 'Bye Snackbar']; int snackBarCounter = 0; const Key tapTarget = Key('tap-target'); @@ -2354,7 +2880,7 @@ void main() { ); } - testWidgets('Setting SnackBarBehavior.fixed will still assert for margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting SnackBarBehavior.fixed will still assert for margin', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 await tester.pumpWidget(doBuildApp( behavior: SnackBarBehavior.fixed, @@ -2371,7 +2897,7 @@ void main() { ); }); - testWidgets('Default SnackBarBehavior will still assert for margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SnackBarBehavior will still assert for margin', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 await tester.pumpWidget(doBuildApp( behavior: null, @@ -2388,7 +2914,7 @@ void main() { ); }); - testWidgets('Setting SnackBarBehavior.fixed will still assert for width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting SnackBarBehavior.fixed will still assert for width', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 await tester.pumpWidget(doBuildApp( behavior: SnackBarBehavior.fixed, @@ -2405,7 +2931,7 @@ void main() { ); }); - testWidgets('Default SnackBarBehavior will still assert for width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SnackBarBehavior will still assert for width', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 await tester.pumpWidget(doBuildApp( behavior: null, @@ -2423,7 +2949,7 @@ void main() { }); for (final double overflowThreshold in <double>[-1.0, -.0001, 1.000001, 5]) { - testWidgets('SnackBar will assert for actionOverflowThreshold outside of 0-1 range', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar will assert for actionOverflowThreshold outside of 0-1 range', (WidgetTester tester) async { await tester.pumpWidget(doBuildApp( actionOverflowThreshold: overflowThreshold, behavior: SnackBarBehavior.fixed, @@ -2437,8 +2963,7 @@ void main() { }); } - - testWidgets('Snackbar by default clips BackdropFilter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Snackbar by default clips BackdropFilter', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/98205 await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), @@ -2468,10 +2993,43 @@ void main() { await tester.tap(find.text('I am a snack bar.')); await tester.pump(); // start animation await tester.pump(const Duration(milliseconds: 750)); - await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.backdropFilter.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.backdropFilter.png')); + }); + + testWidgetsWithLeakTracking('Material3 - Snackbar by default clips BackdropFilter', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/98205 + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: const Scaffold(), + floatingActionButton: FloatingActionButton(onPressed: () {}), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state<ScaffoldMessengerState>( + find.byType(ScaffoldMessenger), + ); + scaffoldMessengerState.showSnackBar(SnackBar( + backgroundColor: Colors.transparent, + content: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20.0, + sigmaY: 20.0, + ), + child: const Text('I am a snack bar.'), + ), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + behavior: SnackBarBehavior.fixed, + )); + await tester.pumpAndSettle(); + await tester.tap(find.text('I am a snack bar.')); + await tester.pump(); // start animation + await tester.pump(const Duration(milliseconds: 750)); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.backdropFilter.png')); }); - testWidgets('Floating snackbar can display optional icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating snackbar can display optional icon', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2503,7 +3061,7 @@ void main() { 'snack_bar.goldenTest.floatingWithActionWithIcon.png')); }); - testWidgets('Fixed width snackbar can display optional icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Fixed width snackbar can display optional icon', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2527,10 +3085,37 @@ void main() { )); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.fixedWithActionWithIcon.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.fixedWithActionWithIcon.png')); }); - testWidgets('Fixed snackbar can display optional icon without action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Fixed width snackbar can display optional icon', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar(SnackBar( + content: const Text('Go get a snack'), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + showCloseIcon: true, + behavior: SnackBarBehavior.fixed, + )); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.fixedWithActionWithIcon.png')); + }); + + testWidgetsWithLeakTracking('Material2 - Fixed snackbar can display optional icon without action', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2555,11 +3140,38 @@ void main() { ); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - await expectLater(find.byType(MaterialApp), matchesGoldenFile('snack_bar.goldenTest.fixedWithIcon.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.fixedWithIcon.png')); }); - testWidgets( - 'Floating width snackbar can display optional icon without action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Fixed snackbar can display optional icon without action', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar( + const SnackBar( + content: Text('I wonder if there are snacks nearby?'), + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.fixed, + showCloseIcon: true, + ), + ); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.fixedWithIcon.png')); + }); + + testWidgetsWithLeakTracking('Material2 - Floating width snackbar can display optional icon without action', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2582,11 +3194,36 @@ void main() { )); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - await expectLater(find.byType(MaterialApp), - matchesGoldenFile('snack_bar.goldenTest.floatingWithIcon.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.floatingWithIcon.png')); + }); + + testWidgetsWithLeakTracking('Material3 - Floating width snackbar can display optional icon without action', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar(const SnackBar( + content: Text('Must go get a snack!'), + duration: Duration(seconds: 2), + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + )); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.floatingWithIcon.png')); }); - testWidgets('Floating multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Floating multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2610,11 +3247,37 @@ void main() { )); await tester.pumpAndSettle(); // Have the SnackBar fully animate out. - await expectLater(find.byType(MaterialApp), - matchesGoldenFile('snack_bar.goldenTest.multiLineWithIcon.png')); + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m2_snack_bar.goldenTest.multiLineWithIcon.png')); + }); + + testWidgetsWithLeakTracking('Material3 - Floating multi-line snackbar with icon is aligned correctly', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar(const SnackBar( + content: Text( + 'This is a really long snackbar message. So long, it spans across more than one line!'), + duration: Duration(seconds: 2), + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + )); + await tester.pumpAndSettle(); // Have the SnackBar fully animate out. + + await expectLater(find.byType(MaterialApp), matchesGoldenFile('m3_snack_bar.goldenTest.multiLineWithIcon.png')); }); - testWidgets('Floating multi-line snackbar with icon and actionOverflowThreshold=1 is aligned correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Floating multi-line snackbar with icon and actionOverflowThreshold=1 is aligned correctly', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: const Scaffold( @@ -2638,8 +3301,40 @@ void main() { )); await tester.pumpAndSettle(); // Have the SnackBar fully animate in. - await expectLater(find.byType(MaterialApp), - matchesGoldenFile('snack_bar.goldenTest.multiLineWithIconWithZeroActionOverflowThreshold.png')); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('m2_snack_bar.goldenTest.multiLineWithIconWithZeroActionOverflowThreshold.png'), + ); + }); + + testWidgetsWithLeakTracking('Material3 - Floating multi-line snackbar with icon and actionOverflowThreshold=1 is aligned correctly', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold( + bottomSheet: SizedBox( + width: 200, + height: 50, + child: ColoredBox( + color: Colors.pink, + ), + ), + ), + )); + + final ScaffoldMessengerState scaffoldMessengerState = tester.state(find.byType(ScaffoldMessenger)); + scaffoldMessengerState.showSnackBar(const SnackBar( + content: Text('This is a really long snackbar message. So long, it spans across more than one line!'), + duration: Duration(seconds: 2), + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + actionOverflowThreshold: 1, + )); + await tester.pumpAndSettle(); // Have the SnackBar fully animate in. + + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('m3_snack_bar.goldenTest.multiLineWithIconWithZeroActionOverflowThreshold.png'), + ); }); testWidgets( @@ -2671,7 +3366,7 @@ void main() { ); }); -testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tester) async { +testWidgetsWithLeakTracking('SnackBarAction backgroundColor works as a Color', (WidgetTester tester) async { const Color backgroundColor = Colors.blue; await tester.pumpWidget( @@ -2720,7 +3415,7 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes expect(materialAfterDismissed.color, Colors.transparent); }); - testWidgets('SnackBarAction backgroundColor works as a MaterialStateColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarAction backgroundColor works as a MaterialStateColor', (WidgetTester tester) async { final MaterialStateColor backgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; @@ -2774,7 +3469,7 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes expect(materialAfterDismissed.color, Colors.blue); }); - testWidgets('SnackBarAction disabledBackgroundColor works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarAction disabledBackgroundColor works as expected', (WidgetTester tester) async { const Color backgroundColor = Colors.blue; const Color disabledBackgroundColor = Colors.red; @@ -2825,7 +3520,7 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes expect(materialAfterDismissed.color, disabledBackgroundColor); }); - testWidgets('SnackBarAction asserts when backgroundColor is a MaterialStateColor and disabledBackgroundColor is also provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarAction asserts when backgroundColor is a MaterialStateColor and disabledBackgroundColor is also provided', (WidgetTester tester) async { final Color backgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; @@ -2872,7 +3567,7 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes ); }); - testWidgets('SnackBar material applies SnackBar.clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar material applies SnackBar.clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -2915,6 +3610,175 @@ testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tes expect(material.clipBehavior, Clip.antiAlias); }); + + testWidgetsWithLeakTracking('Tap on button behind snack bar defined by width', (WidgetTester tester) async { + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer<void> completer = Completer<void>(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + behavior: SnackBarBehavior.floating, + width: 100, + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, true); + }); + + + testWidgetsWithLeakTracking('Tap on button behind snack bar defined by margin', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/78537. + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer<void> completer = Completer<void>(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only(left: 100), + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, true); + }); + + testWidgets("Can't tap on button behind snack bar defined by margin and HitTestBehavior.opaque", (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/78537. + tester.view.physicalSize = const Size.square(200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const String buttonText = 'Show snackbar'; + const String snackbarContent = 'Snackbar'; + const String buttonText2 = 'Try press me'; + + final Completer<void> completer = Completer<void>(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + hitTestBehavior: HitTestBehavior.opaque, + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only(left: 100), + content: Text(snackbarContent), + ), + ); + }, + child: const Text(buttonText), + ), + ElevatedButton( + onPressed: () { + completer.complete(); + }, + child: const Text(buttonText2), + ), + ], + ); + }, + ), + ), + )); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(snackbarContent), findsOneWidget); + await tester.tapAt(tester.getTopLeft(find.text(buttonText2))); + expect(find.text(snackbarContent), findsOneWidget); + + expect(completer.isCompleted, false); + }); } /// Start test for "SnackBar dismiss test". @@ -3006,3 +3870,8 @@ class _TestMaterialStateColor extends MaterialStateColor { return const Color(_colorRed); } } + +class _CustomFloatingActionButtonLocation extends StandardFabLocation + with FabEndOffsetX, FabFloatOffsetY { + const _CustomFloatingActionButtonLocation(); +} diff --git a/packages/flutter/test/material/snack_bar_theme_test.dart b/packages/flutter/test/material/snack_bar_theme_test.dart index be25edd2d5a95..d39bdf418c8cb 100644 --- a/packages/flutter/test/material/snack_bar_theme_test.dart +++ b/packages/flutter/test/material/snack_bar_theme_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('SnackBarThemeData copyWith, ==, hashCode basics', () { @@ -45,7 +46,7 @@ void main() { throwsAssertionError); }); - testWidgets('Default SnackBarThemeData debugFillProperties', + testWidgetsWithLeakTracking('Default SnackBarThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SnackBarThemeData().debugFillProperties(builder); @@ -58,7 +59,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('SnackBarThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SnackBarThemeData( backgroundColor: Color(0xFFFFFFFF), @@ -96,10 +97,43 @@ void main() { ]); }); - testWidgets('Passing no SnackBarThemeData returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Passing no SnackBarThemeData returns defaults', (WidgetTester tester) async { const String text = 'I am a snack bar.'; - final ThemeData theme = ThemeData(); - final bool material3 = theme.useMaterial3; + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text(text), + duration: const Duration(seconds: 2), + action: SnackBarAction(label: 'ACTION', onPressed: () {}), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Material material = _getSnackBarMaterial(tester); + final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); + + expect(content.text.style, Typography.material2018().white.titleMedium); + expect(material.color, const Color(0xFF333333)); + expect(material.elevation, 6.0); + expect(material.shape, null); + }); + + testWidgetsWithLeakTracking('Material3 - Passing no SnackBarThemeData returns defaults', (WidgetTester tester) async { + const String text = 'I am a snack bar.'; + final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( theme: theme, home: Scaffold( @@ -126,15 +160,13 @@ void main() { final Material material = _getSnackBarMaterial(tester); final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); - expect(content.text.style, material3 - ? Typography.material2021().englishLike.bodyMedium?.merge(Typography.material2021().black.bodyMedium).copyWith(color: theme.colorScheme.onInverseSurface, decorationColor: theme.colorScheme.onSurface) - : Typography.material2018().white.titleMedium); - expect(material.color, material3 ? theme.colorScheme.inverseSurface : const Color(0xFF333333)); + expect(content.text.style, Typography.material2021().englishLike.bodyMedium?.merge(Typography.material2021().black.bodyMedium).copyWith(color: theme.colorScheme.onInverseSurface, decorationColor: theme.colorScheme.onSurface)); + expect(material.color, theme.colorScheme.inverseSurface); expect(material.elevation, 6.0); expect(material.shape, null); }); - testWidgets('SnackBar uses values from SnackBarThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar uses values from SnackBarThemeData', (WidgetTester tester) async { const String text = 'I am a snack bar.'; const String action = 'ACTION'; final SnackBarThemeData snackBarTheme = _snackBarTheme(showCloseIcon: true); @@ -175,7 +207,7 @@ void main() { expect(icon.icon, Icons.close); }); - testWidgets('SnackBar widget properties take priority over theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar widget properties take priority over theme', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; const Color textColor = Colors.pink; const double elevation = 7.0; @@ -235,7 +267,7 @@ void main() { expect(snackBarBottomRight.dx, (800 + snackBarWidth) / 2); // Device width is 800. }); - testWidgets('SnackBarAction uses actionBackgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarAction uses actionBackgroundColor', (WidgetTester tester) async { final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; @@ -284,7 +316,7 @@ void main() { expect(materialAfterDismissed.color, Colors.blue); }); - testWidgets('SnackBarAction backgroundColor overrides SnackBarThemeData actionBackgroundColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarAction backgroundColor overrides SnackBarThemeData actionBackgroundColor', (WidgetTester tester) async { final MaterialStateColor snackBarActionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.amber; @@ -341,7 +373,7 @@ void main() { expect(materialAfterDismissed.color, Colors.amber); }); - testWidgets('SnackBarThemeData asserts when actionBackgroundColor is a MaterialStateColor and disabledActionBackgroundColor is also provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBarThemeData asserts when actionBackgroundColor is a MaterialStateColor and disabledActionBackgroundColor is also provided', (WidgetTester tester) async { final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return Colors.blue; @@ -377,7 +409,7 @@ void main() { ); }); - testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar theme behavior is correct for floating', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating)), @@ -417,7 +449,7 @@ void main() { expect(snackBarBottomCenter.dy == floatingActionButtonTopCenter.dy, true); }); - testWidgets('SnackBar theme behavior is correct for fixed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar theme behavior is correct for fixed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.fixed), @@ -498,7 +530,7 @@ void main() { ); } - testWidgets('SnackBar theme behavior will assert properly for margin use', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar theme behavior will assert properly for margin use', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84935 // SnackBarBehavior.floating set in theme does not assert with margin await tester.pumpWidget(buildApp( @@ -537,7 +569,7 @@ void main() { }); } - testWidgets('SnackBar theme behavior will assert properly for width use', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnackBar theme behavior will assert properly for width use', (WidgetTester tester) async { // SnackBarBehavior.floating set in theme does not assert with width await tester.pumpWidget(buildApp( themedBehavior: SnackBarBehavior.floating, diff --git a/packages/flutter/test/material/spell_check_suggestions_toolbar_layout_delegate_test.dart b/packages/flutter/test/material/spell_check_suggestions_toolbar_layout_delegate_test.dart index 2346513651263..494dbe7308aca 100644 --- a/packages/flutter/test/material/spell_check_suggestions_toolbar_layout_delegate_test.dart +++ b/packages/flutter/test/material/spell_check_suggestions_toolbar_layout_delegate_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('positions itself at anchorAbove if it fits and shifts up when not', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at anchorAbove if it fits and shifts up when not', (WidgetTester tester) async { late StateSetter setState; const double toolbarOverlap = 100; const double height = 500; diff --git a/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart b/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart index 66c668f671b02..e9bbc4950eae7 100644 --- a/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart +++ b/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // Vertical position at which to anchor the toolbar for testing. const double _kAnchor = 200; @@ -47,7 +48,7 @@ void main() { ); } - testWidgets('positions toolbar below anchor when it fits above bottom view padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions toolbar below anchor when it fits above bottom view padding', (WidgetTester tester) async { // We expect the toolbar to be positioned right below the anchor with padding accounted for. await tester.pumpWidget( MaterialApp( @@ -64,7 +65,7 @@ void main() { expect(toolbarY, equals(_kAnchor)); }); - testWidgets('re-positions toolbar higher below anchor when it does not fit above bottom view padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('re-positions toolbar higher below anchor when it does not fit above bottom view padding', (WidgetTester tester) async { // We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor. const double expectedToolbarY = _kAnchor - _kTestToolbarOverlap; @@ -83,7 +84,7 @@ void main() { expect(toolbarY, equals(expectedToolbarY)); }); - testWidgets('more than three suggestions throws an error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('more than three suggestions throws an error', (WidgetTester tester) async { Future<void> pumpToolbar(List<String> suggestions) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index d85070b607b4f..405b7097a5e59 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Material3 has sentence case labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 has sentence case labels', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -38,7 +39,7 @@ void main() { expect(find.text('Cancel'), findsWidgets); }); - testWidgets('Stepper tap callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper tap callback test', (WidgetTester tester) async { int index = 0; await tester.pumpWidget( @@ -72,7 +73,7 @@ void main() { expect(index, 1); }); - testWidgets('Stepper expansion test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper expansion test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( @@ -139,7 +140,7 @@ void main() { expect(box.size.height, 432.0); }); - testWidgets('Stepper horizontal size test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper horizontal size test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( @@ -165,7 +166,7 @@ void main() { expect(box.size.height, 600.0); }); - testWidgets('Stepper visibility test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper visibility test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -214,15 +215,57 @@ void main() { expect(find.text('B'), findsOneWidget); }); - testWidgets('Stepper button test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Stepper button test', (WidgetTester tester) async { bool continuePressed = false; bool cancelPressed = false; - final ThemeData theme = ThemeData(); - final bool material3 = theme.useMaterial3; await tester.pumpWidget( MaterialApp( - theme: theme, + theme: ThemeData(useMaterial3: false), + home: Material( + child: Stepper( + type: StepperType.horizontal, + onStepContinue: () { + continuePressed = true; + }, + onStepCancel: () { + cancelPressed = true; + }, + steps: const <Step>[ + Step( + title: Text('Step 1'), + content: SizedBox( + width: 100.0, + height: 100.0, + ), + ), + Step( + title: Text('Step 2'), + content: SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ], + ), + ), + ), + ); + + await tester.tap(find.text('CONTINUE')); + await tester.tap(find.text('CANCEL')); + + expect(continuePressed, isTrue); + expect(cancelPressed, isTrue); + }); + + testWidgetsWithLeakTracking('Material3 - Stepper button test', (WidgetTester tester) async { + bool continuePressed = false; + bool cancelPressed = false; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), home: Material( child: Stepper( type: StepperType.horizontal, @@ -253,14 +296,14 @@ void main() { ), ); - await tester.tap(find.text(material3 ? 'Continue' : 'CONTINUE')); - await tester.tap(find.text(material3 ? 'Cancel' : 'CANCEL')); + await tester.tap(find.text('Continue')); + await tester.tap(find.text('Cancel')); expect(continuePressed, isTrue); expect(cancelPressed, isTrue); }); - testWidgets('Stepper disabled step test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper disabled step test', (WidgetTester tester) async { int index = 0; await tester.pumpWidget( @@ -296,7 +339,7 @@ void main() { expect(index, 0); }); - testWidgets('Stepper scroll test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper scroll test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -370,7 +413,7 @@ void main() { expect(scrollableState.position.pixels, greaterThan(0.0)); }); - testWidgets('Stepper index test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper index test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( @@ -403,7 +446,7 @@ void main() { expect(find.text('2'), findsOneWidget); }); - testWidgets('Stepper custom controls test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper custom controls test', (WidgetTester tester) async { bool continuePressed = false; void setContinue() { continuePressed = true; @@ -482,7 +525,7 @@ void main() { expect(continuePressed, isTrue); }); -testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async { +testWidgetsWithLeakTracking('Stepper custom indexed controls test', (WidgetTester tester) async { int currentStep = 0; void setContinue() { @@ -577,7 +620,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(find.text('Continue to 2'), findsNWidgets(1)); }); - testWidgets('Stepper error test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper error test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Center( @@ -602,7 +645,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(find.text('!'), findsOneWidget); }); - testWidgets('Nested stepper error test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested stepper error test', (WidgetTester tester) async { late FlutterErrorDetails errorDetails; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { @@ -669,7 +712,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async }); ///https://github.com/flutter/flutter/issues/16920 - testWidgets('Stepper icons size test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper icons size test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -698,7 +741,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(renderObject.size, equals(const Size.square(18.0))); }); - testWidgets('Stepper physics scroll error test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper physics scroll error test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -731,7 +774,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(find.text('Text After Stepper'), findsNothing); }); - testWidgets("Vertical Stepper can't be focused when disabled.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Vertical Stepper can't be focused when disabled.", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -755,7 +798,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(disabledNode.hasPrimaryFocus, isFalse); }); - testWidgets("Horizontal Stepper can't be focused when disabled.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Horizontal Stepper can't be focused when disabled.", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -780,7 +823,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(disabledNode.hasPrimaryFocus, isFalse); }); - testWidgets('Stepper header title should not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper header title should not overflow', (WidgetTester tester) async { const String longText = 'A long long long long long long long long long long long long text'; @@ -806,7 +849,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(tester.takeException(), isNull); }); - testWidgets('Stepper header subtitle should not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper header subtitle should not overflow', (WidgetTester tester) async { const String longText = 'A long long long long long long long long long long long long text'; @@ -833,7 +876,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(tester.takeException(), isNull); }); - testWidgets('Stepper enabled button styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Stepper enabled button styles', (WidgetTester tester) async { Widget buildFrame(ThemeData theme) { return MaterialApp( theme: theme, @@ -861,15 +904,14 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async const OutlinedBorder buttonShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2))); - final ThemeData themeLight = ThemeData.light(); - final bool material3Light = themeLight.useMaterial3; + final ThemeData themeLight = ThemeData(useMaterial3: false); await tester.pumpWidget(buildFrame(themeLight)); - final String continueStr = material3Light ? 'Continue' : 'CONTINUE'; - final String cancelStr = material3Light ? 'Cancel' : 'CANCEL'; - final Rect continueButtonRect = material3Light ? const Rect.fromLTRB(24.0, 212.0, 169.0, 260.0) : const Rect.fromLTRB(24.0, 212.0, 168.0, 260.0); - final Rect cancelButtonRect = material3Light ? const Rect.fromLTRB(177.0, 212.0, 294.0, 260.0) : const Rect.fromLTRB(176.0, 212.0, 292.0, 260.0); - expect(buttonMaterial(continueStr).color!.value, material3Light ? themeLight.colorScheme.primary.value : 0xff2196f3); + const String continueStr = 'CONTINUE'; + const String cancelStr = 'CANCEL'; + const Rect continueButtonRect = Rect.fromLTRB(24.0, 212.0, 168.0, 260.0); + const Rect cancelButtonRect = Rect.fromLTRB(176.0, 212.0, 292.0, 260.0); + expect(buttonMaterial(continueStr).color!.value, 0xff2196f3); expect(buttonMaterial(continueStr).textStyle!.color!.value, 0xffffffff); expect(buttonMaterial(continueStr).shape, buttonShape); expect(tester.getRect(find.widgetWithText(TextButton, continueStr)), continueButtonRect); @@ -879,13 +921,12 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(buttonMaterial(cancelStr).shape, buttonShape); expect(tester.getRect(find.widgetWithText(TextButton, cancelStr)), cancelButtonRect); - final ThemeData themeDark = ThemeData.dark(); - final bool material3Dark = themeDark.useMaterial3; + final ThemeData themeDark = ThemeData.dark(useMaterial3: false); await tester.pumpWidget(buildFrame(themeDark)); await tester.pumpAndSettle(); // Complete the theme animation. expect(buttonMaterial(continueStr).color!.value, 0); - expect(buttonMaterial(continueStr).textStyle!.color!.value, material3Dark ? themeDark.colorScheme.onSurface.value : 0xffffffff); + expect(buttonMaterial(continueStr).textStyle!.color!.value, 0xffffffff); expect(buttonMaterial(continueStr).shape, buttonShape); expect(tester.getRect(find.widgetWithText(TextButton, continueStr)), continueButtonRect); @@ -895,13 +936,15 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(tester.getRect(find.widgetWithText(TextButton, cancelStr)), cancelButtonRect); }); - testWidgets('Stepper disabled button styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Stepper enabled button styles', (WidgetTester tester) async { Widget buildFrame(ThemeData theme) { return MaterialApp( theme: theme, home: Material( child: Stepper( type: StepperType.horizontal, + onStepCancel: () { }, + onStepContinue: () { }, steps: const <Step>[ Step( title: Text('step1'), @@ -919,31 +962,159 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async ); } - final ThemeData themeLight = ThemeData.light(); - final bool material3Light = themeLight.useMaterial3; + const OutlinedBorder buttonShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2))); + + final ThemeData themeLight = ThemeData(useMaterial3: true); await tester.pumpWidget(buildFrame(themeLight)); - final String continueStr = material3Light ? 'Continue' : 'CONTINUE'; - final String cancelStr = material3Light ? 'Cancel' : 'CANCEL'; + const String continueStr = 'Continue'; + const String cancelStr = 'Cancel'; + const Rect continueButtonRect = Rect.fromLTRB(24.0, 212.0, 168.8, 260.0); + const Rect cancelButtonRect = Rect.fromLTRB(176.8, 212.0, 293.4, 260.0); + expect(buttonMaterial(continueStr).color!.value, themeLight.colorScheme.primary.value); + expect(buttonMaterial(continueStr).textStyle!.color!.value, 0xffffffff); + expect(buttonMaterial(continueStr).shape, buttonShape); + expect( + tester.getRect(find.widgetWithText(TextButton, continueStr)), + rectMoreOrLessEquals(continueButtonRect, epsilon: 0.001), + ); + + expect(buttonMaterial(cancelStr).color!.value, 0); + expect(buttonMaterial(cancelStr).textStyle!.color!.value, 0x8a000000); + expect(buttonMaterial(cancelStr).shape, buttonShape); + expect( + tester.getRect(find.widgetWithText(TextButton, cancelStr)), + rectMoreOrLessEquals(cancelButtonRect, epsilon: 0.001), + ); + + final ThemeData themeDark = ThemeData.dark(useMaterial3: true); + await tester.pumpWidget(buildFrame(themeDark)); + await tester.pumpAndSettle(); // Complete the theme animation. + expect(buttonMaterial(continueStr).color!.value, 0); - expect(buttonMaterial(continueStr).textStyle!.color!.value, material3Light ? themeLight.colorScheme.onSurface.withOpacity(0.38).value : 0x61000000); + expect(buttonMaterial(continueStr).textStyle!.color!.value, themeDark.colorScheme.onSurface.value); + expect(buttonMaterial(continueStr).shape, buttonShape); + expect( + tester.getRect(find.widgetWithText(TextButton, continueStr)), + rectMoreOrLessEquals(continueButtonRect, epsilon: 0.001), + ); expect(buttonMaterial(cancelStr).color!.value, 0); - expect(buttonMaterial(cancelStr).textStyle!.color!.value, material3Light ? themeLight.colorScheme.onSurface.withOpacity(0.38).value : 0x61000000); + expect(buttonMaterial(cancelStr).textStyle!.color!.value, 0xb3ffffff); + expect(buttonMaterial(cancelStr).shape, buttonShape); + expect( + tester.getRect(find.widgetWithText(TextButton, cancelStr)), + rectMoreOrLessEquals(cancelButtonRect, epsilon: 0.001), + ); + }); - final ThemeData themeDark = ThemeData.dark(); - final bool material3Dark = themeDark.useMaterial3; + testWidgetsWithLeakTracking('Material2 - Stepper disabled button styles', (WidgetTester tester) async { + Widget buildFrame(ThemeData theme) { + return MaterialApp( + theme: theme, + home: Material( + child: Stepper( + type: StepperType.horizontal, + steps: const <Step>[ + Step( + title: Text('step1'), + content: SizedBox(width: 100, height: 100), + ), + ], + ), + ), + ); + } + + Material buttonMaterial(String label) { + return tester.widget<Material>( + find.descendant(of: find.widgetWithText(TextButton, label), matching: find.byType(Material)), + ); + } + + final ThemeData themeLight = ThemeData(useMaterial3: false); + await tester.pumpWidget(buildFrame(themeLight)); + + const String continueStr = 'CONTINUE'; + const String cancelStr = 'CANCEL'; + expect(buttonMaterial(continueStr).color!.value, 0); + expect(buttonMaterial(continueStr).textStyle!.color!.value, 0x61000000); + + expect(buttonMaterial(cancelStr).color!.value, 0); + expect(buttonMaterial(cancelStr).textStyle!.color!.value, 0x61000000); + + final ThemeData themeDark = ThemeData.dark(useMaterial3: false); await tester.pumpWidget(buildFrame(themeDark)); await tester.pumpAndSettle(); // Complete the theme animation. expect(buttonMaterial(continueStr).color!.value, 0); - expect(buttonMaterial(continueStr).textStyle!.color!.value, material3Dark ? themeDark.colorScheme.onSurface.withOpacity(0.38).value : 0x61ffffff); + expect(buttonMaterial(continueStr).textStyle!.color!.value, 0x61ffffff); expect(buttonMaterial(cancelStr).color!.value, 0); - expect(buttonMaterial(cancelStr).textStyle!.color!.value, material3Dark ? themeDark.colorScheme.onSurface.withOpacity(0.38).value : 0x61ffffff); + expect(buttonMaterial(cancelStr).textStyle!.color!.value, 0x61ffffff); }); - testWidgets('Vertical and Horizontal Stepper physics test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Stepper disabled button styles', (WidgetTester tester) async { + Widget buildFrame(ThemeData theme) { + return MaterialApp( + theme: theme, + home: Material( + child: Stepper( + type: StepperType.horizontal, + steps: const <Step>[ + Step( + title: Text('step1'), + content: SizedBox(width: 100, height: 100), + ), + ], + ), + ), + ); + } + + Material buttonMaterial(String label) { + return tester.widget<Material>( + find.descendant(of: find.widgetWithText(TextButton, label), matching: find.byType(Material)), + ); + } + + final ThemeData themeLight = ThemeData(useMaterial3: true); + final ColorScheme colorsLight = themeLight.colorScheme; + await tester.pumpWidget(buildFrame(themeLight)); + + const String continueStr = 'Continue'; + const String cancelStr = 'Cancel'; + expect(buttonMaterial(continueStr).color!.value, 0); + expect( + buttonMaterial(continueStr).textStyle!.color!.value, + colorsLight.onSurface.withOpacity(0.38).value, + ); + + expect(buttonMaterial(cancelStr).color!.value, 0); + expect( + buttonMaterial(cancelStr).textStyle!.color!.value, + colorsLight.onSurface.withOpacity(0.38).value, + ); + + final ThemeData themeDark = ThemeData.dark(useMaterial3: true); + final ColorScheme colorsDark = themeDark.colorScheme; + await tester.pumpWidget(buildFrame(themeDark)); + await tester.pumpAndSettle(); // Complete the theme animation. + + expect(buttonMaterial(continueStr).color!.value, 0); + expect( + buttonMaterial(continueStr).textStyle!.color!.value, + colorsDark.onSurface.withOpacity(0.38).value, + ); + + expect(buttonMaterial(cancelStr).color!.value, 0); + expect( + buttonMaterial(cancelStr).textStyle!.color!.value, + colorsDark.onSurface.withOpacity(0.38).value, + ); + }); + + testWidgetsWithLeakTracking('Vertical and Horizontal Stepper physics test', (WidgetTester tester) async { const ScrollPhysics physics = NeverScrollableScrollPhysics(); for (final StepperType type in StepperType.values) { @@ -972,38 +1143,39 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async } }); - testWidgets('ScrollController is passed to the stepper listview', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); - for (final StepperType type in StepperType.values) { - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Stepper( - controller: controller, - type: type, - steps: const <Step>[ - Step( - title: Text('Step 1'), - content: SizedBox( - width: 100.0, - height: 100.0, - ), + testWidgetsWithLeakTracking('ScrollController is passed to the stepper listview', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + addTearDown(() => controller.dispose()); + for (final StepperType type in StepperType.values) { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Stepper( + controller: controller, + type: type, + steps: const <Step>[ + Step( + title: Text('Step 1'), + content: SizedBox( + width: 100.0, + height: 100.0, ), - ], - ), + ), + ], ), ), - ); + ), + ); - final ListView listView = tester.widget<ListView>( - find.descendant(of: find.byType(Stepper), - matching: find.byType(ListView), - )); - expect(listView.controller, controller); - } - }); + final ListView listView = tester.widget<ListView>( + find.descendant(of: find.byType(Stepper), + matching: find.byType(ListView), + )); + expect(listView.controller, controller); + } + }); - testWidgets('Stepper horizontal size test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper horizontal size test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/77732 Widget buildFrame({ bool isActive = true, Brightness? brightness }) { return MaterialApp( @@ -1048,7 +1220,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(circleFillColor(), dark.background); }); - testWidgets('Stepper custom elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper custom elevation', (WidgetTester tester) async { const double elevation = 4.0; await tester.pumpWidget( @@ -1082,7 +1254,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(material.elevation, elevation); }); - testWidgets('Stepper with default elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper with default elevation', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1114,7 +1286,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(material.elevation, 2.0); }); - testWidgets('Stepper horizontal preserves state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper horizontal preserves state', (WidgetTester tester) async { const Color untappedColor = Colors.blue; const Color tappedColor = Colors.red; int index = 0; @@ -1185,7 +1357,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async // The color should still be `tappedColor` expect(getColor(), tappedColor); }); - testWidgets('Stepper custom margin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper custom margin', (WidgetTester tester) async { const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only( bottom: 20, @@ -1222,7 +1394,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(material.margin, equals(margin)); }); - testWidgets('Stepper with Alternative Label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper with Alternative Label', (WidgetTester tester) async { int index = 0; late TextStyle bodyLargeStyle; late TextStyle bodyMediumStyle; @@ -1306,7 +1478,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(bodyMediumStyle, nextLabelTextWidget.style); }); - testWidgets('Stepper Connector Style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper Connector Style', (WidgetTester tester) async { const Color selectedColor = Colors.black; const Color disabledColor = Colors.white; int index = 0; @@ -1375,7 +1547,7 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(lineColor('line0'), selectedColor); }); - testWidgets('Stepper stepIconBuilder test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stepper stepIconBuilder test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( diff --git a/packages/flutter/test/material/switch_list_tile_test.dart b/packages/flutter/test/material/switch_list_tile_test.dart index e1ffc363765f4..b6dc0200ecfaa 100644 --- a/packages/flutter/test/material/switch_list_tile_test.dart +++ b/packages/flutter/test/material/switch_list_tile_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -23,7 +23,7 @@ Widget wrap({ required Widget child }) { } void main() { - testWidgets('SwitchListTile control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile control test', (WidgetTester tester) async { final List<dynamic> log = <dynamic>[]; await tester.pumpWidget(wrap( child: SwitchListTile( @@ -38,7 +38,7 @@ void main() { expect(log, equals(<dynamic>[false, '-', false])); }); - testWidgets('SwitchListTile semantics test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile semantics test', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(wrap( child: Column( @@ -116,7 +116,7 @@ void main() { semantics.dispose(); }); - testWidgets('Material2 - SwitchListTile has the right colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile has the right colors', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( MediaQuery( @@ -170,7 +170,7 @@ void main() { ); }); - testWidgets('Material3 - SwitchListTile has the right colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile has the right colors', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( MediaQuery( @@ -221,7 +221,7 @@ void main() { ); }); - testWidgets('SwitchListTile.adaptive delegates to', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile.adaptive delegates to', (WidgetTester tester) async { bool value = false; Widget buildFrame(TargetPlatform platform) { @@ -268,7 +268,7 @@ void main() { } }); - testWidgets('SwitchListTile contentPadding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile contentPadding', (WidgetTester tester) async { Widget buildFrame(TextDirection textDirection) { return MediaQuery( data: const MediaQueryData(), @@ -306,7 +306,7 @@ void main() { expect(tester.getTopRight(find.text('L')).dx, 790.0); // 800 - contentPadding.start }); - testWidgets('SwitchListTile can autofocus unless disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile can autofocus unless disabled.', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( @@ -350,7 +350,7 @@ void main() { expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse); }); - testWidgets('SwitchListTile controlAffinity test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile controlAffinity test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: SwitchListTile( @@ -370,7 +370,7 @@ void main() { expect(listTile.trailing.runtimeType, Icon); }); - testWidgets('SwitchListTile controlAffinity default value test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile controlAffinity default value test', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: SwitchListTile( @@ -390,7 +390,7 @@ void main() { expect(listTile.trailing.runtimeType, Switch); }); - testWidgets('SwitchListTile respects shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects shape', (WidgetTester tester) async { const ShapeBorder shapeBorder = RoundedRectangleBorder( borderRadius: BorderRadius.horizontal(right: Radius.circular(100)), ); @@ -409,7 +409,7 @@ void main() { expect(tester.widget<InkWell>(find.byType(InkWell)).customBorder, shapeBorder); }); - testWidgets('SwitchListTile respects tileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects tileColor', (WidgetTester tester) async { final Color tileColor = Colors.red.shade500; await tester.pumpWidget( @@ -428,7 +428,7 @@ void main() { expect(find.byType(Material), paints..rect(color: tileColor)); }); - testWidgets('SwitchListTile respects selectedTileColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects selectedTileColor', (WidgetTester tester) async { final Color selectedTileColor = Colors.green.shade500; await tester.pumpWidget( @@ -448,7 +448,7 @@ void main() { expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); - testWidgets('SwitchListTile selected item text Color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile selected item text Color', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/76909 const Color activeColor = Color(0xff00ff00); @@ -487,7 +487,7 @@ void main() { expect(textColor('title'), activeColor); }); - testWidgets('SwitchListTile respects visualDensity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects visualDensity', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( @@ -511,7 +511,7 @@ void main() { expect(box.size, equals(const Size(800, 56))); }); - testWidgets('SwitchListTile respects focusNode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects focusNode', (WidgetTester tester) async { final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( wrap( @@ -533,7 +533,7 @@ void main() { expect(tileNode.hasPrimaryFocus, isTrue); }); - testWidgets('SwitchListTile onFocusChange callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile onFocusChange callback', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'SwitchListTile onFocusChange'); bool gotFocus = false; await tester.pumpWidget( @@ -560,9 +560,10 @@ void main() { await tester.pump(); expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + node.dispose(); }); - testWidgets('SwitchListTile.adaptive onFocusChange Callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile.adaptive onFocusChange Callback', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'SwitchListTile.adaptive onFocusChange'); bool gotFocus = false; await tester.pumpWidget( @@ -589,6 +590,7 @@ void main() { await tester.pump(); expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + node.dispose(); }); group('feedback', () { @@ -602,7 +604,7 @@ void main() { feedback.dispose(); }); - testWidgets('SwitchListTile respects enableFeedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects enableFeedback', (WidgetTester tester) async { Future<void> buildTest(bool enableFeedback) async { return tester.pumpWidget( wrap( @@ -631,7 +633,7 @@ void main() { }); }); - testWidgets('SwitchListTile respects hoverColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects hoverColor', (WidgetTester tester) async { const Key key = Key('test'); await tester.pumpWidget( wrap( @@ -671,7 +673,7 @@ void main() { ); }); - testWidgets('Material2 - SwitchListTile respects thumbColor in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile respects thumbColor in active/enabled states', (WidgetTester tester) async { const Color activeEnabledThumbColor = Color(0xFF000001); const Color activeDisabledThumbColor = Color(0xFF000002); const Color inactiveEnabledThumbColor = Color(0xFF000003); @@ -739,7 +741,7 @@ void main() { ); }); - testWidgets('Material3 - SwitchListTile respects thumbColor in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile respects thumbColor in active/enabled states', (WidgetTester tester) async { const Color activeEnabledThumbColor = Color(0xFF000001); const Color activeDisabledThumbColor = Color(0xFF000002); const Color inactiveEnabledThumbColor = Color(0xFF000003); @@ -807,7 +809,7 @@ void main() { ); }); - testWidgets('Material2 - SwitchListTile respects thumbColor in hovered/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile respects thumbColor in hovered/pressed states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredThumbColor = Color(0xFF4caf50); const Color pressedThumbColor = Color(0xFFF44336); @@ -863,7 +865,7 @@ void main() { ); }); - testWidgets('Material3 - SwitchListTile respects thumbColor in hovered/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile respects thumbColor in hovered/pressed states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredThumbColor = Color(0xFF4caf50); const Color pressedThumbColor = Color(0xFFF44336); @@ -919,7 +921,7 @@ void main() { ); }); - testWidgets('SwitchListTile respects trackColor in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects trackColor in active/enabled states', (WidgetTester tester) async { const Color activeEnabledTrackColor = Color(0xFF000001); const Color activeDisabledTrackColor = Color(0xFF000002); const Color inactiveEnabledTrackColor = Color(0xFF000003); @@ -984,7 +986,7 @@ void main() { ); }); - testWidgets('SwitchListTile respects trackColor in hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects trackColor in hovered states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredTrackColor = Color(0xFF4caf50); @@ -1025,7 +1027,7 @@ void main() { ); }); - testWidgets('SwitchListTile respects thumbIcon - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects thumbIcon - M3', (WidgetTester tester) async { const Icon activeIcon = Icon(Icons.check); const Icon inactiveIcon = Icon(Icons.close); @@ -1110,7 +1112,7 @@ void main() { ); }); - testWidgets('Material2 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1138,7 +1140,7 @@ void main() { expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0)); }); - testWidgets('Material3 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1166,7 +1168,7 @@ void main() { expect(tester.getSize(find.byType(Switch)), const Size(60.0, 40.0)); }); - testWidgets('Material2 - SwitchListTile.adaptive respects applyCupertinoTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile.adaptive respects applyCupertinoTheme', (WidgetTester tester) async { Widget buildSwitchListTile(bool applyCupertinoTheme, TargetPlatform platform) { return MaterialApp( theme: ThemeData(useMaterial3: false, platform: platform), @@ -1202,7 +1204,7 @@ void main() { } }); - testWidgets('Material3 - SwitchListTile.adaptive respects applyCupertinoTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile.adaptive respects applyCupertinoTheme', (WidgetTester tester) async { Widget buildSwitchListTile(bool applyCupertinoTheme, TargetPlatform platform) { return MaterialApp( theme: ThemeData(useMaterial3: true, platform: platform), @@ -1238,7 +1240,7 @@ void main() { } }); - testWidgets('Material2 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { return MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1266,7 +1268,7 @@ void main() { expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0)); }); - testWidgets('Material3 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { return MaterialApp( theme: ThemeData(useMaterial3: true), @@ -1294,7 +1296,7 @@ void main() { expect(tester.getSize(find.byType(Switch)), const Size(60.0, 40.0)); }); - testWidgets('SwitchListTile passes the value of dragStartBehavior to Switch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile passes the value of dragStartBehavior to Switch', (WidgetTester tester) async { Widget buildSwitchListTile(DragStartBehavior dragStartBehavior) { return wrap( child: StatefulBuilder( @@ -1317,7 +1319,7 @@ void main() { expect(switchWidget1.dragStartBehavior, DragStartBehavior.down); }); - testWidgets('Switch on SwitchListTile changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch on SwitchListTile changes mouse cursor when hovered', (WidgetTester tester) async { // Test SwitchListTile.adaptive() constructor await tester.pumpWidget(wrap( child: StatefulBuilder( @@ -1378,7 +1380,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('Switch with splash radius set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch with splash radius set', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 35; await tester.pumpWidget(wrap( @@ -1404,7 +1406,7 @@ void main() { ); }); - testWidgets('The overlay color for the thumb of the switch resolves in active/pressed/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The overlay color for the thumb of the switch resolves in active/pressed/hovered states', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color activeThumbColor = Color(0xFF000000); const Color inactiveThumbColor = Color(0xFF000010); @@ -1525,7 +1527,7 @@ void main() { ); }); - testWidgets('SwitchListTile respects trackOutlineColor in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects trackOutlineColor in active/enabled states', (WidgetTester tester) async { const Color activeEnabledTrackOutlineColor = Color(0xFF000001); const Color activeDisabledTrackOutlineColor = Color(0xFF000002); const Color inactiveEnabledTrackOutlineColor = Color(0xFF000003); @@ -1594,7 +1596,7 @@ void main() { ); }); - testWidgets('SwitchListTile respects trackOutlineColor in hovered state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchListTile respects trackOutlineColor in hovered state', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredTrackColor = Color(0xFF4caf50); diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index f7377f97d3d42..df4d7bef3c559 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -17,14 +17,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { final ThemeData theme = ThemeData(); - testWidgets('Switch can toggle on tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can toggle on tap', (WidgetTester tester) async { final Key switchKey = UniqueKey(); bool value = false; @@ -60,7 +59,7 @@ void main() { expect(value, isTrue); }); - testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( Theme( @@ -106,7 +105,7 @@ void main() { expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0)); }); - testWidgets('Material2 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async { const double maxWidth = 300; const double maxHeight = 100; @@ -158,7 +157,7 @@ void main() { ); }); - testWidgets('Material3 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async { const double maxWidth = 300; const double maxHeight = 100; @@ -210,7 +209,7 @@ void main() { ); }); - testWidgets('Switch can drag (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can drag (LTR)', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( @@ -260,7 +259,7 @@ void main() { expect(value, isFalse); }); - testWidgets('Switch can drag with dragStartBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can drag with dragStartBehavior', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( @@ -352,7 +351,7 @@ void main() { expect(value, isFalse); }); - testWidgets('Switch can drag (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can drag (RTL)', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( @@ -400,7 +399,7 @@ void main() { expect(value, isFalse); }); - testWidgets('Material2 - Switch has default colors when enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch has default colors when enabled', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( MaterialApp( @@ -459,7 +458,7 @@ void main() { ); }); - testWidgets('Material3 - Switch has default colors when enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch has default colors when enabled', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colors = theme.colorScheme; bool value = false; @@ -524,7 +523,7 @@ void main() { ); }); - testWidgets('Material2 - Switch has default colors when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch has default colors when disabled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -588,7 +587,7 @@ void main() { ); }); - testWidgets('Material3 - Inactive Switch has default colors when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Inactive Switch has default colors when disabled', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; @@ -626,7 +625,7 @@ void main() { ); }); - testWidgets('Material3 - Active Switch has default colors when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Active Switch has default colors when disabled', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; await tester.pumpWidget(MaterialApp( @@ -659,7 +658,7 @@ void main() { ); }); - testWidgets('Material2 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; Finder findSwitch() { @@ -701,9 +700,11 @@ void main() { expect(getSwitchMaterial(tester), paints..circle(color: theme.hoverColor) ); + + focusNode.dispose(); }); - testWidgets('Material3 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -746,9 +747,11 @@ void main() { expect(getSwitchMaterial(tester), paints..circle(color: theme.colorScheme.primary.withOpacity(0.08)) ); + + focusNode.dispose(); }); - testWidgets('Material2 - Switch can be set color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch can be set color', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( MaterialApp( @@ -809,7 +812,7 @@ void main() { ); }); - testWidgets('Material3 - Switch can be set color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch can be set color', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; @@ -874,7 +877,7 @@ void main() { ); }); - testWidgets('Drag ends after animation completes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag ends after animation completes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17773 bool value = false; @@ -919,7 +922,7 @@ void main() { expect(tester.hasRunningAnimations, false); }); - testWidgets('can veto switch dragging result', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can veto switch dragging result', (WidgetTester tester) async { bool value = false; await tester.pumpWidget( @@ -1000,7 +1003,7 @@ void main() { expect(state.position.value, 1.0); }); - testWidgets('switch has semantic events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('switch has semantic events', (WidgetTester tester) async { dynamic semanticEvent; bool value = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { @@ -1047,7 +1050,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('switch sends semantic events from parent if fully merged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('switch sends semantic events from parent if fully merged', (WidgetTester tester) async { dynamic semanticEvent; bool value = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { @@ -1098,7 +1101,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('Switch.adaptive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch.adaptive', (WidgetTester tester) async { bool value = false; const Color activeTrackColor = Color(0xffff1200); const Color inactiveTrackColor = Color(0xffff12ff); @@ -1158,7 +1161,7 @@ void main() { } }); - testWidgets('Material2 - Switch is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool value = true; @@ -1238,9 +1241,11 @@ void main() { ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xffbdbdbd)), ); + + focusNode.dispose(); }); - testWidgets('Material3 - Switch is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch is focusable and has correct focus color', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); @@ -1325,9 +1330,11 @@ void main() { ) ..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), ); + + focusNode.dispose(); }); - testWidgets('Switch with splash radius set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch with splash radius set', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { @@ -1354,7 +1361,7 @@ void main() { ); }); - testWidgets('Material2 - Switch can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool value = true; Widget buildApp({bool enabled = true}) { @@ -1430,7 +1437,7 @@ void main() { ); }); - testWidgets('Material3 - Switch can be hovered and has correct hover color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch can be hovered and has correct hover color', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1502,7 +1509,7 @@ void main() { ); }); - testWidgets('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool value = true; Widget buildApp({bool enabled = true}) { @@ -1543,7 +1550,7 @@ void main() { expect(value, isTrue); }); - testWidgets('Switch changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch changes mouse cursor when hovered', (WidgetTester tester) async { // Test Switch.adaptive() constructor await tester.pumpWidget( MaterialApp( @@ -1647,7 +1654,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Material switch should not recreate its render object when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material switch should not recreate its render object when disabled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/61247. bool value = true; bool enabled = true; @@ -1694,7 +1701,7 @@ void main() { expect(updatedSwitchState.position.isDismissed, false); }); - testWidgets('Material2 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async { const Color activeEnabledThumbColor = Color(0xFF000001); const Color activeDisabledThumbColor = Color(0xFF000002); const Color inactiveEnabledThumbColor = Color(0xFF000003); @@ -1802,7 +1809,7 @@ void main() { ); }); - testWidgets('Material3 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; const Color activeEnabledThumbColor = Color(0xFF000001); @@ -1911,7 +1918,7 @@ void main() { ); }); - testWidgets('Material2 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredThumbColor = Color(0xFF000001); @@ -1985,9 +1992,11 @@ void main() { ..rrect(color: hoveredThumbColor), reason: 'Inactive disabled switch should default track and custom thumb color', ); + + focusNode.dispose(); }); - testWidgets('Material3 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); @@ -2061,9 +2070,11 @@ void main() { ..rrect(color: hoveredThumbColor), reason: 'active enabled switch should default track and custom thumb color', ); + + focusNode.dispose(); }); - testWidgets('Material2 - Track color resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Track color resolves in active/enabled states', (WidgetTester tester) async { const Color activeEnabledTrackColor = Color(0xFF000001); const Color activeDisabledTrackColor = Color(0xFF000002); const Color inactiveEnabledTrackColor = Color(0xFF000003); @@ -2152,7 +2163,7 @@ void main() { ); }); - testWidgets('Material3 - Track color resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Track color resolves in active/enabled states', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); const Color activeEnabledTrackColor = Color(0xFF000001); const Color activeDisabledTrackColor = Color(0xFF000002); @@ -2245,7 +2256,7 @@ void main() { ); }); - testWidgets('Material2 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredTrackColor = Color(0xFF000001); @@ -2312,9 +2323,11 @@ void main() { ), reason: 'Inactive enabled switch should match these colors', ); + + focusNode.dispose(); }); - testWidgets('Material3 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -2382,9 +2395,11 @@ void main() { ), reason: 'Active enabled switch should match these colors', ); + + focusNode.dispose(); }); - testWidgets('Material2 - Switch thumb color is blended against surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch thumb color is blended against surface color', (WidgetTester tester) async { final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60); final ThemeData theme = ThemeData.light(useMaterial3: false); @@ -2435,7 +2450,7 @@ void main() { ); }); - testWidgets('Material3 - Switch thumb color is blended against surface color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch thumb color is blended against surface color', (WidgetTester tester) async { final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60); final ThemeData theme = ThemeData(useMaterial3: true); final ColorScheme colors = theme.colorScheme; @@ -2485,7 +2500,7 @@ void main() { ); }); - testWidgets('Switch overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -2634,9 +2649,11 @@ void main() { ), reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor', ); + + focusNode.dispose(); }); - testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { Widget buildSwitch(bool show) { return MaterialApp( theme: theme, @@ -2660,7 +2677,7 @@ void main() { await gesture.up(); }); - testWidgets('disabled switch shows tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled switch shows tooltip', (WidgetTester tester) async { const String longPressTooltip = 'long press tooltip'; const String tapTooltip = 'tap tooltip'; await tester.pumpWidget( @@ -2727,7 +2744,7 @@ void main() { image = await createTestImage(width: 100, height: 100); }); - testWidgets('thumb image shows up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('thumb image shows up', (WidgetTester tester) async { imageCache.clear(); final _TestImageProvider provider1 = _TestImageProvider(); final _TestImageProvider provider2 = _TestImageProvider(); @@ -2768,7 +2785,7 @@ void main() { expect(imageCache.liveImageCount, 2); }); - testWidgets('do not crash when imageProvider completes after Switch is disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('do not crash when imageProvider completes after Switch is disposed', (WidgetTester tester) async { final DelayedImageProvider imageProvider = DelayedImageProvider(image); await tester.pumpWidget( @@ -2796,7 +2813,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('do not crash when previous imageProvider completes after Switch is disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('do not crash when previous imageProvider completes after Switch is disposed', (WidgetTester tester) async { final DelayedImageProvider imageProvider1 = DelayedImageProvider(image); final DelayedImageProvider imageProvider2 = DelayedImageProvider(image); @@ -2837,7 +2854,7 @@ void main() { }); group('Switch M3 only tests', () { - testWidgets('M3 Switch has a 300-millisecond animation in total', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 Switch has a 300-millisecond animation in total', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); bool value = false; await tester.pumpWidget( @@ -2877,7 +2894,7 @@ void main() { expect(tester.hasRunningAnimations, false); }); - testWidgets('M3 Switch has a stadium shape in the middle of the track', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 Switch has a stadium shape in the middle of the track', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true, colorSchemeSeed: Colors.deepPurple); bool value = false; await tester.pumpWidget( @@ -2922,7 +2939,7 @@ void main() { ); }); - testWidgets('M3 Switch thumb bounces in the end of the animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 Switch thumb bounces in the end of the animation', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); bool value = false; await tester.pumpWidget( @@ -2971,7 +2988,7 @@ void main() { expect(state.position.value, greaterThan(1)); }); - testWidgets('Switch thumb shows correct pressed color - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch thumb shows correct pressed color - M3', (WidgetTester tester) async { final ThemeData themeData = ThemeData(useMaterial3: true); final ColorScheme colors = themeData.colorScheme; Widget buildApp({bool enabled = true, bool value = true}) { @@ -3054,7 +3071,7 @@ void main() { ); }, variant: TargetPlatformVariant.mobile()); - testWidgets('Track outline color resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Track outline color resolves in active/enabled states', (WidgetTester tester) async { const Color activeEnabledTrackOutlineColor = Color(0xFF000001); const Color activeDisabledTrackOutlineColor = Color(0xFF000002); const Color inactiveEnabledTrackOutlineColor = Color(0xFF000003); @@ -3130,7 +3147,7 @@ void main() { ); }); - testWidgets('Switch track outline color resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch track outline color resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color hoveredTrackOutlineColor = Color(0xFF000001); @@ -3187,9 +3204,11 @@ void main() { ..rrect(color: hoveredTrackOutlineColor, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline should match this color', ); + + focusNode.dispose(); }); - testWidgets('Track outline width resolves in active/enabled states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Track outline width resolves in active/enabled states', (WidgetTester tester) async { const double activeEnabledTrackOutlineWidth = 1.0; const double activeDisabledTrackOutlineWidth = 2.0; const double inactiveEnabledTrackOutlineWidth = 3.0; @@ -3265,7 +3284,7 @@ void main() { ); }); - testWidgets('Switch track outline width resolves in hovered/focused states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch track outline width resolves in hovered/focused states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double hoveredTrackOutlineWidth = 4.0; @@ -3322,9 +3341,11 @@ void main() { ..rrect(strokeWidth: hoveredTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline width should be 4.0', ); + + focusNode.dispose(); }); - testWidgets('Switch can set icon - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch can set icon - M3', (WidgetTester tester) async { final ThemeData themeData = ThemeData( useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), @@ -3405,7 +3426,7 @@ void main() { }); }); - testWidgets('Switch.adaptive(Cupertino) is focusable and has correct focus color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch.adaptive(Cupertino) is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch.adaptive'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool value = true; @@ -3486,9 +3507,11 @@ void main() { ..rrect(color: const Color(0x0a000000)) ..rrect(color: const Color(0xffffffff)), ); + + focusNode.dispose(); }); - testWidgets('Switch.onFocusChange callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switch.onFocusChange callback', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Switch'); bool focused = false; await tester.pumpWidget(MaterialApp( @@ -3515,6 +3538,8 @@ void main() { await tester.pump(); expect(focused, isFalse); expect(focusNode.hasFocus, isFalse); + + focusNode.dispose(); }); } @@ -3531,7 +3556,7 @@ class DelayedImageProvider extends ImageProvider<DelayedImageProvider> { } @override - ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(DelayedImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter(_completer.future); } @@ -3567,7 +3592,7 @@ class _TestImageProvider extends ImageProvider<Object> { } @override - ImageStreamCompleter load(Object key, DecoderCallback decode) { + ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) { _loadCallCount += 1; return _streamCompleter; } diff --git a/packages/flutter/test/material/switch_theme_test.dart b/packages/flutter/test/material/switch_theme_test.dart index 159defb60b811..720c103da491b 100644 --- a/packages/flutter/test/material/switch_theme_test.dart +++ b/packages/flutter/test/material/switch_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('SwitchThemeData copyWith, ==, hashCode basics', () { @@ -44,7 +43,7 @@ void main() { expect(theme.data.thumbIcon, null); }); - testWidgets('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SwitchThemeData().debugFillProperties(builder); @@ -56,7 +55,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('SwitchThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SwitchThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SwitchThemeData( thumbColor: MaterialStatePropertyAll<Color>(Color(0xfffffff0)), @@ -86,7 +85,7 @@ void main() { expect(description[8], 'thumbIcon: MaterialStatePropertyAll(Icon(IconData(U+0007B)))'); }); - testWidgets('Material2 - Switch is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch is themeable', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color defaultThumbColor = Color(0xfffffff0); @@ -209,7 +208,7 @@ void main() { expect(_getSwitchMaterial(tester), paints..circle(color: focusOverlayColor, radius: splashRadius)); }); - testWidgets('Material3 - Switch is themeable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch is themeable', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color defaultThumbColor = Color(0xfffffff0); @@ -327,7 +326,7 @@ void main() { expect(_getSwitchMaterial(tester), paints..circle(color: focusOverlayColor, radius: splashRadius)); }); - testWidgets('Material2 - Switch properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color themeDefaultThumbColor = Color(0xfffffff0); @@ -497,7 +496,7 @@ void main() { expect(_getSwitchMaterial(tester), paints..circle(color: focusColor, radius: splashRadius)); }); - testWidgets('Material3 - Switch properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color themeDefaultThumbColor = Color(0xfffffff0); @@ -660,7 +659,7 @@ void main() { expect(_getSwitchMaterial(tester), paints..circle(color: focusColor, radius: splashRadius)); }); - testWidgets('Material2 - Switch active and inactive properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch active and inactive properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color themeDefaultThumbColor = Color(0xfffffff0); @@ -735,7 +734,7 @@ void main() { ); }); - testWidgets('Material3 - Switch active and inactive properties are taken over the theme values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch active and inactive properties are taken over the theme values', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color themeDefaultThumbColor = Color(0xfffffff0); @@ -806,7 +805,7 @@ void main() { ); }); - testWidgets('Material2 - Switch theme overlay color resolves in active/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Switch theme overlay color resolves in active/pressed states', (WidgetTester tester) async { const Color activePressedOverlayColor = Color(0xFF000001); const Color inactivePressedOverlayColor = Color(0xFF000002); @@ -871,7 +870,7 @@ void main() { ); }); - testWidgets('Material3 - Switch theme overlay color resolves in active/pressed states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Switch theme overlay color resolves in active/pressed states', (WidgetTester tester) async { const Color activePressedOverlayColor = Color(0xFF000001); const Color inactivePressedOverlayColor = Color(0xFF000002); @@ -937,7 +936,7 @@ void main() { ); }); - testWidgets('Material2 - Local SwitchTheme can override global SwitchTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Local SwitchTheme can override global SwitchTheme', (WidgetTester tester) async { const Color globalThemeThumbColor = Color(0xfffffff1); const Color globalThemeTrackColor = Color(0xfffffff2); const Color globalThemeOutlineColor = Color(0xfffffff3); @@ -991,7 +990,7 @@ void main() { ); }); - testWidgets('Material3 - Local SwitchTheme can override global SwitchTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Local SwitchTheme can override global SwitchTheme', (WidgetTester tester) async { const Color globalThemeThumbColor = Color(0xfffffff1); const Color globalThemeTrackColor = Color(0xfffffff2); const Color globalThemeOutlineColor = Color(0xfffffff3); diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart index 4b8c5a924399e..2fec5877bbbe0 100644 --- a/packages/flutter/test/material/tab_bar_theme_test.dart +++ b/packages/flutter/test/material/tab_bar_theme_test.dart @@ -12,8 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - const String _tab1Text = 'tab 1'; const String _tab2Text = 'tab 2'; const String _tab3Text = 'tab 3'; @@ -88,6 +86,7 @@ void main() { expect(const TabBarTheme().indicatorColor, null); expect(const TabBarTheme().indicatorSize, null); expect(const TabBarTheme().dividerColor, null); + expect(const TabBarTheme().dividerHeight, null); expect(const TabBarTheme().labelColor, null); expect(const TabBarTheme().labelPadding, null); expect(const TabBarTheme().labelStyle, null); @@ -125,27 +124,32 @@ void main() { final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); + const double tabStartOffset = 52.0; // Verify tabOne coordinates. - expect(tabOneRect.left, equals(kTabLabelPadding.left)); + expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset)); expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabTwo coordinates. - expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right)); + final double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width + + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, tabTwoRight); expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); - // Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo. + // Verify tabOne and tabTwo are separated by right padding of tabOne and left padding of tabTwo. expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right)); - // Test default indicator color and divider color. + // Test default indicator & divider color. final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); expect( tabBarBox, paints - ..line(color: theme.colorScheme.surfaceVariant) - // Indicator is a rrect in the primary tab bar. + ..line( + color: theme.colorScheme.surfaceVariant, + strokeWidth: 1.0, + ) ..rrect(color: theme.colorScheme.primary), ); }); @@ -176,29 +180,34 @@ void main() { final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); + const double tabStartOffset = 52.0; // Verify tabOne coordinates. - expect(tabOneRect.left, equals(kTabLabelPadding.left)); + expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset)); expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabTwo coordinates. - expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right)); + final double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width + + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, tabTwoRight); expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); - // Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo. + // Verify tabOne and tabTwo are separated by right padding of tabOne and left padding of tabTwo. expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right)); - // Test default indicator color and divider color. + // Test default indicator & divider color. final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); expect( tabBarBox, paints - ..line(color: theme.colorScheme.surfaceVariant) - // Indicator is a line in the secondary tab bar. + ..line( + color: theme.colorScheme.surfaceVariant, + strokeWidth: 1.0, + ) ..line(color: theme.colorScheme.primary), - ); + ); }); testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async { @@ -313,7 +322,7 @@ void main() { expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily)); }); - testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async { + testWidgets('Material2 - Tab bar label padding overrides theme label padding', (WidgetTester tester) async { const double verticalPadding = 10.0; const double horizontalPadding = 10.0; const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric( @@ -334,7 +343,7 @@ void main() { await tester.pumpWidget( MaterialApp( - theme: ThemeData(tabBarTheme: tabBarTheme), + theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: false), home: Scaffold(body: RepaintBoundary( key: _painterKey, @@ -367,6 +376,61 @@ void main() { expect(tabOneRect.right, equals(tabTwoRect.left - (2 * horizontalPadding))); }); + testWidgets('Material3 - Tab bar label padding overrides theme label padding', (WidgetTester tester) async { + const double tabStartOffset = 52.0; + const double verticalPadding = 10.0; + const double horizontalPadding = 10.0; + const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ); + + const double verticalThemePadding = 20.0; + const double horizontalThemePadding = 20.0; + const EdgeInsetsGeometry themeLabelPadding = EdgeInsets.symmetric( + vertical: verticalThemePadding, + horizontal: horizontalThemePadding, + ); + + const double indicatorWeight = 2.0; // default value + + const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: themeLabelPadding); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: true), + home: Scaffold(body: + RepaintBoundary( + key: _painterKey, + child: TabBar( + tabs: _sizedTabs, + isScrollable: true, + controller: TabController(length: _sizedTabs.length, vsync: const TestVSync()), + labelPadding: labelPadding, + ), + ), + ), + ), + ); + + final Rect tabBar = tester.getRect(find.byType(TabBar)); + final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); + final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); + + // verify coordinates of tabOne + expect(tabOneRect.left, equals(horizontalPadding + tabStartOffset)); + expect(tabOneRect.top, equals(verticalPadding)); + expect(tabOneRect.bottom, equals(tabBar.bottom - verticalPadding - indicatorWeight)); + + // verify coordinates of tabTwo + expect(tabTwoRect.right, equals(tabStartOffset + horizontalThemePadding + tabOneRect.width + tabTwoRect.width + (horizontalThemePadding / 2))); + expect(tabTwoRect.top, equals(verticalPadding)); + expect(tabTwoRect.bottom, equals(tabBar.bottom - verticalPadding - indicatorWeight)); + + // verify tabOne and tabTwo are separated by 2x horizontalPadding + expect(tabOneRect.right, equals(tabTwoRect.left - (2 * horizontalPadding))); + }); + testWidgets('Tab bar theme overrides label color (unselected)', (WidgetTester tester) async { const Color unselectedLabelColor = Colors.black; const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor); @@ -379,7 +443,7 @@ void main() { expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor)); }); - testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { + testWidgets('Tab bar default tab indicator size (primary)', (WidgetTester tester) async { await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true)); await expectLater( @@ -388,12 +452,12 @@ void main() { ); }); - testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { + testWidgets('Tab bar default tab indicator size (secondary)', (WidgetTester tester) async { await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true)); await expectLater( find.byKey(_painterKey), - matchesGoldenFile('tab_bar.default.tab_indicator_size.png'), + matchesGoldenFile('tab_bar_secondary.default.tab_indicator_size.png'), ); }); @@ -547,11 +611,12 @@ void main() { expect( tabBarBox, paints - // Divider + // Divider. ..line( color: theme.colorScheme.surfaceVariant, + strokeWidth: 1.0, ) - // Tab indicator + // Tab indicator. ..line( color: theme.colorScheme.primary, strokeWidth: indicatorWeight, @@ -599,18 +664,389 @@ void main() { expect( tabBarBox, paints - // Divider + // Divider. ..line( color: theme.colorScheme.surfaceVariant, + strokeWidth: 1.0, ) // Tab indicator ..line( color: theme.colorScheme.primary, strokeWidth: indicatorWeight, - p1: const Offset(bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 65.75 : 65.5, indicatorY), - p2: const Offset(bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? 134.25 : 134.5, indicatorY), + p1: const Offset(65.75, indicatorY), + p2: const Offset(134.25, indicatorY), + ), + ); + }); + + testWidgets('TabBar divider can use TabBarTheme.dividerColor & TabBarTheme.dividerHeight', (WidgetTester tester) async { + const Color dividerColor = Color(0xff00ff00); + const double dividerHeight = 10.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme( + dividerColor: dividerColor, + dividerHeight: dividerHeight, + ), + useMaterial3: true, ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: TabController(length: 3, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + // Test divider color. + expect(tabBarBox, paints..line(color: dividerColor, strokeWidth: dividerHeight)); + }); + + testWidgets('dividerColor & dividerHeight overrides TabBarTheme.dividerColor', (WidgetTester tester) async { + const Color dividerColor = Color(0xff0000ff); + const double dividerHeight = 8.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme( + dividerColor: Colors.pink, + dividerHeight: 5.0, + ), + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + dividerColor: dividerColor, + dividerHeight: dividerHeight, + controller: TabController(length: 3, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + // Test divider color. + expect(tabBarBox, paints..line(color: dividerColor, strokeWidth: dividerHeight)); + }); + + testWidgets('TabBar respects TabBarTheme.tabAlignment', (WidgetTester tester) async { + // Test non-scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + const double availableWidth = 800.0; + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + double tabOneLeft = (availableWidth / 2) - tabOneRect.width - kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + double tabTwoRight = (availableWidth / 2) + tabTwoRect.width + kTabLabelPadding.right; + expect(tabTwoRect.right, equals(tabTwoRight)); + + // Test scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.start), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + isScrollable: true, + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + tabOneLeft = kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, equals(tabTwoRight)); + }); + + testWidgets('TabBar.tabAlignment overrides TabBarTheme.tabAlignment', (WidgetTester tester) async { + /// Test non-scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.fill), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + tabAlignment: TabAlignment.center, + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + const double availableWidth = 800.0; + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + double tabOneLeft = (availableWidth / 2) - tabOneRect.width - kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + double tabTwoRight = (availableWidth / 2) + tabTwoRect.width + kTabLabelPadding.right; + expect(tabTwoRect.right, equals(tabTwoRight)); + + /// Test scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), ); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + tabOneLeft = kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, equals(tabTwoRight)); + }); + + testWidgets( + 'TabBar labels use colors from TabBarTheme.labelStyle & TabBarTheme.unselectedLabelStyle', + (WidgetTester tester) async { + const TextStyle labelStyle = TextStyle( + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + const TextStyle unselectedLabelStyle = TextStyle( + color: Color(0x950000ff), + fontStyle: FontStyle.italic, + ); + const TabBarTheme tabBarTheme = TabBarTheme( + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + ); + + // Test tab bar with TabBarTheme labelStyle & unselectedLabelStyle. + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + + final IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + final IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + final TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)) + .text.style!; + final TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)) + .text.style!; + + // Selected tab should use labelStyle color. + expect(selectedTabIcon.color, labelStyle.color); + expect(selectedTextStyle.color, labelStyle.color); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use unselectedLabelStyle color. + expect(uselectedTabIcon.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + }); + + testWidgets( + "TabBarTheme's labelColor & unselectedLabelColor override labelStyle & unselectedLabelStyle colors", + (WidgetTester tester) async { + const Color labelColor = Color(0xfff00000); + const Color unselectedLabelColor = Color(0x95ff0000); + const TextStyle labelStyle = TextStyle( + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + const TextStyle unselectedLabelStyle = TextStyle( + color: Color(0x950000ff), + fontStyle: FontStyle.italic, + ); + TabBarTheme tabBarTheme = const TabBarTheme( + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + ); + + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + + // Test tab bar with TabBarTheme labelStyle & unselectedLabelStyle. + await tester.pumpWidget(buildTabBar()); + + IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)) + .text.style!; + TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)) + .text.style!; + + // Selected tab should use the labelStyle color. + expect(selectedTabIcon.color, labelStyle.color); + expect(selectedTextStyle.color, labelStyle.color); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the unselectedLabelStyle color. + expect(uselectedTabIcon.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + + // Update the TabBarTheme with labelColor & unselectedLabelColor. + tabBarTheme = const TabBarTheme( + labelColor: labelColor, + unselectedLabelColor: unselectedLabelColor, + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + ); + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + await tester.pumpAndSettle(); + + selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)).text.style!; + unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)).text.style!; + + // Selected tab should use the labelColor. + expect(selectedTabIcon.color, labelColor); + expect(selectedTextStyle.color, labelColor); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the unselectedLabelColor. + expect(uselectedTabIcon.color, unselectedLabelColor); + expect(unselectedTextStyle.color, unselectedLabelColor); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + }); + + testWidgets( + "TabBarTheme's labelColor & unselectedLabelColor override TabBar.labelStyle & TabBar.unselectedLabelStyle colors", + (WidgetTester tester) async { + const Color labelColor = Color(0xfff00000); + const Color unselectedLabelColor = Color(0x95ff0000); + const TextStyle labelStyle = TextStyle( + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + const TextStyle unselectedLabelStyle = TextStyle( + color: Color(0x950000ff), + fontStyle: FontStyle.italic, + ); + + Widget buildTabBar({TabBarTheme? tabBarTheme}) { + return MaterialApp( + theme: ThemeData(tabBarTheme: tabBarTheme), + home: const Material( + child: DefaultTabController( + length: 2, + child: TabBar( + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + tabs: <Widget>[ + Tab(text: _tab1Text), + Tab(text: _tab2Text), + ], + ), + ), + ), + ); + } + + // Test tab bar with [TabBar.labeStyle] & [TabBar.unselectedLabelStyle]. + await tester.pumpWidget(buildTabBar()); + + IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)) + .text.style!; + TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)) + .text.style!; + + // Selected tab should use the [TabBar.labelStyle] color. + expect(selectedTabIcon.color, labelStyle.color); + expect(selectedTextStyle.color, labelStyle.color); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the [TabBar.unselectedLabelStyle] color. + expect(uselectedTabIcon.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + + // Add TabBarTheme with labelColor & unselectedLabelColor. + await tester.pumpWidget(buildTabBar(tabBarTheme: const TabBarTheme( + labelColor: labelColor, + unselectedLabelColor: unselectedLabelColor, + ))); + await tester.pumpAndSettle(); + + selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)).text.style!; + unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)).text.style!; + + // Selected tab should use the [TabBarTheme.labelColor]. + expect(selectedTabIcon.color, labelColor); + expect(selectedTextStyle.color, labelColor); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the [TabBarTheme.unselectedLabelColor]. + expect(uselectedTabIcon.color, unselectedLabelColor); + expect(unselectedTextStyle.color, unselectedLabelColor); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); }); group('Material 2', () { @@ -690,7 +1126,7 @@ void main() { expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); - // Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo. + // Verify tabOne and tabTwo are separated by right padding of tabOne and left padding of tabTwo. expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right)); // Test default indicator color. @@ -804,5 +1240,227 @@ void main() { ), ); }); + + testWidgets('TabBar respects TabBarTheme.tabAlignment', (WidgetTester tester) async { + // Test non-scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center), + useMaterial3: false, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final Rect tabOneRect = tester.getRect(find.byType(Tab).first); + final Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + final double tabOneLeft = (800 / 2) - tabOneRect.width - kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + final double tabTwoRight = (800 / 2) + tabTwoRect.width + kTabLabelPadding.right; + expect(tabTwoRect.right, equals(tabTwoRight)); + }); + + testWidgets('TabBar.tabAlignment overrides TabBarTheme.tabAlignment', (WidgetTester tester) async { + // Test non-scrollable tab bar. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.fill), + useMaterial3: false, + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + tabAlignment: TabAlignment.center, + controller: TabController(length: 2, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final Rect tabOneRect = tester.getRect(find.byType(Tab).first); + final Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + final double tabOneLeft = (800 / 2) - tabOneRect.width - kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + final double tabTwoRight = (800 / 2) + tabTwoRect.width + kTabLabelPadding.right; + expect(tabTwoRect.right, equals(tabTwoRight)); + }); + }); + + testWidgets('Material3 - TabBar indicator respects TabBarTheme.indicatorColor', (WidgetTester tester) async { + final List<Widget> tabs = List<Widget>.generate(4, (int index) { + return Tab(text: 'Tab $index'); + }); + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + const Color tabBarThemeIndicatorColor = Color(0xffff0000); + + Widget buildTabBar({ required ThemeData theme }) { + return MaterialApp( + theme: theme, + home: Material( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildTabBar(theme: ThemeData(useMaterial3: true))); + + RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + expect(tabBarBox,paints..rrect(color: ThemeData(useMaterial3: true).colorScheme.primary)); + + await tester.pumpWidget(buildTabBar(theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme(indicatorColor: tabBarThemeIndicatorColor) + ))); + await tester.pumpAndSettle(); + + tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + expect(tabBarBox,paints..rrect(color: tabBarThemeIndicatorColor)); + }); + + testWidgets('Material2 - TabBar indicator respects TabBarTheme.indicatorColor', (WidgetTester tester) async { + final List<Widget> tabs = List<Widget>.generate(4, (int index) { + return Tab(text: 'Tab $index'); + }); + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + const Color themeIndicatorColor = Color(0xffff0000); + const Color tabBarThemeIndicatorColor = Color(0xffffff00); + + Widget buildTabBar({ Color? themeIndicatorColor, Color? tabBarThemeIndicatorColor }) { + return MaterialApp( + theme: ThemeData( + indicatorColor: themeIndicatorColor, + tabBarTheme: TabBarTheme(indicatorColor: tabBarThemeIndicatorColor), + useMaterial3: false, + ), + home: Material( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildTabBar(themeIndicatorColor: themeIndicatorColor)); + + RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + expect(tabBarBox,paints..line(color: themeIndicatorColor)); + + await tester.pumpWidget(buildTabBar(tabBarThemeIndicatorColor: tabBarThemeIndicatorColor)); + await tester.pumpAndSettle(); + + tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + expect(tabBarBox,paints..line(color: tabBarThemeIndicatorColor)); + }); + + testWidgets('TabBarTheme.labelColor resolves material states', (WidgetTester tester) async { + const Color selectedColor = Color(0xff00ff00); + const Color unselectedColor = Color(0xffff0000); + final MaterialStateColor labelColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return unselectedColor; + }); + + final TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor); + + // Test labelColor correctly resolves material states. + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + + final IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + final IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + final TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)).text.style!; + final TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)).text.style!; + + expect(selectedTabIcon.color, selectedColor); + expect(uselectedTabIcon.color, unselectedColor); + expect(selectedTextStyle.color, selectedColor); + expect(unselectedTextStyle.color, unselectedColor); + }); + + testWidgets('TabBarTheme.labelColor & TabBarTheme.unselectedLabelColor override material state TabBarTheme.labelColor', + (WidgetTester tester) async { + const Color selectedStateColor = Color(0xff00ff00); + const Color unselectedStateColor = Color(0xffff0000); + final MaterialStateColor labelColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return selectedStateColor; + } + return unselectedStateColor; + }); + const Color selectedColor = Color(0xff00ffff); + const Color unselectedColor = Color(0xffff12ff); + + TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor); + + // Test material state label color. + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + + IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)).text.style!; + TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)).text.style!; + + expect(selectedTabIcon.color, selectedStateColor); + expect(uselectedTabIcon.color, unselectedStateColor); + expect(selectedTextStyle.color, selectedStateColor); + expect(unselectedTextStyle.color, unselectedStateColor); + + // Test labelColor & unselectedLabelColor override material state labelColor. + tabBarTheme = const TabBarTheme( + labelColor: selectedColor, + unselectedLabelColor: unselectedColor, + ); + await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme)); + await tester.pumpAndSettle(); + + selectedTabIcon = IconTheme.of(tester.element(find.text(_tab1Text))); + uselectedTabIcon = IconTheme.of(tester.element(find.text(_tab2Text))); + selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab1Text)).text.style!; + unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(_tab2Text)).text.style!; + + expect(selectedTabIcon.color, selectedColor); + expect(uselectedTabIcon.color, unselectedColor); + expect(selectedTextStyle.color, selectedColor); + expect(unselectedTextStyle.color, unselectedColor); }); } diff --git a/packages/flutter/test/material/tab_controller_test.dart b/packages/flutter/test/material/tab_controller_test.dart new file mode 100644 index 0000000000000..50d266e812005 --- /dev/null +++ b/packages/flutter/test/material/tab_controller_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + testWidgetsWithLeakTracking('$TabController dispatches creation in constructor.', (WidgetTester widgetTester) async { + await expectLater( + await memoryEvents(() async => TabController(length: 1, vsync: const TestVSync()).dispose(), TabController), + areCreateAndDispose, + ); + }); +} diff --git a/packages/flutter/test/material/tabbed_scrollview_warp_test.dart b/packages/flutter/test/material/tabbed_scrollview_warp_test.dart index 176b7aee4242a..98e1dd8280b40 100644 --- a/packages/flutter/test/material/tabbed_scrollview_warp_test.dart +++ b/packages/flutter/test/material/tabbed_scrollview_warp_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // This is a regression test for https://github.com/flutter/flutter/issues/10549 // which was failing because _SliverPersistentHeaderElement.visitChildren() @@ -74,7 +75,7 @@ class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin { } void main() { - testWidgets('Tabbed CustomScrollViews, warp from tab 1 to 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tabbed CustomScrollViews, warp from tab 1 to 3', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: MyHomePage())); // should not crash. diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index c2194170fe01f..bc26ae7ff1a55 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -7,9 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; -import '../rendering/recording_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -273,12 +272,18 @@ RenderParagraph _getText(WidgetTester tester, String text) { return tester.renderObject<RenderParagraph>(find.text(text)); } +TabController _tabController({required int length, required TickerProvider vsync, int initialIndex = 0, Duration? animationDuration}) { + final TabController result = TabController(length: length, vsync: vsync, initialIndex: initialIndex, animationDuration: animationDuration); + addTearDown(result.dispose); + return result; +} + void main() { setUp(() { debugResetSemanticsIdCounter(); }); - testWidgets('indicatorPadding update test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('indicatorPadding update test', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/108102 const Tab tab = Tab(text: 'A'); const EdgeInsets indicatorPadding = EdgeInsets.only(left: 7.0, right: 7.0); @@ -307,41 +312,46 @@ void main() { expect(tester.renderObject(find.byType(CustomPaint)).debugNeedsPaint, true); }); - testWidgets('Tab sizing - icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - icon', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0))))), ); expect(tester.getSize(find.byType(Tab)), const Size(10.0, 46.0)); }); - testWidgets('Tab sizing - child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - child', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: Center(child: Material(child: Tab(child: SizedBox(width: 10.0, height: 10.0))))), ); expect(tester.getSize(find.byType(Tab)), const Size(10.0, 46.0)); }); - testWidgets('Tab sizing - text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - text', (WidgetTester tester) async { final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); final bool material3 = theme.useMaterial3; await tester.pumpWidget( MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(text: 'x')))), ); expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); - expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 46.0) : const Size(14.0, 46.0)); + expect( + tester.getSize(find.byType(Tab)), + material3 ? const Size(14.25, 46.0) : const Size(14.0, 46.0), + ); }); - testWidgets('Tab sizing - icon and text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - icon and text', (WidgetTester tester) async { final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); final bool material3 = theme.useMaterial3; await tester.pumpWidget( MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0), text: 'x')))), ); expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); - expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 72.0) : const Size(14.0, 72.0)); + expect( + tester.getSize(find.byType(Tab)), + material3 ? const Size(14.25, 72.0) : const Size(14.0, 72.0)); }); - testWidgets('Tab sizing - icon, iconMargin and text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - icon, iconMargin and text', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(fontFamily: 'FlutterTest'), @@ -365,47 +375,49 @@ void main() { expect(tester.getSize(find.byType(Tab)), const Size(210.0, 72.0)); }); - testWidgets('Tab sizing - icon and child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab sizing - icon and child', (WidgetTester tester) async { final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); final bool material3 = theme.useMaterial3; await tester.pumpWidget( MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0), child: Text('x'))))), ); expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); - expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 72.0) : const Size(14.0, 72.0)); + expect( + tester.getSize(find.byType(Tab)), + material3 ? const Size(14.25, 72.0) : const Size(14.0, 72.0)); }); - testWidgets('Tab color - normal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab color - normal', (WidgetTester tester) async { final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); final bool material3 = theme.useMaterial3; - final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); + final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: _tabController(length: 1, vsync: tester)); await tester.pumpWidget( MaterialApp(theme: theme, home: Material(child: tabBar)), ); expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.blue[500])); }); - testWidgets('Tab color - match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab color - match', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final bool material3 = theme.useMaterial3; - final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); + final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: _tabController(length: 1, vsync: tester)); await tester.pumpWidget( MaterialApp(theme: theme, home: Material(color: const Color(0xff2196f3), child: tabBar)), ); expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.white)); }); - testWidgets('Tab color - transparency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tab color - transparency', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final bool material3 = theme.useMaterial3; - final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); + final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: _tabController(length: 1, vsync: tester)); await tester.pumpWidget( MaterialApp(theme: theme, home: Material(type: MaterialType.transparency, child: tabBar)), ); expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.blue[500])); }); - testWidgets('TabBar default selected/unselected label style (primary)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TabBar default selected/unselected label style (primary)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); final List<String> tabs = <String>['A', 'B', 'C']; @@ -463,7 +475,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -488,24 +500,14 @@ void main() { const double indicatorWeight = 3.0; - - final RRect rrect = const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? RRect.fromLTRBAndCorners( - 64.75, - tabBarBox.size.height - indicatorWeight, - 135.25, - tabBarBox.size.height, - topLeft: const Radius.circular(3.0), - topRight: const Radius.circular(3.0), - ) - : RRect.fromLTRBAndCorners( - 64.5, - tabBarBox.size.height - indicatorWeight, - 135.5, - tabBarBox.size.height, - topLeft: const Radius.circular(3.0), - topRight: const Radius.circular(3.0), - ); + final RRect rrect = RRect.fromLTRBAndCorners( + 64.75, + tabBarBox.size.height - indicatorWeight, + 135.25, + tabBarBox.size.height, + topLeft: const Radius.circular(3.0), + topRight: const Radius.circular(3.0), + ); expect( tabBarBox, @@ -522,7 +524,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -699,10 +701,16 @@ void main() { expect(controller.index, 0); }); - testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async { + testWidgets('Material2 - Scrollable TabBar tap centers selected tab', (WidgetTester tester) async { final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; const Key tabBarKey = Key('TabBar'); - await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey)); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAAAA', + isScrollable: true, + tabBarKey: tabBarKey, + useMaterial3: false, + )); final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); expect(controller, isNotNull); expect(controller.index, 0); @@ -718,12 +726,44 @@ void main() { expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0)); }); - testWidgets('Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async { + testWidgets('Material3 - Scrollable TabBar tap centers selected tab', (WidgetTester tester) async { + final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; + const Key tabBarKey = Key('TabBar'); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAAAA', + isScrollable: true, + tabBarKey: tabBarKey, + useMaterial3: true, + )); + final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); + expect(controller, isNotNull); + expect(controller.index, 0); + + expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0)); + // The center of the FFFFFF item is to the right of the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0)); + + await tester.tap(find.text('FFFFFF')); + await tester.pumpAndSettle(); + expect(controller.index, 5); + // The center of the FFFFFF item is now at the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(452.0, epsilon: 1.0)); + }); + + testWidgets('Material2 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/112776 final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; const Key tabBarKey = Key('TabBar'); const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); - await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey, padding: padding)); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAAAA', + isScrollable: true, + tabBarKey: tabBarKey, + padding: padding, + useMaterial3: false, + )); final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); expect(controller, isNotNull); expect(controller.index, 0); @@ -739,7 +779,35 @@ void main() { expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0)); }); - testWidgets('Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async { + testWidgets('Material3 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/112776 + final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; + const Key tabBarKey = Key('TabBar'); + const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAAAA', + isScrollable: true, + tabBarKey: tabBarKey, + padding: padding, + useMaterial3: true, + )); + final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); + expect(controller, isNotNull); + expect(controller.index, 0); + + expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0)); + // The center of the FFFFFF item is to the right of the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0)); + + await tester.tap(find.text('FFFFFF')); + await tester.pumpAndSettle(); + expect(controller.index, 5); + // The center of the FFFFFF item is now at the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(452.0, epsilon: 1.0)); + }); + + testWidgets('Material2 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/112776 final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; const Key tabBarKey = Key('TabBar'); @@ -751,6 +819,7 @@ void main() { tabBarKey: tabBarKey, padding: padding, textDirection: TextDirection.rtl, + useMaterial3: false, )); final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); expect(controller, isNotNull); @@ -767,10 +836,45 @@ void main() { expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0)); }); - testWidgets('TabBar can be scrolled independent of the selection', (WidgetTester tester) async { + testWidgets('Material3 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/112776 + final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; + const Key tabBarKey = Key('TabBar'); + const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAAAA', + isScrollable: true, + tabBarKey: tabBarKey, + padding: padding, + textDirection: TextDirection.rtl, + useMaterial3: true, + )); + final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA'))); + expect(controller, isNotNull); + expect(controller.index, 0); + + expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0)); + // The center of the FFFFFF item is to the left of the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, lessThan(401.0)); + + await tester.tap(find.text('FFFFFF')); + await tester.pumpAndSettle(); + expect(controller.index, 5); + // The center of the FFFFFF item is now at the TabBar's center + expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(348.0, epsilon: 1.0)); + }); + + testWidgets('Material2 - TabBar can be scrolled independent of the selection', (WidgetTester tester) async { final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL']; const Key tabBarKey = Key('TabBar'); - await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAA', isScrollable: true, tabBarKey: tabBarKey)); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAA', + isScrollable: true, + tabBarKey: tabBarKey, + useMaterial3: false, + )); final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA'))); expect(controller, isNotNull); expect(controller.index, 0); @@ -786,6 +890,31 @@ void main() { expect(controller.index, 0); }); + testWidgets('Material3 - TabBar can be scrolled independent of the selection', (WidgetTester tester) async { + final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL']; + const Key tabBarKey = Key('TabBar'); + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'AAAA', + isScrollable: true, + tabBarKey: tabBarKey, + useMaterial3: true, + )); + final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA'))); + expect(controller, isNotNull); + expect(controller.index, 0); + + // Fling-scroll the TabBar to the left + expect(tester.getCenter(find.text('HHHH')).dx, lessThan(720.0)); + await tester.fling(find.byKey(tabBarKey), const Offset(-200.0, 0.0), 10000.0); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // finish the scroll animation + expect(tester.getCenter(find.text('HHHH')).dx, lessThan(500.0)); + + // Scrolling the TabBar doesn't change the selection + expect(controller.index, 0); + }); + testWidgets('TabBarView maintains state', (WidgetTester tester) async { final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE']; String value = tabs[0]; @@ -1194,7 +1323,7 @@ void main() { }); testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -1231,7 +1360,7 @@ void main() { }); testWidgets('TabBarView page left and right test', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -1322,7 +1451,7 @@ void main() { const Duration animationDuration = Duration(milliseconds: 100); final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: tabs.length, @@ -1372,7 +1501,7 @@ void main() { const Duration animationDuration = Duration(seconds: 2); final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: tabs.length, animationDuration: animationDuration, @@ -1431,7 +1560,7 @@ void main() { const Duration animationDuration = Duration(milliseconds: 100); final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: tabs.length, @@ -1475,7 +1604,7 @@ void main() { const Duration animationDuration = Duration(milliseconds: 100); final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: tabs.length, @@ -1514,6 +1643,54 @@ void main() { expect(pageController.viewportFraction, 1); }); + testWidgets('TabBarView viewportFraction can be updated', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/135557. + final List<String> tabs = <String>['A', 'B', 'C']; + TabController? controller; + + Widget buildFrame(double viewportFraction) { + controller = _tabController( + vsync: const TestVSync(), + length: tabs.length, + initialIndex: 1, + ); + return boilerplate( + child: Column( + children: <Widget>[ + TabBar( + tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), + controller: controller, + ), + SizedBox( + width: 400.0, + height: 400.0, + child: TabBarView( + viewportFraction: viewportFraction, + controller: controller, + children: const <Widget>[ + Center(child: Text('0')), + Center(child: Text('1')), + Center(child: Text('2')), + ], + ), + ), + ], + ), + ); + } + + await tester.pumpWidget(buildFrame(0.8)); + PageView pageView = tester.widget(find.byType(PageView)); + PageController pageController = pageView.controller; + expect(pageController.viewportFraction, 0.8); + + // Rebuild with a different viewport fraction. + await tester.pumpWidget(buildFrame(0.5)); + pageView = tester.widget(find.byType(PageView)); + pageController = pageView.controller; + expect(pageController.viewportFraction, 0.5); + }); + testWidgets('TabBarView has clipBehavior Clip.hardEdge by default', (WidgetTester tester) async { final List<Widget> tabs = <Widget>[const Text('First'), const Text('Second')]; @@ -1653,7 +1830,7 @@ void main() { testWidgets('TabBarView skips animation when disabled in controller', (WidgetTester tester) async { final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: tabs.length, @@ -1698,7 +1875,7 @@ void main() { testWidgets('TabBarView skips animation when disabled in controller - skip tabs', (WidgetTester tester) async { final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: tabs.length, animationDuration: Duration.zero, @@ -1743,7 +1920,7 @@ void main() { testWidgets('TabBarView skips animation when disabled in controller - skip tabs twice', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110970 final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: tabs.length, animationDuration: Duration.zero, @@ -1792,7 +1969,7 @@ void main() { testWidgets('TabBarView skips animation when disabled in controller - skip tabs followed by single tab navigation', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110970 final List<String> tabs = <String>['A', 'B', 'C']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: tabs.length, animationDuration: Duration.zero, @@ -1844,7 +2021,7 @@ void main() { testWidgets('TabBarView skips animation when disabled in controller - two tabs', (WidgetTester tester) async { final List<String> tabs = <String>['A', 'B']; - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: tabs.length, animationDuration: Duration.zero, @@ -1924,7 +2101,7 @@ void main() { // This is a regression test for this patch: // https://github.com/flutter/flutter/pull/9015 - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -2041,7 +2218,7 @@ void main() { testWidgets('TabBarView scrolls end close to a new page', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/9375 - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: 3, @@ -2095,10 +2272,61 @@ void main() { expect(tabController.index, 0); }); + testWidgets('On going TabBarView animation can be interrupted by a new animation', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/132293. + + final List<String> tabs = <String>['A', 'B', 'C']; + final TabController tabController = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + await tester.pumpWidget(boilerplate( + child: Column( + children: <Widget>[ + TabBar( + tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), + controller: tabController, + ), + SizedBox( + width: 400.0, + height: 400.0, + child: TabBarView( + controller: tabController, + children: const <Widget>[ + Center(child: Text('0')), + Center(child: Text('1')), + Center(child: Text('2')), + ], + ), + ), + ], + ), + )); + + // First page is visible. + expect(tabController.index, 0); + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Animate to the second page. + tabController.animateTo(1); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + // Animate back to the first page before the previous animation ends. + tabController.animateTo(0); + await tester.pumpAndSettle(); + + // First page should be visible. + expect(tabController.index, 0); + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + }); + testWidgets('Can switch to non-neighboring tab in nested TabBarView without crashing', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/18756 - final TabController mainTabController = TabController(length: 4, vsync: const TestVSync()); - final TabController nestedTabController = TabController(length: 2, vsync: const TestVSync()); + final TabController mainTabController = _tabController(length: 4, vsync: const TestVSync()); + final TabController nestedTabController = _tabController(length: 2, vsync: const TestVSync()); await tester.pumpWidget( MaterialApp( @@ -2141,7 +2369,7 @@ void main() { testWidgets('TabBarView can warp when child is kept alive and contains ink', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/57662. - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 3, ); @@ -2177,7 +2405,7 @@ void main() { }); testWidgets('TabBarView scrolls end close to a new page with custom physics', (WidgetTester tester) async { - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), initialIndex: 1, length: 3, @@ -2237,7 +2465,7 @@ void main() { return Tab(text: 'TAB #$index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, initialIndex: tabs.length - 1, @@ -2267,7 +2495,7 @@ void main() { return Tab(text: 'TAB #$index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, initialIndex: tabs.length - 1, @@ -2290,7 +2518,7 @@ void main() { // that. Tabs are padded horizontally with kTabLabelPadding. final double tabRight = 800.0 - kTabLabelPadding.right; - expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, tabRight); + expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, moreOrLessEquals(tabRight)); }); testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async { @@ -2303,7 +2531,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2363,7 +2591,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2420,7 +2648,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2492,7 +2720,7 @@ void main() { const double indicatorWeight = 2.0; // the default - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2562,7 +2790,7 @@ void main() { const double indicatorWeight = 2.0; // the default - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2634,7 +2862,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2705,7 +2933,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2778,7 +3006,7 @@ void main() { const Decoration indicator = BoxDecoration(color: indicatorColor); const double indicatorWeight = 2.0; // the default - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2857,7 +3085,7 @@ void main() { const Decoration indicator = BoxDecoration(color: indicatorColor); const double indicatorWeight = 2.0; // the default - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2934,7 +3162,7 @@ void main() { SizedBox(key: UniqueKey(), width: double.infinity, height: 40.0), ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -2979,9 +3207,10 @@ void main() { expect(tabBarBox.size.width, tabRight); }); - testWidgets('TabBar with padding isScrollable: true', (WidgetTester tester) async { + testWidgets('Material3 - TabBar with padding isScrollable: true', (WidgetTester tester) async { const double indicatorWeight = 2.0; // default indicator weight const EdgeInsets padding = EdgeInsets.only(left: 3.0, top: 7.0, right: 5.0, bottom: 3.0); + const double tabStartOffset = 52.0; final List<Widget> tabs = <Widget>[ SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), @@ -2989,7 +3218,7 @@ void main() { SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3006,6 +3235,7 @@ void main() { tabs: tabs, ), ), + useMaterial3: true, ), ); @@ -3014,7 +3244,7 @@ void main() { expect(tabBarBox.size.height, tabBarHeight); // Tab0 width = 130, height = 30 - double tabLeft = padding.left; + double tabLeft = padding.left + tabStartOffset; double tabRight = tabLeft + 130.0; double tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 30.0) / 2.0; double tabBottom = tabTop + 30.0; @@ -3038,7 +3268,7 @@ void main() { expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); tabRight += padding.right; - expect(tabBarBox.size.width, tabRight); + expect(tabBarBox.size.width, tabRight + 320.0); // Right tab + remaining space of the stretched tab bar. }); testWidgets('TabBar with labelPadding', (WidgetTester tester) async { @@ -3052,7 +3282,7 @@ void main() { SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3123,7 +3353,7 @@ void main() { SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3191,7 +3421,7 @@ void main() { SizedBox(key: UniqueKey(), width: 68.0, height: 40.0), ); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3256,7 +3486,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3333,7 +3563,7 @@ void main() { return Tab(text: 'TAB #$index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3407,7 +3637,7 @@ void main() { return Tab(text: 'This is a very wide tab #$index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3456,7 +3686,7 @@ void main() { }); testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 0, ); @@ -3496,7 +3726,7 @@ void main() { }); testWidgets('TabBar etc with one tab', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 1, ); @@ -3551,7 +3781,7 @@ void main() { }); testWidgets('can tap on indicator at very bottom of TabBar to switch tabs', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -3600,7 +3830,7 @@ void main() { ); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -3686,7 +3916,7 @@ void main() { } final List<String> tabs = <String>['A', 'B', 'C']; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, initialIndex: tabs.indexOf('C'), @@ -3810,12 +4040,12 @@ void main() { ); } - final TabController controller1 = TabController( + final TabController controller1 = _tabController( vsync: const TestVSync(), length: 2, ); - final TabController controller2 = TabController( + final TabController controller2 = _tabController( vsync: const TestVSync(), length: 2, ); @@ -3888,12 +4118,12 @@ void main() { ); } - final TabController controller1 = TabController( + final TabController controller1 = _tabController( vsync: const TestVSync(), length: 2, ); - final TabController controller2 = TabController( + final TabController controller2 = _tabController( vsync: const TestVSync(), length: 3, ); @@ -3906,7 +4136,6 @@ void main() { await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0); await tester.pump(const Duration(milliseconds: 10)); // start the fling animation - controller1.dispose(); await tester.pump(const Duration(milliseconds: 10)); await tester.pumpWidget(buildFrame(controller2)); // replace controller @@ -3923,7 +4152,7 @@ void main() { TabController? controller; Widget buildFrame(int length) { - controller = TabController( + controller = _tabController( vsync: const TestVSync(), length: length, initialIndex: length - 1, @@ -3962,11 +4191,11 @@ void main() { testWidgets('Do not throw when switching between a scrollable TabBar and a non-scrollable TabBar', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/120649 - final TabController controller1 = TabController( + final TabController controller1 = _tabController( vsync: const TestVSync(), length: 2, ); - final TabController controller2 = TabController( + final TabController controller2 = _tabController( vsync: const TestVSync(), length: 2, ); @@ -4164,7 +4393,7 @@ void main() { 'Tab3', 'Tab4', ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -4215,7 +4444,7 @@ void main() { 'Tab4', 'Tab5', ]; - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -4267,7 +4496,7 @@ void main() { Tab(text: 'GABBA'), Tab(text: 'HEY'), ]; - final TabController controller = TabController(vsync: const TestVSync(), length: tabs.length); + final TabController controller = _tabController(vsync: const TestVSync(), length: tabs.length); Widget buildTestWidget({double? width, double? height}) { return MaterialApp( @@ -4387,139 +4616,99 @@ void main() { expect(iconTheme.color, equals(selectedTabColor)); }); - testWidgets('TabBar colors labels correctly', (WidgetTester tester) async { - MaterialStateColor buildMSC(Color selectedColor, Color unselectedColor) { - return MaterialStateColor - .resolveWith((Set<MaterialState> states) { - if (states.contains(MaterialState.selected)) { - return selectedColor; - } - return unselectedColor; - }); - } + testWidgets('TabBar.labelColor resolves material states', (WidgetTester tester) async { + const String tab1 = 'Tab 1'; + const String tab2 = 'Tab 2'; - final Color materialLabelColor = buildMSC(const Color(0x00000000), const Color(0x00000001)); - const Color labelColor = Color(0x00000002); - const Color unselectedLabelColor = Color(0x00000003); - - // this is to make sure labelStyles (in TabBar and in TabBarTheme) don't - // affect label's color. for details: https://github.com/flutter/flutter/pull/109541#issuecomment-1294241417 - const TextStyle labelStyle = TextStyle(color: Color(0x00000004)); - const TextStyle unselectedLabelStyle = TextStyle(color: Color(0x00000005)); - - final TabBarTheme materialTabBarTheme = TabBarTheme( - labelColor: buildMSC(const Color(0x00000006), const Color(0x00000007)), - unselectedLabelColor: const Color(0x00000008), - labelStyle: TextStyle(color: buildMSC(const Color(0x00000009), const Color(0x00000010))), - unselectedLabelStyle: const TextStyle(color: Color(0x00000011)), - ); - const TabBarTheme tabBarTheme = TabBarTheme( - labelColor: Color(0x00000012), - unselectedLabelColor: Color(0x00000013), - labelStyle: TextStyle(color: Color(0x00000014)), - unselectedLabelStyle: TextStyle(color: Color(0x00000015)), - ); - const TabBarTheme tabBarThemeWithNullUnselectedLabelColor = TabBarTheme( - labelColor: Color(0x00000016), - labelStyle: TextStyle(color: Color(0x00000017)), - unselectedLabelStyle: TextStyle(color: Color(0x00000018)), - ); - final ThemeData theme = ThemeData(useMaterial3: false); - Widget buildTabBar({ - bool isLabelColorMSC = false, - bool isLabelColorNull = false, - bool isUnselectedLabelColorNull = false, - bool isTabBarThemeMSC = false, - bool isTabBarThemeNull = false, - bool isTabBarThemeUnselectedLabelColorNull = false, - }) { - final TabBarTheme? effectiveTheme = isTabBarThemeNull - ? null : isTabBarThemeUnselectedLabelColorNull - ? tabBarThemeWithNullUnselectedLabelColor - : isTabBarThemeMSC - ? materialTabBarTheme - : tabBarTheme; + const Color selectedColor = Color(0xff00ff00); + const Color unselectedColor = Color(0xffff0000); + final MaterialStateColor labelColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return unselectedColor; + }); + + // Test labelColor correctly resolves material states. + await tester.pumpWidget(boilerplate( + child: DefaultTabController( + length: 2, + child: TabBar( + labelColor: labelColor, + tabs: const <Widget>[ + Text(tab1), + Text(tab2), + ], + ), + ), + )); + + final IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + final IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + final TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + final TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; + + expect(selectedTabIcon.color, selectedColor); + expect(uselectedTabIcon.color, unselectedColor); + expect(selectedTextStyle.color, selectedColor); + expect(unselectedTextStyle.color, unselectedColor); + }); + + testWidgets('labelColor & unselectedLabelColor override material state labelColor', (WidgetTester tester) async { + const String tab1 = 'Tab 1'; + const String tab2 = 'Tab 2'; + + const Color selectedStateColor = Color(0xff00ff00); + const Color unselectedStateColor = Color(0xffff0000); + final MaterialStateColor labelColor = MaterialStateColor.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return selectedStateColor; + } + return unselectedStateColor; + }); + const Color selectedColor = Color(0xff00ffff); + const Color unselectedColor = Color(0xffff12ff); + + Widget buildTabBar({ bool stateColor = true }){ return boilerplate( - child: Theme( - data: theme.copyWith(tabBarTheme: effectiveTheme), - child: DefaultTabController( - length: 2, - child: TabBar( - labelColor: isLabelColorNull ? null : isLabelColorMSC ? materialLabelColor : labelColor, - unselectedLabelColor: isUnselectedLabelColorNull ? null : unselectedLabelColor, - labelStyle: labelStyle, - unselectedLabelStyle: unselectedLabelStyle, - tabs: const <Widget>[Text('1'), Text('2')], - ), - ), + child: DefaultTabController( + length: 2, + child: TabBar( + labelColor: stateColor ? labelColor : selectedColor, + unselectedLabelColor: stateColor ? null : unselectedColor, + tabs: const <Widget>[ + Text(tab1), + Text(tab2), + ], ), - ); + )); } - // Returns int `color.value`s instead of Color `color`s to prevent false - // negative due to object types being different (Color != MaterialStateColor) - // when `expect`ing. - int? getTab1Color() => IconTheme.of(tester.element(find.text('1'))).color?.value; - int? getTab2Color() => IconTheme.of(tester.element(find.text('2'))).color?.value; - int getSelectedColor(Color color) => (color as MaterialStateColor) - .resolve(<MaterialState>{MaterialState.selected}).value; - int getUnselectedColor(Color color) => (color as MaterialStateColor) - .resolve(<MaterialState>{}).value; - - // highest precedence: labelColor as MaterialStateColor - await tester.pumpWidget(buildTabBar(isLabelColorMSC: true)); - expect(getTab1Color(), equals(getSelectedColor(materialLabelColor))); - expect(getTab2Color(), equals(getUnselectedColor(materialLabelColor))); - - // next precedence: labelColor and unselectedLabelColor + // Test material state label color. await tester.pumpWidget(buildTabBar()); - expect(getTab1Color(), equals(labelColor.value)); - expect(getTab2Color(), equals(unselectedLabelColor.value)); - // next precedence: tabBarTheme.labelColor as MaterialStateColor - await tester.pumpWidget(buildTabBar( - isLabelColorNull: true, - isTabBarThemeMSC: true, - )); - expect(getTab1Color(), equals(getSelectedColor(materialTabBarTheme.labelColor!))); - expect(getTab2Color(), equals(getUnselectedColor(materialTabBarTheme.labelColor!))); - - // next precedence: tabBarTheme.labelColor and - // tabBarTheme.unselectedLabelColor - await tester.pumpWidget(buildTabBar( - isLabelColorNull: true, - isUnselectedLabelColorNull: true, - )); - expect(getTab1Color(), equals(tabBarTheme.labelColor!.value)); - expect(getTab2Color(), equals(tabBarTheme.unselectedLabelColor!.value)); + IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; - // next precedence: labelColor and labelColor at 70% opacity - await tester.pumpWidget(buildTabBar( - isUnselectedLabelColorNull: true, - isTabBarThemeUnselectedLabelColorNull: true, - )); - expect(getTab1Color(), equals(labelColor.value)); - expect(getTab2Color(), equals(labelColor.withAlpha(0xB2).value)); - - // next precedence: tabBarTheme.labelColor and tabBarTheme.labelColor at 70% - // opacity - await tester.pumpWidget(buildTabBar( - isLabelColorNull: true, - isUnselectedLabelColorNull: true, - isTabBarThemeUnselectedLabelColorNull: true, - )); - expect(getTab1Color(), equals(tabBarThemeWithNullUnselectedLabelColor.labelColor!.value)); - expect(getTab2Color(), equals(tabBarThemeWithNullUnselectedLabelColor.labelColor!.withAlpha(0xB2).value)); - - // last precedence: themeData.primaryTextTheme.bodyText1.color and - // themeData.primaryTextTheme.bodyText1.color.withAlpha(0xB2) - await tester.pumpWidget(buildTabBar( - isLabelColorNull: true, - isUnselectedLabelColorNull: true, - isTabBarThemeNull: true, - )); - expect(getTab1Color(), equals(theme.primaryTextTheme.bodyText1!.color!.value)); - expect(getTab2Color(), equals(theme.primaryTextTheme.bodyText1!.color!.withAlpha(0xB2).value)); + expect(selectedTabIcon.color, selectedStateColor); + expect(uselectedTabIcon.color, unselectedStateColor); + expect(selectedTextStyle.color, selectedStateColor); + expect(unselectedTextStyle.color, unselectedStateColor); + + // Test labelColor & unselectedLabelColor override material state labelColor. + await tester.pumpWidget(buildTabBar(stateColor: false)); + + selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; + + expect(selectedTabIcon.color, selectedColor); + expect(uselectedTabIcon.color, unselectedColor); + expect(selectedTextStyle.color, selectedColor); + expect(unselectedTextStyle.color, unselectedColor); }); testWidgets('Replacing the tabController after disposing the old one', (WidgetTester tester) async { @@ -4542,7 +4731,7 @@ void main() { onPressed: () { setState(() { controller.dispose(); - controller = TabController(vsync: const TestVSync(), length: 3); + controller = _tabController(vsync: const TestVSync(), length: 3); }); }, ), @@ -4722,19 +4911,19 @@ void main() { testWidgets('TabBar - updating to and from zero tabs', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/68962. final List<String> tabTitles = <String>[]; - TabController tabController = TabController(length: tabTitles.length, vsync: const TestVSync()); + TabController tabController = _tabController(length: tabTitles.length, vsync: const TestVSync()); void onTabAdd(StateSetter setState) { setState(() { tabTitles.add('Tab ${tabTitles.length + 1}'); - tabController = TabController(length: tabTitles.length, vsync: const TestVSync()); + tabController = _tabController(length: tabTitles.length, vsync: const TestVSync()); }); } void onTabRemove(StateSetter setState) { setState(() { tabTitles.removeLast(); - tabController = TabController(length: tabTitles.length, vsync: const TestVSync()); + tabController = _tabController(length: tabTitles.length, vsync: const TestVSync()); }); } @@ -4883,7 +5072,7 @@ void main() { }); testWidgets('TabController.offset changes reflect labelColor', (WidgetTester tester) async { - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -4891,19 +5080,19 @@ void main() { late Color firstColor; late Color secondColor; - Widget buildTabBar({bool labelColorIsMaterialStateColor = false}) { - final Color labelColor = labelColorIsMaterialStateColor - ? MaterialStateColor - .resolveWith((Set<MaterialState> states) { - if (states.contains(MaterialState.selected)) { - return Colors.white; - } else { - // this is a third color to also test if unselectedLabelColor - // is ignored when labelColor is MaterialStateColor - return Colors.transparent; - } - }) - : Colors.white; + Widget buildTabBar({ bool stateColor = false }) { + final Color labelColor = stateColor + ? MaterialStateColor + .resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.selected)) { + return Colors.white; + } else { + // this is a third color to also test if unselectedLabelColor + // is ignored when labelColor is MaterialStateColor + return Colors.transparent; + } + }) + : Colors.white; return boilerplate( child: TabBar( @@ -4963,7 +5152,7 @@ void main() { controller.index = 0; await tester.pump(); - await tester.pumpWidget(buildTabBar(labelColorIsMaterialStateColor: true)); + await tester.pumpWidget(buildTabBar(stateColor: true)); await testLabelColor(selectedColor: Colors.white, unselectedColor: Colors.transparent); }); @@ -4975,7 +5164,7 @@ void main() { }); testWidgets("TabController's animation value should be in sync with TabBarView's scroll value when user interrupts ballistic scroll", (WidgetTester tester) async { - final TabController tabController = TabController( + final TabController tabController = _tabController( vsync: const TestVSync(), length: 3, ); @@ -5205,7 +5394,7 @@ void main() { home: Scaffold( appBar: AppBar( bottom: TabBar( - controller: TabController(length: 3, vsync: const TestVSync()), + controller: _tabController(length: 3, vsync: const TestVSync()), tabs: const <Widget>[ Tab(text: 'Tab 1', icon: Icon(Icons.plus_one)), Tab(text: 'Tab 2'), @@ -5237,7 +5426,7 @@ void main() { appBar: AppBar( bottom: TabBar( labelPadding: labelPadding, - controller: TabController(length: 3, vsync: const TestVSync()), + controller: _tabController(length: 3, vsync: const TestVSync()), tabs: const <Widget>[ Tab(text: 'Tab 1', icon: Icon(Icons.plus_one)), Tab(text: 'Tab 2'), @@ -5271,7 +5460,7 @@ void main() { home: Scaffold( appBar: AppBar( bottom: TabBar( - controller: TabController(length: 3, vsync: const TestVSync()), + controller: _tabController(length: 3, vsync: const TestVSync()), tabs: const <Widget>[ Tab(text: 'Tab 1', icon: Icon(Icons.plus_one)), Tab(text: 'Tab 2'), @@ -5361,7 +5550,7 @@ void main() { testWidgets('Test semantics of TabPageSelector', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: 2, ); @@ -5470,17 +5659,17 @@ void main() { ); } - final TabController controller1 = TabController( + final TabController controller1 = _tabController( vsync: const TestVSync(), length: 3, ); - final TabController controller2 = TabController( + final TabController controller2 = _tabController( vsync: const TestVSync(), length: 2, ); - final TabController controller3 = TabController( + final TabController controller3 = _tabController( vsync: const TestVSync(), length: 3, ); @@ -5542,12 +5731,12 @@ void main() { ); } - final TabController controller1 = TabController( + final TabController controller1 = _tabController( vsync: const TestVSync(), length: 3, ); - final TabController controller2 = TabController( + final TabController controller2 = _tabController( vsync: const TestVSync(), length: 2, ); @@ -5948,15 +6137,12 @@ void main() { ); }); - testWidgets('Default TabAlignment', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); + testWidgets('Material3 - Default TabAlignment', (WidgetTester tester) async { final List<String> tabs = <String>['A', 'B']; + const double tabStartOffset = 52.0; // Test default TabAlignment when isScrollable is false. - await tester.pumpWidget(MaterialApp( - theme: theme, - home: buildFrame(tabs: tabs, value: 'B'), - )); + await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: true)); final Rect tabBar = tester.getRect(find.byType(TabBar)); Rect tabOneRect = tester.getRect(find.byType(Tab).first); @@ -5964,24 +6150,26 @@ void main() { // Tabs should fill the width of the TabBar. double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2; - expect(tabOneRect.left, equals(tabOneLeft)); + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2; - expect(tabTwoRect.right, equals(tabTwoRight)); + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); // Test default TabAlignment when isScrollable is true. - await tester.pumpWidget(MaterialApp( - theme: theme, - home: buildFrame(tabs: tabs, value: 'B', isScrollable: true), + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + useMaterial3: true, )); tabOneRect = tester.getRect(find.byType(Tab).first); tabTwoRect = tester.getRect(find.byType(Tab).last); // Tabs should be aligned to the start of the TabBar. - tabOneLeft = kTabLabelPadding.left; - expect(tabOneRect.left, equals(tabOneLeft)); - tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; - expect(tabTwoRect.right, equals(tabTwoRight)); + tabOneLeft = kTabLabelPadding.left + tabStartOffset; + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabStartOffset + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); }); testWidgets('TabAlignment.fill only supports non-scrollable tab bar', (WidgetTester tester) async { @@ -6042,6 +6230,374 @@ void main() { expect(tester.takeException(), isAssertionError); }); + testWidgets('Material3 - TabAlignment updates tabs alignment (non-scrollable TabBar)', (WidgetTester tester) async { + final List<String> tabs = <String>['A', 'B']; + + // Test TabAlignment.fill (default) when isScrollable is false. + await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: true)); + + const double availableWidth = 800.0; + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + // By defaults tabs should fill the width of the TabBar. + double tabOneLeft = ((availableWidth / 2) - tabOneRect.width) / 2; + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); + double tabTwoRight = availableWidth - ((availableWidth / 2) - tabTwoRect.width) / 2; + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); + + // Test TabAlignment.center when isScrollable is false. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + tabAlignment: TabAlignment.center, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should not fill the width of the TabBar. + tabOneLeft = kTabLabelPadding.left; + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); + }); + + testWidgets('Material3 - TabAlignment updates tabs alignment (scrollable TabBar)', (WidgetTester tester) async { + final List<String> tabs = <String>['A', 'B']; + const double tabStartOffset = 52.0; + + // Test TabAlignment.startOffset (default) when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + useMaterial3: true, + )); + + final Rect tabBar = tester.getRect(find.byType(TabBar)); + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + // By default tabs should be aligned to the start of the TabBar with + // an horizontal offset of 52.0 pixels. + double tabOneLeft = kTabLabelPadding.left + tabStartOffset; + expect(tabOneRect.left, equals(tabOneLeft)); + double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width + + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, equals(tabTwoRight)); + + // Test TabAlignment.start when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + tabAlignment: TabAlignment.start, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be aligned to the start of the TabBar. + tabOneLeft = kTabLabelPadding.left; + expect(tabOneRect.left, equals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, equals(tabTwoRight)); + + // Test TabAlignment.center when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + tabAlignment: TabAlignment.center, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be centered in the TabBar. + tabOneLeft = (tabBar.width / 2) - tabOneRect.width - kTabLabelPadding.right; + expect(tabOneRect.left, equals(tabOneLeft)); + tabTwoRight = (tabBar.width / 2) + tabTwoRect.width + kTabLabelPadding.left; + expect(tabTwoRect.right, equals(tabTwoRight)); + + // Test TabAlignment.startOffset when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + tabAlignment: TabAlignment.startOffset, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be aligned to the start of the TabBar with an + // horizontal offset of 52.0 pixels. + tabOneLeft = kTabLabelPadding.left + tabStartOffset; + expect(tabOneRect.left, equals(tabOneLeft)); + tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width + + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, equals(tabTwoRight)); + }); + + testWidgets('Material3 - TabAlignment.start & TabAlignment.startOffset respects TextDirection.rtl', (WidgetTester tester) async { + final List<String> tabs = <String>['A', 'B']; + const double tabStartOffset = 52.0; + + // Test TabAlignment.startOffset (default) when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + textDirection: TextDirection.rtl, + useMaterial3: true, + )); + + final Rect tabBar = tester.getRect(find.byType(TabBar)); + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be aligned to the start of the TabBar with an + // horizontal offset of 52.0 pixels. + double tabOneRight = tabBar.width - kTabLabelPadding.right - tabStartOffset; + expect(tabOneRect.right, equals(tabOneRight)); + double tabTwoLeft = tabBar.width - tabStartOffset - kTabLabelPadding.horizontal - tabOneRect.width + - kTabLabelPadding.right - tabTwoRect.width; + expect(tabTwoRect.left, equals(tabTwoLeft)); + + // Test TabAlignment.start when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + tabAlignment: TabAlignment.start, + textDirection: TextDirection.rtl, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be aligned to the start of the TabBar. + tabOneRight = tabBar.width - kTabLabelPadding.right; + expect(tabOneRect.right, equals(tabOneRight)); + tabTwoLeft = tabBar.width - kTabLabelPadding.horizontal - tabOneRect.width + - kTabLabelPadding.left - tabTwoRect.width; + expect(tabTwoRect.left, equals(tabTwoLeft)); + + // Test TabAlignment.startOffset when isScrollable is true. + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + tabAlignment: TabAlignment.startOffset, + textDirection: TextDirection.rtl, + useMaterial3: true, + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should be aligned to the start of the TabBar with an + // horizontal offset of 52.0 pixels. + tabOneRight = tabBar.width - kTabLabelPadding.right - tabStartOffset; + expect(tabOneRect.right, equals(tabOneRight)); + tabTwoLeft = tabBar.width - tabStartOffset - kTabLabelPadding.horizontal - tabOneRect.width + - kTabLabelPadding.right - tabTwoRect.width; + expect(tabTwoRect.left, equals(tabTwoLeft)); + }); + + testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async { + const Color dividerColor = Colors.yellow; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme(dividerColor: dividerColor), + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: _tabController(length: 3, vsync: const TestVSync()), + tabs: const <Widget>[ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + // Test painter's divider color. + final CustomPaint paint = tester.widget<CustomPaint>(find.byType(CustomPaint).last); + // ignore: avoid_dynamic_calls + expect((paint.painter as dynamic).dividerColor, dividerColor); + }); + + // This is a regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151. + testWidgets('Divider can be constrained', (WidgetTester tester) async { + const Color dividerColor = Colors.yellow; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme(dividerColor: dividerColor), + ), + home: Scaffold( + body: DefaultTabController( + length: 2, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: ColoredBox( + color: Colors.grey[200]!, + child: const TabBar.secondary( + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: <Widget>[ + Tab(text: 'Test 1'), + Tab(text: 'Test 2'), + ], + ), + ) + ), + ), + ), + ), + ), + ); + + // Test tab bar width. + expect(tester.getSize(find.byType(TabBar)).width, 360); + // Test divider width. + expect(tester.getSize(find.byType(CustomPaint).at(1)).width, 360); + }); + + testWidgets('TabBar labels use colors from labelStyle & unselectedLabelStyle', (WidgetTester tester) async { + const String tab1 = 'Tab 1'; + const String tab2 = 'Tab 2'; + + const TextStyle labelStyle = TextStyle( + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + const TextStyle unselectedLabelStyle = TextStyle( + color: Color(0x950000ff), + fontStyle: FontStyle.italic, + ); + + // Test tab bar with labeStyle & unselectedLabelStyle. + await tester.pumpWidget(boilerplate( + child: const DefaultTabController( + length: 2, + child: TabBar( + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + tabs: <Widget>[ + Tab(text: tab1), + Tab(text: tab2), + ], + ), + ), + )); + + final IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + final IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + final TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + final TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; + + // Selected tab should use the labelStyle color. + expect(selectedTabIcon.color, labelStyle.color); + expect(selectedTextStyle.color, labelStyle.color); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the unselectedLabelStyle color. + expect(uselectedTabIcon.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + }); + + testWidgets('labelColor & unselectedLabelColor override labelStyle & unselectedLabelStyle colors', (WidgetTester tester) async { + const String tab1 = 'Tab 1'; + const String tab2 = 'Tab 2'; + + const Color labelColor = Color(0xfff00000); + const Color unselectedLabelColor = Color(0x95ff0000); + const TextStyle labelStyle = TextStyle( + color: Color(0xff0000ff), + fontStyle: FontStyle.italic, + ); + const TextStyle unselectedLabelStyle = TextStyle( + color: Color(0x950000ff), + fontStyle: FontStyle.italic, + ); + + Widget buildTabBar({ Color? labelColor, Color? unselectedLabelColor }) { + return boilerplate( + child: DefaultTabController( + length: 2, + child: TabBar( + labelColor: labelColor, + unselectedLabelColor: unselectedLabelColor, + labelStyle: labelStyle, + unselectedLabelStyle: unselectedLabelStyle, + tabs: const <Widget>[ + Tab(text: tab1), + Tab(text: tab2), + ], + ), + ), + ); + } + + // Test tab bar with labeStyle & unselectedLabelStyle. + await tester.pumpWidget(buildTabBar()); + + IconThemeData selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + IconThemeData uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + TextStyle unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; + + // Selected tab should use labelStyle color. + expect(selectedTabIcon.color, labelStyle.color); + expect(selectedTextStyle.color, labelStyle.color); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use unselectedLabelStyle color. + expect(uselectedTabIcon.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.color, unselectedLabelStyle.color); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + + // Update tab bar with labelColor & unselectedLabelColor. + await tester.pumpWidget(buildTabBar(labelColor: labelColor, unselectedLabelColor: unselectedLabelColor)); + await tester.pumpAndSettle(); + + selectedTabIcon = IconTheme.of(tester.element(find.text(tab1))); + uselectedTabIcon = IconTheme.of(tester.element(find.text(tab2))); + selectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab1)).text.style!; + unselectedTextStyle = tester.renderObject<RenderParagraph>(find.text(tab2)).text.style!; + + // Selected tab should use the labelColor. + expect(selectedTabIcon.color, labelColor); + expect(selectedTextStyle.color, labelColor); + expect(selectedTextStyle.fontStyle, labelStyle.fontStyle); + // Unselected tab should use the unselectedLabelColor. + expect(uselectedTabIcon.color, unselectedLabelColor); + expect(unselectedTextStyle.color, unselectedLabelColor); + expect(unselectedTextStyle.fontStyle, unselectedLabelStyle.fontStyle); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests @@ -6102,45 +6658,11 @@ void main() { ); }); - testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async { - const Color dividerColor = Colors.yellow; - - await tester.pumpWidget( - MaterialApp( - theme: ThemeData( - useMaterial3: true, - tabBarTheme: const TabBarTheme(dividerColor: dividerColor), - ), - home: Scaffold( - appBar: AppBar( - bottom: TabBar( - controller: TabController(length: 3, vsync: const TestVSync()), - tabs: const <Widget>[ - Tab(text: 'Tab 1'), - Tab(text: 'Tab 2'), - Tab(text: 'Tab 3'), - ], - ), - ), - ), - ), - ); - - // Test painter's divider color. - final CustomPaint paint = tester.widget<CustomPaint>(find.byType(CustomPaint).last); - // ignore: avoid_dynamic_calls - expect((paint.painter as dynamic).dividerColor, dividerColor); - }); - - testWidgets('Default TabAlignment', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: false); + testWidgets('Material2 - Default TabAlignment', (WidgetTester tester) async { final List<String> tabs = <String>['A', 'B']; // Test default TabAlignment when isScrollable is false. - await tester.pumpWidget(MaterialApp( - theme: theme, - home: buildFrame(tabs: tabs, value: 'B'), - )); + await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: false)); final Rect tabBar = tester.getRect(find.byType(TabBar)); Rect tabOneRect = tester.getRect(find.byType(Tab).first); @@ -6153,9 +6675,11 @@ void main() { expect(tabTwoRect.right, equals(tabTwoRight)); // Test default TabAlignment when isScrollable is true. - await tester.pumpWidget(MaterialApp( - theme: theme, - home: buildFrame(tabs: tabs, value: 'B', isScrollable: true), + await tester.pumpWidget(buildFrame( + tabs: tabs, + value: 'B', + isScrollable: true, + useMaterial3: false, )); tabOneRect = tester.getRect(find.byType(Tab).first); @@ -6174,7 +6698,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -6220,7 +6744,7 @@ void main() { return Tab(text: 'Tab $index'); }); - final TabController controller = TabController( + final TabController controller = _tabController( vsync: const TestVSync(), length: tabs.length, ); @@ -6259,6 +6783,106 @@ void main() { ), ); }); + + testWidgets('Material2 - TabBar with padding isScrollable: true', (WidgetTester tester) async { + const double indicatorWeight = 2.0; // default indicator weight + const EdgeInsets padding = EdgeInsets.only(left: 3.0, top: 7.0, right: 5.0, bottom: 3.0); + + final List<Widget> tabs = <Widget>[ + SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), + SizedBox(key: UniqueKey(), width: 140.0, height: 40.0), + SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), + ]; + + final TabController controller = _tabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + padding: padding, + labelPadding: EdgeInsets.zero, + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + useMaterial3: false, + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); + final double tabBarHeight = 50.0 + indicatorWeight + padding.top + padding.bottom; // 50 = max tab height + expect(tabBarBox.size.height, tabBarHeight); + + // Tab0 width = 130, height = 30 + double tabLeft = padding.left; + double tabRight = tabLeft + 130.0; + double tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 30.0) / 2.0; + double tabBottom = tabTop + 30.0; + Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[0].key!)), tabRect); + + // Tab1 width = 140, height = 40 + tabLeft = tabRight; + tabRight = tabLeft + 140.0; + tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 40.0) / 2.0; + tabBottom = tabTop + 40.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect); + + // Tab2 width = 150, height = 50 + tabLeft = tabRight; + tabRight = tabLeft + 150.0; + tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 50.0) / 2.0; + tabBottom = tabTop + 50.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); + + tabRight += padding.right; + expect(tabBarBox.size.width, tabRight); + }); + + testWidgets('Material2 - TabAlignment updates tabs alignment (non-scrollable TabBar)', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); + final List<String> tabs = <String>['A', 'B']; + + // Test TabAlignment.fill (default) when isScrollable is false. + await tester.pumpWidget(MaterialApp( + theme: theme, + home: buildFrame(tabs: tabs, value: 'B'), + )); + + final Rect tabBar = tester.getRect(find.byType(TabBar)); + Rect tabOneRect = tester.getRect(find.byType(Tab).first); + Rect tabTwoRect = tester.getRect(find.byType(Tab).last); + + // By default tabs should fill the width of the TabBar. + double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2; + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); + double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2; + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); + + // Test TabAlignment.center when isScrollable is false. + await tester.pumpWidget(MaterialApp( + theme: theme, + home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.center), + )); + await tester.pumpAndSettle(); + + tabOneRect = tester.getRect(find.byType(Tab).first); + tabTwoRect = tester.getRect(find.byType(Tab).last); + + // Tabs should not fill the width of the TabBar. + tabOneLeft = kTabLabelPadding.left; + expect(tabOneRect.left, moreOrLessEquals(tabOneLeft)); + tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; + expect(tabTwoRect.right, moreOrLessEquals(tabTwoRight)); + }); }); } diff --git a/packages/flutter/test/material/text_button_test.dart b/packages/flutter/test/material/text_button_test.dart index 6e458587fbb2d..56ef76f9bd00c 100644 --- a/packages/flutter/test/material/text_button_test.dart +++ b/packages/flutter/test/material/text_button_test.dart @@ -7,12 +7,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('TextButton, TextButton.icon defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton, TextButton.icon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); final bool material3 = theme.useMaterial3; @@ -156,7 +155,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Default TextButton meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default TextButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -198,11 +197,13 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); await expectLater(tester, meetsGuideline(textContrastGuideline)); + + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('TextButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); Color getTextColor(Set<MaterialState> states) { @@ -265,11 +266,13 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. await expectLater(tester, meetsGuideline(textContrastGuideline)); + + focusNode.dispose(); }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); - testWidgets('TextButton default overlayColor resolves pressed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); @@ -320,9 +323,11 @@ void main() { focusNode.requestFocus(); await tester.pumpAndSettle(); expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.12))); + + focusNode.dispose(); }); - testWidgets('TextButton uses stateful color for text color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton uses stateful color for text color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); const Color pressedColor = Color(0x00000001); @@ -387,9 +392,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(textColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('TextButton uses stateful color for icon color in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton uses stateful color for icon color in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final Key buttonKey = UniqueKey(); @@ -454,9 +461,11 @@ void main() { await tester.pump(); // Start the splash and highlight animations. await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way. expect(iconColor(), pressedColor); + + focusNode.dispose(); }); - testWidgets('TextButton has no clip by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton has no clip by default', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -473,7 +482,7 @@ void main() { ); }); - testWidgets('Does TextButton work with hover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does TextButton work with hover', (WidgetTester tester) async { const Color hoverColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -502,7 +511,7 @@ void main() { expect(inkFeatures, paints..rect(color: hoverColor)); }); - testWidgets('Does TextButton work with focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does TextButton work with focus', (WidgetTester tester) async { const Color focusColor = Color(0xff001122); Color? getOverlayColor(Set<MaterialState> states) { @@ -530,9 +539,11 @@ void main() { final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(color: focusColor)); + + focusNode.dispose(); }); - testWidgets('Does TextButton contribute semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does TextButton contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -577,7 +588,7 @@ void main() { semantics.dispose(); }); - testWidgets('Does TextButton scale with font scale changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does TextButton scale with font scale changes', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -618,12 +629,8 @@ void main() { ), ); - const Size textButtonSize = bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? Size(68.5, 48.0) - : Size(69.0, 48.0); - const Size textSize = bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') - ? Size(52.5, 18.0) - : Size(53.0, 18.0); + const Size textButtonSize = Size(68.5, 48.0); + const Size textSize = Size(52.5, 18.0); expect(tester.getSize(find.byType(TextButton)), textButtonSize); expect(tester.getSize(find.byType(Text)), textSize); @@ -650,7 +657,7 @@ void main() { expect(tester.getSize(find.byType(Text)), const Size(126.0, 42.0)); }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/61016 - testWidgets('TextButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return Theme( data: ThemeData(useMaterial3: false, materialTapTargetSize: tapTargetSize), @@ -677,7 +684,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(66.0, 36.0)); }); - testWidgets('TextButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async { bool wasPressed; Finder textButton; @@ -720,7 +727,7 @@ void main() { expect(tester.widget<TextButton>(textButton).enabled, false); }); - testWidgets('TextButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async { bool didPressButton = false; bool didLongPressButton = false; @@ -751,7 +758,7 @@ void main() { expect(didLongPressButton, isTrue); }); - testWidgets("TextButton response doesn't hover when disabled", (WidgetTester tester) async { + testWidgetsWithLeakTracking("TextButton response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'TextButton Focus'); final GlobalKey childKey = GlobalKey(); @@ -799,9 +806,11 @@ void main() { await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); + + focusNode.dispose(); }); - testWidgets('disabled and hovered TextButton responds to mouse-exit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled and hovered TextButton responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; @@ -863,7 +872,7 @@ void main() { expect(hover, false); }); - testWidgets('Can set TextButton focus and Can set unFocus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set TextButton focus and Can set unFocus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'TextButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -890,9 +899,11 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('When TextButton disable, Can not set TextButton focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When TextButton disable, Can not set TextButton focus.', (WidgetTester tester) async { final FocusNode node = FocusNode(debugLabel: 'TextButton Focus'); bool gotFocus = false; await tester.pumpWidget( @@ -913,9 +924,11 @@ void main() { expect(gotFocus, isFalse); expect(node.hasFocus, isFalse); + + node.dispose(); }); - testWidgets('TextButton responds to density changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key childKey = Key('test child'); @@ -1058,7 +1071,7 @@ void main() { 'RTL', ].join(', '); - testWidgets(testName, (WidgetTester tester) async { + testWidgetsWithLeakTracking(testName, (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -1206,7 +1219,7 @@ void main() { } }); - testWidgets('Override TextButton default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override TextButton default padding', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light()), @@ -1240,7 +1253,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.all(22)); }); - testWidgets('M3 TextButton has correct default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 TextButton has correct default padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1266,7 +1279,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 12,vertical: 8)); }); - testWidgets('M3 TextButton.icon has correct default padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('M3 TextButton.icon has correct default padding', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1293,7 +1306,7 @@ void main() { expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)); }); - testWidgets('Fixed size TextButtons', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size TextButtons', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1326,7 +1339,7 @@ void main() { expect(tester.getSize(find.widgetWithText(TextButton, 'wx200')).height, 200); }); - testWidgets('TextButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton with NoSplash splashFactory paints nothing', (WidgetTester tester) async { Widget buildFrame({ InteractiveInkFeatureFactory? splashFactory }) { return MaterialApp( home: Scaffold( @@ -1366,7 +1379,7 @@ void main() { } }); - testWidgets('TextButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton uses InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -1393,7 +1406,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('TextButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton uses InkRipple when useMaterial3 is false', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( @@ -1415,7 +1428,7 @@ void main() { expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory)); }, variant: TargetPlatformVariant.all()); - testWidgets('TextButton.icon does not overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton.icon does not overflow', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/77815 await tester.pumpWidget( MaterialApp( @@ -1436,7 +1449,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('TextButton.icon icon,label layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton.icon icon,label layout', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); final Key iconKey = UniqueKey(); final Key labelKey = UniqueKey(); @@ -1473,7 +1486,7 @@ void main() { expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0)); }); - testWidgets('TextButton maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton maximumSize', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); @@ -1515,7 +1528,7 @@ void main() { expect(tester.getSize(find.byKey(key1)), const Size(104.0, 128.0)); }); - testWidgets('Fixed size TextButton, same as minimumSize == maximumSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fixed size TextButton, same as minimumSize == maximumSize', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1545,7 +1558,7 @@ void main() { expect(tester.getSize(find.widgetWithText(TextButton, '200,200')), const Size(200, 200)); }); - testWidgets('TextButton changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1623,7 +1636,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('TextButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton in SelectionArea changes mouse cursor when hovered', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104595. await tester.pumpWidget(MaterialApp( home: SelectionArea( @@ -1646,7 +1659,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('TextButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton.styleFrom can be used to set foreground and background colors', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1676,6 +1689,7 @@ void main() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( @@ -1776,20 +1790,21 @@ void main() { await gesture.removePointer(); } - testWidgets('TextButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton statesController', (WidgetTester tester) async { testStatesController(null, tester); }); - testWidgets('TextButton.icon statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextButton.icon statesController', (WidgetTester tester) async { testStatesController(const Icon(Icons.add), tester); }); - testWidgets('Disabled TextButton statesController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled TextButton statesController', (WidgetTester tester) async { int count = 0; void valueChanged() { count += 1; } final MaterialStatesController controller = MaterialStatesController(); + addTearDown(controller.dispose); controller.addListener(valueChanged); await tester.pumpWidget( @@ -1807,7 +1822,7 @@ void main() { expect(count, 1); }); - testWidgets('icon color can be different from the text color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('icon color can be different from the text color', (WidgetTester tester) async { final Key iconButtonKey = UniqueKey(); const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -1863,7 +1878,7 @@ void main() { expect(iconColor(), equals(Colors.blue)); }); - testWidgets("TextButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("TextButton.styleFrom doesn't throw exception on passing only one cursor", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/118071. await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/material/text_button_theme_test.dart b/packages/flutter/test/material/text_button_theme_test.dart index 473e0a64473e7..b9326fdcf10c4 100644 --- a/packages/flutter/test/material/text_button_theme_test.dart +++ b/packages/flutter/test/material/text_button_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('TextButtonTheme lerp special cases', () { @@ -12,7 +13,7 @@ void main() { expect(identical(TextButtonThemeData.lerp(data, data, 0.5), data), true); }); - testWidgets('Material3: Passing no TextButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Passing no TextButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -49,7 +50,7 @@ void main() { expect(align.alignment, Alignment.center); }); - testWidgets('Material2: Passing no TextButtonTheme returns defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Passing no TextButtonTheme returns defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); await tester.pumpWidget( MaterialApp( @@ -187,19 +188,19 @@ void main() { expect(align.alignment, alignment); } - testWidgets('Button style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(themeStyle: style)); await tester.pumpAndSettle(); checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(overallStyle: style)); await tester.pumpAndSettle(); checkButton(tester); @@ -207,26 +208,26 @@ void main() { // Same as the previous tests with empty ButtonStyle's instead of null. - testWidgets('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button style overrides defaults, empty theme and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: style, themeStyle: const ButtonStyle(), overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Button theme style overrides defaults, empty button and overall styles', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), themeStyle: style, overallStyle: const ButtonStyle())); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); - testWidgets('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overall Theme button theme style overrides defaults, null theme and empty overall style', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(buttonStyle: const ButtonStyle(), overallStyle: style)); await tester.pumpAndSettle(); // allow the animations to finish checkButton(tester); }); }); - testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -297,7 +298,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2: Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); diff --git a/packages/flutter/test/material/text_field_focus_test.dart b/packages/flutter/test/material/text_field_focus_test.dart index 6612b36fdc14e..e194a0bdac255 100644 --- a/packages/flutter/test/material/text_field_focus_test.dart +++ b/packages/flutter/test/material/text_field_focus_test.dart @@ -7,10 +7,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/87099 - testWidgets('TextField.autofocus should skip the element that never layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField.autofocus should skip the element that never layout', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -27,10 +28,11 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Dialog interaction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog interaction', (WidgetTester tester) async { expect(tester.testTextInput.isVisible, isFalse); final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -70,8 +72,9 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Request focus shows keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Request focus shows keyboard', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -97,7 +100,7 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Autofocus shows keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Autofocus shows keyboard', (WidgetTester tester) async { expect(tester.testTextInput.isVisible, isFalse); await tester.pumpWidget( @@ -119,7 +122,7 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Tap shows keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap shows keyboard', (WidgetTester tester) async { expect(tester.testTextInput.isVisible, isFalse); await tester.pumpWidget( @@ -158,8 +161,9 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Focus triggers keep-alive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus triggers keep-alive', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -198,8 +202,9 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Focus keep-alive works with GlobalKey reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus keep-alive works with GlobalKey reparenting', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); Widget makeTest(String? prefix) { return MaterialApp( @@ -233,7 +238,7 @@ void main() { expect(find.byType(TextField, skipOffstage: false), findsOneWidget); }); - testWidgets('TextField with decoration:null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with decoration:null', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/16880 await tester.pumpWidget( @@ -254,11 +259,14 @@ void main() { expect(tester.testTextInput.isVisible, isTrue); }); - testWidgets('Sibling FocusScopes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sibling FocusScopes', (WidgetTester tester) async { expect(tester.testTextInput.isVisible, isFalse); final FocusScopeNode focusScopeNode0 = FocusScopeNode(); + addTearDown(focusScopeNode0.dispose); final FocusScopeNode focusScopeNode1 = FocusScopeNode(); + addTearDown(focusScopeNode1.dispose); + final Key textField0 = UniqueKey(); final Key textField1 = UniqueKey(); @@ -320,7 +328,7 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('Sibling Navigators', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sibling Navigators', (WidgetTester tester) async { expect(tester.testTextInput.isVisible, isFalse); final Key textField0 = UniqueKey(); @@ -395,9 +403,12 @@ void main() { expect(tester.testTextInput.isVisible, isFalse); }); - testWidgets('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop', (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(); + addTearDown(focusNodeA.dispose); final FocusNode focusNodeB = FocusNode(); + addTearDown(focusNodeB.dispose); + final Key key = UniqueKey(); await tester.pumpWidget( @@ -452,8 +463,9 @@ void main() { expect(focusNodeB.hasFocus, true); }, variant: TargetPlatformVariant.desktop()); - testWidgets('A Focused text-field will not lose focus when clicking on its decoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A Focused text-field will not lose focus when clicking on its decoration', (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(); + addTearDown(focusNodeA.dispose); final Key iconKey = UniqueKey(); await tester.pumpWidget( @@ -489,9 +501,12 @@ void main() { expect(focusNodeA.hasFocus, true); }, variant: TargetPlatformVariant.desktop()); - testWidgets('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop after tab navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A Focused text-field will lose focus when clicking outside of its hitbox with a mouse on desktop after tab navigation', (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(debugLabel: 'A'); + addTearDown(focusNodeA.dispose); final FocusNode focusNodeB = FocusNode(debugLabel: 'B'); + addTearDown(focusNodeB.dispose); + final Key key = UniqueKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/material/text_field_helper_text_test.dart b/packages/flutter/test/material/text_field_helper_text_test.dart index a5b4a06a421fd..962ea66a6527e 100644 --- a/packages/flutter/test/material/text_field_helper_text_test.dart +++ b/packages/flutter/test/material/text_field_helper_text_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('TextField works correctly when changing helperText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField works correctly when changing helperText', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Material(child: TextField(decoration: InputDecoration(helperText: 'Awesome'))))); expect(find.text('Awesome'), findsNWidgets(1)); await tester.pump(const Duration(milliseconds: 100)); diff --git a/packages/flutter/test/material/text_field_restoration_test.dart b/packages/flutter/test/material/text_field_restoration_test.dart index acc7bddd4af01..6e3a23d1d8dc6 100644 --- a/packages/flutter/test/material/text_field_restoration_test.dart +++ b/packages/flutter/test/material/text_field_restoration_test.dart @@ -4,12 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const String text = 'Hello World! How are you? Life is good!'; const String alternativeText = 'Everything is awesome!!'; void main() { - testWidgets('TextField restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField restoration', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -20,7 +21,7 @@ void main() { await restoreAndVerify(tester); }); - testWidgets('TextField restoration with external controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField restoration with external controller', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'root', diff --git a/packages/flutter/test/material/text_field_splash_test.dart b/packages/flutter/test/material/text_field_splash_test.dart index b16f0ced9b951..28169c992ad8c 100644 --- a/packages/flutter/test/material/text_field_splash_test.dart +++ b/packages/flutter/test/material/text_field_splash_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart' show kPressTimeout; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; bool confirmCalled = false; bool cancelCalled = false; @@ -76,7 +77,7 @@ void main() { cancelCalled = false; }); - testWidgets('Tapping should never cause a splash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping should never cause a splash', (WidgetTester tester) async { final Key textField1 = UniqueKey(); final Key textField2 = UniqueKey(); @@ -135,7 +136,7 @@ void main() { expect(cancelCalled, isFalse); }); - testWidgets('Splash should never be created or canceled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Splash should never be created or canceled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Theme( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index b7a226e989f1a..6e4d47857126a 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart'; @@ -79,6 +80,7 @@ Widget overlay({ required Widget child }) { ); }, ); + addTearDown(() => entry..remove()..dispose()); return overlayWithEntry(entry); } @@ -154,6 +156,18 @@ class TestFormatter extends TextInputFormatter { } } +FocusNode _focusNode() { + final FocusNode result = FocusNode(); + addTearDown(result.dispose); + return result; +} + +TextEditingController _textEditingController({String text = ''}) { + final TextEditingController result = TextEditingController(text: text); + addTearDown(result.dispose); + return result; +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final MockClipboard mockClipboard = MockClipboard(); @@ -204,14 +218,14 @@ void main() { ); } - testWidgets( + testWidgetsWithLeakTracking( 'Live Text button shows and hides correctly when LiveTextStatus changes', (WidgetTester tester) async { final LiveTextInputTester liveTextInputTester = LiveTextInputTester(); addTearDown(liveTextInputTester.dispose); - final TextEditingController controller = TextEditingController(text: ''); + final TextEditingController controller = _textEditingController(); const Key key = ValueKey<String>('TextField'); - final FocusNode focusNode = FocusNode(); + final FocusNode focusNode = _focusNode(); final Widget app = MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Scaffold( @@ -245,7 +259,7 @@ void main() { }, ); - testWidgets('text field selection toolbar should hide when the user starts typing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text field selection toolbar should hide when the user starts typing', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -296,9 +310,9 @@ void main() { expect(state.selectionOverlay!.toolbarIsVisible, isFalse); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('Composing change does not hide selection handle caret', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Composing change does not hide selection handle caret', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/108673 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -386,8 +400,8 @@ void main() { expect(identical(handleRenderObjectBegin, handleRenderObjectEnd), true); }); - testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( @@ -461,8 +475,8 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( @@ -605,7 +619,154 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Look Up shows up on iOS only', (WidgetTester tester) async { + String? lastLookUp; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'LookUp.invoke') { + expect(methodCall.arguments, isA<String>()); + lastLookUp = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = _textEditingController( + text: 'Test ', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Look Up'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Look Up')); + expect(lastLookUp, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgetsWithLeakTracking('Search Web shows up on iOS only', (WidgetTester tester) async { + String? lastSearch; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SearchWeb.invoke') { + expect(methodCall.arguments, isA<String>()); + lastSearch = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = _textEditingController( + text: 'Test ', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Search Web'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Search Web')); + expect(lastSearch, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgetsWithLeakTracking('Share shows up on iOS only', (WidgetTester tester) async { + String? lastShare; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'Share.invoke') { + expect(methodCall.arguments, isA<String>()); + lastShare = methodCall.arguments as String; + } + return null; + }); + + final TextEditingController controller = _textEditingController( + text: 'Test ', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + final bool isTargetPlatformiOS = defaultTargetPlatform == TargetPlatform.iOS; + + // Long press to put the cursor after the "s". + const int index = 3; + await tester.longPressAt(textOffsetToPosition(tester, index)); + await tester.pump(); + + // Double tap on the same location to select the word around the cursor. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 4)); + expect(find.text('Share...'), isTargetPlatformiOS? findsOneWidget : findsNothing); + + if (isTargetPlatformiOS) { + await tester.tap(find.text('Share...')); + expect(lastShare, 'Test'); + } + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgetsWithLeakTracking('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { const Color selectionColor = Colors.orange; const Color cursorColor = Colors.red; @@ -626,7 +787,43 @@ void main() { expect(state.widget.cursorColor, cursorColor); }); - testWidgets('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async { + testWidgets('Use error cursor color when an InputDecoration with an errorText or error widget is provided', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: TextField( + autofocus: true, + decoration: InputDecoration( + error: Text('error'), + errorStyle: TextStyle(color: Colors.teal), + ), + ), + ), + ), + ); + await tester.pump(); + EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); + expect(state.widget.cursorColor, Colors.teal); + + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: TextField( + autofocus: true, + decoration: InputDecoration( + errorText: 'error', + errorStyle: TextStyle(color: Colors.teal), + ), + ), + ), + ), + ); + await tester.pump(); + state = tester.state<EditableTextState>(find.byType(EditableText)); + expect(state.widget.cursorColor, Colors.teal); + }); + + testWidgetsWithLeakTracking('sets cursorOpacityAnimates on EditableText correctly', (WidgetTester tester) async { // True @@ -655,10 +852,10 @@ void main() { expect(editableText.cursorOpacityAnimates, false); }); - testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Activates the text field when receives semantics focus on desktops', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; - final FocusNode focusNode = FocusNode(); + final FocusNode focusNode = _focusNode(); await tester.pumpWidget( MaterialApp( home: Material( @@ -686,6 +883,7 @@ void main() { actions: <SemanticsAction>[ SemanticsAction.tap, SemanticsAction.didGainAccessibilityFocus, + SemanticsAction.didLoseAccessibilityFocus, ], textDirection: TextDirection.ltr, ), @@ -705,10 +903,14 @@ void main() { semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus); await tester.pumpAndSettle(); expect(focusNode.hasFocus, isTrue); + + semanticsOwner.performAction(4, SemanticsAction.didLoseAccessibilityFocus); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, isFalse); semantics.dispose(); - }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows })); + }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux })); - testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { void onEditingComplete() { } await tester.pumpWidget( @@ -728,7 +930,7 @@ void main() { expect(editableTextWidget.onEditingComplete, onEditingComplete); }); - testWidgets('TextField has consistent size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField has consistent size', (WidgetTester tester) async { final Key textFieldKey = UniqueKey(); String? textFieldValue; @@ -771,7 +973,7 @@ void main() { expect(inputBox.size, equals(emptyInputSize)); }); - testWidgets('Cursor blinks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor blinks', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -814,8 +1016,8 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/78918. - testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'how are you'); + testWidgetsWithLeakTracking('RenderEditable sets correct text editing value', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'how are you'); final UniqueKey icon = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -840,7 +1042,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 0)); }); - testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor radius is 2.0', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -855,7 +1057,7 @@ void main() { expect(renderEditable.cursorRadius, const Radius.circular(2.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('cursor has expected defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField(), @@ -868,7 +1070,7 @@ void main() { expect(textField.cursorRadius, null); }); - testWidgets('cursor has expected radius value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor has expected radius value', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -882,7 +1084,7 @@ void main() { expect(textField.cursorRadius, const Radius.circular(3.0)); }); - testWidgets('clipBehavior has expected defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clipBehavior has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField(), @@ -893,7 +1095,9 @@ void main() { expect(textField.clipBehavior, Clip.hardEdge); }); - testWidgets('Overflow clipBehavior none golden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow clipBehavior none golden', (WidgetTester tester) async { + final OverflowWidgetTextEditingController controller = OverflowWidgetTextEditingController(); + addTearDown(controller.dispose); final Widget widget = Theme( data: ThemeData(useMaterial3: false), child: overlay( @@ -907,7 +1111,7 @@ void main() { // Make sure the input field is not high enough for the WidgetSpan. height: 50, child: TextField( - controller: OverflowWidgetTextEditingController(), + controller: controller, clipBehavior: Clip.none, ), ), @@ -930,7 +1134,7 @@ void main() { ); }); - testWidgets('Material cursor android golden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material cursor android golden', (WidgetTester tester) async { final Widget widget = Theme( data: ThemeData(useMaterial3: false), child: overlay( @@ -959,7 +1163,7 @@ void main() { ); }); - testWidgets('Material cursor golden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material cursor golden', (WidgetTester tester) async { final Widget widget = Theme( data: ThemeData(useMaterial3: false), child: overlay( @@ -990,15 +1194,15 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('TextInputFormatter gets correct selection value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextInputFormatter gets correct selection value', (WidgetTester tester) async { late TextEditingValue actualOldValue; late TextEditingValue actualNewValue; void callBack(TextEditingValue oldValue, TextEditingValue newValue) { actualOldValue = oldValue; actualNewValue = newValue; } - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(text: '123'); + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(text: '123'); await tester.pumpWidget( boilerplate( child: TextField( @@ -1031,7 +1235,7 @@ void main() { ); }, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events. - testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1083,9 +1287,9 @@ void main() { ); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('text field toolbar options correctly changes options', + testWidgetsWithLeakTracking('text field toolbar options correctly changes options', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -1134,8 +1338,8 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('text selection style 1', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('text selection style 1', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!', ); await tester.pumpWidget( @@ -1183,8 +1387,8 @@ void main() { ); }); - testWidgets('text selection style 2', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('text selection style 2', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!', ); await tester.pumpWidget( @@ -1248,10 +1452,10 @@ void main() { // Text selection styles are not fully supported on web. }, skip: isBrowser); // https://github.com/flutter/flutter/issues/93723 - testWidgets( + testWidgetsWithLeakTracking( 'text field toolbar options correctly changes options', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -1290,11 +1494,12 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('cursor layout has correct width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct width', (WidgetTester tester) async { final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), ); - final FocusNode focusNode = FocusNode(); + addTearDown(controller.dispose); + final FocusNode focusNode = _focusNode(); EditableText.debugDeterministicCursor = true; await tester.pumpWidget( Theme( @@ -1320,11 +1525,12 @@ void main() { EditableText.debugDeterministicCursor = false; }); - testWidgets('cursor layout has correct radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct radius', (WidgetTester tester) async { final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), ); - final FocusNode focusNode = FocusNode(); + addTearDown(controller.dispose); + final FocusNode focusNode = _focusNode(); EditableText.debugDeterministicCursor = true; await tester.pumpWidget( Theme( @@ -1351,11 +1557,13 @@ void main() { EditableText.debugDeterministicCursor = false; }); - testWidgets('cursor layout has correct height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct height', (WidgetTester tester) async { final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), ); - final FocusNode focusNode = FocusNode(); + addTearDown(controller.dispose); + final FocusNode focusNode = _focusNode(); + EditableText.debugDeterministicCursor = true; await tester.pumpWidget( Theme( @@ -1382,8 +1590,8 @@ void main() { EditableText.debugDeterministicCursor = false; }); - testWidgets('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( Theme( @@ -1458,7 +1666,7 @@ void main() { expect(cursorOffsetSpaces.dx, inputWidth - kCaretGap); }); - testWidgets('Overflowing a line with spaces stops the cursor at the end (rtl direction)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflowing a line with spaces stops the cursor at the end (rtl direction)', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -1485,7 +1693,7 @@ void main() { expect(cursorOffsetSpaces.dx >= 0, isTrue); }); - testWidgets('mobile obscureText control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mobile obscureText control test', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -1525,7 +1733,7 @@ void main() { expect(editText.substring(editText.length - 1), '\u2022'); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); - testWidgets('desktop obscureText control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('desktop obscureText control test', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -1564,8 +1772,8 @@ void main() { TargetPlatform.windows, })); - testWidgets('Caret position is updated on tap', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Caret position is updated on tap', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1591,8 +1799,8 @@ void main() { expect(controller.selection.extentOffset, tapIndex); }); - testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('enableInteractiveSelection = false, tap', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1619,8 +1827,8 @@ void main() { expect(controller.selection.isCollapsed, isTrue); }); - testWidgets('Can long press to select', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can long press to select', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1654,8 +1862,8 @@ void main() { expect(controller.selection.baseOffset, testValue.indexOf('h')); }); - testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1693,10 +1901,13 @@ void main() { expect(handle.opacity.value, equals(1.0)); }); - testWidgets('Long pressing a field with selection 0,0 shows the selection menu', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Long pressing a field with selection 0,0 shows the selection menu', (WidgetTester tester) async { + late final TextEditingController controller; + addTearDown(() => controller.dispose()); + await tester.pumpWidget(overlay( child: TextField( - controller: TextEditingController.fromValue( + controller: controller = TextEditingController.fromValue( const TextEditingValue( selection: TextSelection(baseOffset: 0, extentOffset: 0), ), @@ -1711,8 +1922,8 @@ void main() { expect(find.text('Paste'), findsOneWidget); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('Entering text hides selection handle caret', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Entering text hides selection handle caret', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1753,9 +1964,9 @@ void main() { expect(handle.opacity.value, equals(0.0)); }); - testWidgets('selection handles are excluded from the semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection handles are excluded from the semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1799,8 +2010,8 @@ void main() { semantics.dispose(); }); - testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Mouse long press is just like a tap', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -1830,8 +2041,8 @@ void main() { expect(controller.selection.extentOffset, eIndex); }); - testWidgets('Read only text field basic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Read only text field basic', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'readonly'); await tester.pumpWidget( overlay( @@ -1868,7 +2079,7 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('does not paint toolbar when no options available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not paint toolbar when no options available', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -1889,7 +2100,7 @@ void main() { expect(find.byType(CupertinoTextSelectionToolbar), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('text field build empty toolbar when no options available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text field build empty toolbar when no options available', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -1913,8 +2124,8 @@ void main() { expect(container.size, Size.zero); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('Swapping controllers should update selection', (WidgetTester tester) async { - TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Swapping controllers should update selection', (WidgetTester tester) async { + TextEditingController controller = _textEditingController(text: 'readonly'); final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) { return Center( @@ -1927,6 +2138,7 @@ void main() { ); }, ); + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget(overlayWithEntry(entry)); const int dIndex = 3; final Offset dPos = textOffsetToPosition(tester, dIndex); @@ -1945,6 +2157,7 @@ void main() { extentOffset: 7, )), ); + addTearDown(controller.dispose); // Mark entry to be dirty in order to trigger overlay update. entry.markNeedsBuild(); @@ -1955,13 +2168,14 @@ void main() { expect(currentOverlaySelection.extentOffset, 7); }); - testWidgets('Read only text should not compose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Read only text should not compose', (WidgetTester tester) async { final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue( text: 'readonly', composing: TextRange(start: 0, end: 8), // Simulate text composing. ), ); + addTearDown(controller.dispose); await tester.pumpWidget( overlay( @@ -1977,8 +2191,8 @@ void main() { expect(renderEditable.text, TextSpan(text:'readonly', style: renderEditable.text!.style)); }); - testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'readonly'); bool readOnly = true; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) { @@ -1992,6 +2206,7 @@ void main() { ); }, ); + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget(overlayWithEntry(entry)); await tester.tap(find.byType(TextField)); await tester.pump(); @@ -2012,8 +2227,8 @@ void main() { expect(editableText.selectionOverlay!.handlesAreVisible, isFalse); }); - testWidgets('Dynamically switching to read only should close input connection', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Dynamically switching to read only should close input connection', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'readonly'); bool readOnly = false; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) { @@ -2027,6 +2242,7 @@ void main() { ); }, ); + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget(overlayWithEntry(entry)); await tester.tap(find.byType(TextField)); await tester.pump(); @@ -2040,8 +2256,8 @@ void main() { expect(tester.testTextInput.hasAnyClients, isBrowser ? isTrue : isFalse); }); - testWidgets('Dynamically switching to non read only should open input connection', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Dynamically switching to non read only should open input connection', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'readonly'); bool readOnly = true; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) { @@ -2055,6 +2271,7 @@ void main() { ); }, ); + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget(overlayWithEntry(entry)); await tester.tap(find.byType(TextField)); await tester.pump(); @@ -2068,8 +2285,8 @@ void main() { expect(tester.testTextInput.hasAnyClients, true); }); - testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -2096,8 +2313,8 @@ void main() { expect(controller.selection.baseOffset, testValue.length); }); - testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2135,8 +2352,8 @@ void main() { variant: TargetPlatformVariant.desktop(), ); - testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( @@ -2189,9 +2406,9 @@ void main() { variant: TargetPlatformVariant.mobile(), ); - testWidgets('Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select text with a mouse when wrapped in a GestureDetector with tap/double tap callbacks', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/129161. - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2235,8 +2452,8 @@ void main() { expect(controller.selection.extentOffset, testValue.indexOf('g')); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can select text by dragging with a mouse', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2268,7 +2485,7 @@ void main() { }); testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2314,8 +2531,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2360,7 +2577,7 @@ void main() { ); testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2410,7 +2627,7 @@ void main() { testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/122519 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2484,8 +2701,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can move cursor when dragging (Android)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2528,8 +2745,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets('Can move cursor when dragging (Android) - multiline', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can move cursor when dragging (Android) - multiline', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2573,10 +2790,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets('Can move cursor when dragging (Android) - ListView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can move cursor when dragging (Android) - ListView', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/122519 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2644,10 +2861,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Continuous dragging does not cause flickering', (WidgetTester tester) async { int selectionChangedCount = 0; const String testValue = 'abc def ghi'; - final TextEditingController controller = TextEditingController(text: testValue); + final TextEditingController controller = _textEditingController(text: testValue); controller.addListener(() { selectionChangedCount++; @@ -2696,8 +2913,8 @@ void main() { expect(controller.selection.extentOffset, 9); }); - testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Dragging in opposite direction also works', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2728,8 +2945,8 @@ void main() { expect(controller.selection.extentOffset, testValue.indexOf('e')); }); - testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Slow mouse dragging also selects text', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -2759,8 +2976,8 @@ void main() { expect(controller.selection.extentOffset, testValue.indexOf('g')); }); - testWidgets('Can drag handles to change selection on Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can drag handles to change selection on Apple platforms', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -2871,8 +3088,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('Can drag handles to change selection on non-Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can drag handles to change selection on non-Apple platforms', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -2976,14 +3193,15 @@ void main() { variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can drag the left handle while the right handle remains off-screen', (WidgetTester tester) async { // Text is longer than textfield width. const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb'; - final TextEditingController controller = TextEditingController(text: testValue); + final TextEditingController controller = _textEditingController(text: testValue); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( @@ -3063,14 +3281,15 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Can drag the right handle while the left handle remains off-screen', (WidgetTester tester) async { // Text is longer than textfield width. const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb'; - final TextEditingController controller = TextEditingController(text: testValue); + final TextEditingController controller = _textEditingController(text: testValue); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( @@ -3139,10 +3358,10 @@ void main() { }, ); - testWidgets('Drag handles trigger feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag handles trigger feedback', (WidgetTester tester) async { final FeedbackTester feedback = FeedbackTester(); addTearDown(feedback.dispose); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( child: TextField( @@ -3194,10 +3413,10 @@ void main() { expect(feedback.hapticCount, 2); }); - testWidgets('Dragging a collapsed handle should trigger feedback.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dragging a collapsed handle should trigger feedback.', (WidgetTester tester) async { final FeedbackTester feedback = FeedbackTester(); addTearDown(feedback.dispose); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( child: TextField( @@ -3249,8 +3468,8 @@ void main() { expect(feedback.hapticCount, 1); }); - testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Cannot drag one handle past the other', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -3309,8 +3528,8 @@ void main() { expect(controller.selection.extentOffset, 5); }); - testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( // 11 first line, 19 second line, 17 third line = length 49 text: 'a big house\njumped over a mouse\nOne more line yay', ); @@ -3497,8 +3716,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on iOS', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Dragging between multiple lines keeps the contact point at the same place on the handle on iOS', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( // 11 first line, 19 second line, 17 third line = length 49 text: 'a big house\njumped over a mouse\nOne more line yay', ); @@ -3695,7 +3914,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets("dragging caret within a word doesn't affect composing region", (WidgetTester tester) async { + testWidgetsWithLeakTracking("dragging caret within a word doesn't affect composing region", (WidgetTester tester) async { const String testValue = 'abc def ghi'; final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue( @@ -3711,6 +3930,7 @@ void main() { ), ), ); + addTearDown(controller.dispose); await tester.pumpWidget( overlay( child: TextField( @@ -3765,8 +3985,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }) ); - testWidgets('Can use selection toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can use selection toolbar', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -3859,12 +4079,12 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero } - testWidgets( + testWidgetsWithLeakTracking( 'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/29808 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -3913,10 +4133,10 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'the toolbar adjusts its position above/below when bottom inset changes', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -3981,12 +4201,12 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'Toolbar appears in the right places in multiline inputs', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/36749 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), @@ -4038,8 +4258,8 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('Selection toolbar fades in', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Selection toolbar fades in', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -4084,11 +4304,11 @@ void main() { // End the test here to ensure the animation is properly disposed of. }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('An obscured TextField is selectable by default', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/32845 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); Widget buildFrame(bool obscureText) { return overlay( child: TextField( @@ -4111,11 +4331,11 @@ void main() { expect(controller.selection.isCollapsed, false); }); - testWidgets('An obscured TextField is not selectable when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('An obscured TextField is not selectable when disabled', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/32845 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { return overlay( child: TextField( @@ -4139,11 +4359,11 @@ void main() { expect(controller.selection.isCollapsed, true); }); - testWidgets('An obscured TextField is not selectable when read-only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('An obscured TextField is not selectable when read-only', (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/32845 - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); Widget buildFrame(bool obscureText, bool readOnly) { return overlay( child: TextField( @@ -4167,8 +4387,8 @@ void main() { expect(controller.selection.isCollapsed, true); }); - testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('An obscured TextField is selected as one word', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(overlay( child: TextField( @@ -4189,8 +4409,8 @@ void main() { expect(selection.extentOffset, 10); }); - testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('An obscured TextField has correct default context menu', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(overlay( child: TextField( @@ -4225,7 +4445,7 @@ void main() { expect(find.text('Cut'), findsNothing); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('create selection overlay if none exists when toggleToolbar is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('create selection overlay if none exists when toggleToolbar is called', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/111660 final Widget testWidget = MaterialApp( home: Scaffold( @@ -4274,7 +4494,7 @@ void main() { expect(tester.takeException(), isNull); }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS})); - testWidgets('TextField height with minLines unset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField height with minLines unset', (WidgetTester tester) async { await tester.pumpWidget(textFieldBuilder()); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -4338,7 +4558,7 @@ void main() { expect(inputBox.size.width, fourLineInputSize.width); }); - testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField height with minLines and maxLines', (WidgetTester tester) async { await tester.pumpWidget(textFieldBuilder()); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -4389,7 +4609,7 @@ void main() { }, throwsAssertionError); }); - testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiline text when wrapped in Expanded', (WidgetTester tester) async { Widget expandedTextFieldBuilder({ int? maxLines = 1, int? minLines, @@ -4446,7 +4666,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/pull/29093 - testWidgets('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async { final Key intrinsicHeightKey = UniqueKey(); Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) { final TextFormField textField = TextFormField( @@ -4483,7 +4703,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/pull/29093 - testWidgets('errorText empty string', (WidgetTester tester) async { + testWidgetsWithLeakTracking('errorText empty string', (WidgetTester tester) async { Widget textFormFieldBuilder(String? errorText) { return boilerplate( child: Column( @@ -4528,7 +4748,7 @@ void main() { expect(inputBox.size.width, errorNullInputSize.width); }); - testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Growable TextField when content height exceeds parent', (WidgetTester tester) async { const double height = 200.0; const double padding = 24.0; @@ -4637,7 +4857,7 @@ void main() { ); }); - testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async { final Key textFieldKey = UniqueKey(); Widget builder(int? maxLines, final String hintMsg) { @@ -4674,8 +4894,8 @@ void main() { expect(findHintText(multipleLineText).size.height, greaterThanOrEqualTo(oneLineHintSize.height)); }); - testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can drag handles to change selection in multiline', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( Theme( @@ -4773,9 +4993,9 @@ void main() { } }); - testWidgets('Can scroll multiline input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can scroll multiline input', (WidgetTester tester) async { final Key textFieldKey = UniqueKey(); - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: kMoreThanFourLines, ); @@ -4869,7 +5089,7 @@ void main() { expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); }); - testWidgets('TextField smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField smoke test', (WidgetTester tester) async { late String textFieldValue; await tester.pumpWidget( @@ -4897,7 +5117,7 @@ void main() { await checkText('Hello World'); }); - testWidgets('TextField with global key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with global key', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey'); late String textFieldValue; @@ -4927,7 +5147,7 @@ void main() { await checkText('Hello World'); }); - testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField errorText trumps helperText', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -4945,7 +5165,7 @@ void main() { expect(find.text('error text'), findsOneWidget); }); - testWidgets('TextField with default helperStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with default helperStyle', (WidgetTester tester) async { final ThemeData themeData = ThemeData(hintColor: Colors.blue[500], useMaterial3: false); await tester.pumpWidget( overlay( @@ -4964,7 +5184,7 @@ void main() { expect(helperText.style!.fontSize, Typography.englishLike2014.bodySmall!.fontSize); }); - testWidgets('TextField with specified helperStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with specified helperStyle', (WidgetTester tester) async { final TextStyle style = TextStyle( inherit: false, color: Colors.pink[500], @@ -4985,7 +5205,7 @@ void main() { expect(helperText.style, style); }); - testWidgets('TextField with default hintStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with default hintStyle', (WidgetTester tester) async { final TextStyle style = TextStyle( color: Colors.pink[500], fontSize: 10.0, @@ -5013,7 +5233,7 @@ void main() { expect(hintText.style!.fontSize, style.fontSize); }); - testWidgets('TextField with specified hintStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with specified hintStyle', (WidgetTester tester) async { final TextStyle hintStyle = TextStyle( inherit: false, color: Colors.pink[500], @@ -5035,7 +5255,7 @@ void main() { expect(hintText.style, hintStyle); }); - testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with specified prefixStyle', (WidgetTester tester) async { final TextStyle prefixStyle = TextStyle( inherit: false, color: Colors.pink[500], @@ -5057,12 +5277,12 @@ void main() { expect(prefixText.style, prefixStyle); }); - testWidgets('TextField prefix and suffix create a sibling node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField prefix and suffix create a sibling node', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( overlay( child: TextField( - controller: TextEditingController(text: 'some text'), + controller: _textEditingController(text: 'some text'), decoration: const InputDecoration( prefixText: 'Prefix', suffixText: 'Suffix', @@ -5099,7 +5319,7 @@ void main() { semantics.dispose(); }); - testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField with specified suffixStyle', (WidgetTester tester) async { final TextStyle suffixStyle = TextStyle( color: Colors.pink[500], fontSize: 10.0, @@ -5120,7 +5340,7 @@ void main() { expect(suffixText.style, suffixStyle); }); - testWidgets('TextField prefix and suffix appear correctly with no hint or label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField prefix and suffix appear correctly with no hint or label', (WidgetTester tester) async { final Key secondKey = UniqueKey(); await tester.pumpWidget( @@ -5163,7 +5383,7 @@ void main() { expect(find.text('Suffix'), findsOneWidget); }); - testWidgets('TextField prefix and suffix appear correctly with hint text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField prefix and suffix appear correctly with hint text', (WidgetTester tester) async { final TextStyle hintStyle = TextStyle( inherit: false, color: Colors.pink[500], @@ -5223,7 +5443,7 @@ void main() { expect(suffixText.style, hintStyle); }); - testWidgets('TextField prefix and suffix appear correctly with label text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField prefix and suffix appear correctly with label text', (WidgetTester tester) async { final TextStyle prefixStyle = TextStyle( color: Colors.pink[500], fontSize: 10.0, @@ -5286,7 +5506,7 @@ void main() { expect(suffixText.style, suffixStyle); }); - testWidgets('TextField label text animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField label text animates', (WidgetTester tester) async { final Key secondKey = UniqueKey(); await tester.pumpWidget( @@ -5327,7 +5547,7 @@ void main() { expect(newPos.dy, lessThan(pos.dy)); }); - testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon is separated from input/label by 16+12', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const TextField( @@ -5348,7 +5568,7 @@ void main() { expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx)); }); - testWidgets('Collapsed hint text placement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Collapsed hint text placement', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -5366,7 +5586,7 @@ void main() { expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(EditableText)))); }); - testWidgets('Can align to center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align to center', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -5401,7 +5621,7 @@ void main() { expect(topLeft.dx, equals(399.0)); }); - testWidgets('Can align to center within center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align to center within center', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -5437,11 +5657,11 @@ void main() { expect(topLeft.dx, equals(399.0)); }); - testWidgets('Controller can update server', (WidgetTester tester) async { - final TextEditingController controller1 = TextEditingController( + testWidgetsWithLeakTracking('Controller can update server', (WidgetTester tester) async { + final TextEditingController controller1 = _textEditingController( text: 'Initial Text', ); - final TextEditingController controller2 = TextEditingController( + final TextEditingController controller2 = _textEditingController( text: 'More Text', ); @@ -5520,8 +5740,8 @@ void main() { expect(tester.testTextInput.editingState!['text'], equals('The Final Cut')); }); - testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Cannot enter new lines onto single line TextField', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField(controller: textController, decoration: null), @@ -5532,8 +5752,8 @@ void main() { expect(textController.text, 'abcdef'); }); - testWidgets('Injected formatters are chained', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Injected formatters are chained', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5553,8 +5773,8 @@ void main() { expect(textController.text, '#一#二#三#四#五#六'); }); - testWidgets('Injected formatters are chained (deprecated names)', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Injected formatters are chained (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5574,8 +5794,8 @@ void main() { expect(textController.text, '#一#二#三#四#五#六'); }); - testWidgets('Chained formatters are in sequence', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Chained formatters are in sequence', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5601,8 +5821,8 @@ void main() { expect(textController.text, '\n1\n2\n3'); }); - testWidgets('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5628,8 +5848,8 @@ void main() { expect(textController.text, '\n1\n2\n3'); }); - testWidgets('Pasted values are formatted', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Pasted values are formatted', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget( overlay( @@ -5666,8 +5886,8 @@ void main() { expect(textController.text, '145623'); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('Pasted values are formatted (deprecated names)', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('Pasted values are formatted (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget( overlay( @@ -5704,7 +5924,7 @@ void main() { expect(textController.text, '145623'); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not add LengthLimiting formatter to the user supplied list', (WidgetTester tester) async { final List<TextInputFormatter> formatters = <TextInputFormatter>[]; await tester.pumpWidget( @@ -5720,8 +5940,8 @@ void main() { expect(formatters.isEmpty, isTrue); }); - testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Text field scrolls the caret into view', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( Theme( @@ -5763,8 +5983,8 @@ void main() { expect(scrollableState.position.pixels, equals(222.0)); }); - testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Multiline text field scrolls the caret into view', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -5801,10 +6021,10 @@ void main() { expect(scrollableState.position.pixels, moreOrLessEquals(lineHeight, epsilon: 0.1)); }); - testWidgets('haptic feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('haptic feedback', (WidgetTester tester) async { final FeedbackTester feedback = FeedbackTester(); addTearDown(feedback.dispose); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -5828,11 +6048,11 @@ void main() { expect(feedback.hapticCount, 1); }); - testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text field drops selection color when losing focus', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103341. final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); - final TextEditingController controller1 = TextEditingController(); + final TextEditingController controller1 = _textEditingController(); const Color selectionColor = Colors.orange; const Color cursorColor = Colors.red; @@ -5883,8 +6103,8 @@ void main() { expect(state2.widget.selectionColor, selectionColor); }); - testWidgets('Selection is consistent with text length', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Selection is consistent with text length', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); controller.text = 'abcde'; controller.selection = const TextSelection.collapsed(offset: 5); @@ -5912,8 +6132,8 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/35848 - testWidgets('Clearing text field with suffixIcon does not cause text selection exception', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Clearing text field with suffixIcon does not cause text selection exception', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Prefilled text.', ); @@ -5935,8 +6155,8 @@ void main() { expect(controller.text, ''); }); - testWidgets('maxLength limits input.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5949,8 +6169,8 @@ void main() { expect(textController.text, '0123456789'); }); - testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5964,8 +6184,8 @@ void main() { expect(textController.text, '${surrogatePair}012345678'); }); - testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -5979,9 +6199,9 @@ void main() { expect(textController.text, '${graphemeCluster}012345678'); }); - testWidgets('maxLength limits input in the center of a maxed-out field.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('maxLength limits input in the center of a maxed-out field.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/37420. - final TextEditingController textController = TextEditingController(); + final TextEditingController textController = _textEditingController(); const String testValue = '0123456789'; await tester.pumpWidget(boilerplate( @@ -6004,10 +6224,10 @@ void main() { expect(textController.text, testValue); }); - testWidgets( + testWidgetsWithLeakTracking( 'maxLength limits input in the center of a maxed-out field, with collapsed selection', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + final TextEditingController textController = _textEditingController(); const String testValue = '0123456789'; await tester.pumpWidget(boilerplate( @@ -6049,10 +6269,10 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'maxLength limits input in the center of a maxed-out field, with non-collapsed selection', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + final TextEditingController textController = _textEditingController(); const String testValue = '0123456789'; await tester.pumpWidget(boilerplate( @@ -6084,8 +6304,8 @@ void main() { }, ); - testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input length even if decoration is null.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6099,8 +6319,8 @@ void main() { expect(textController.text, '0123456789'); }); - testWidgets('maxLength still works with other formatters', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength still works with other formatters', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6120,8 +6340,8 @@ void main() { expect(textController.text, '#一#二#三#四#五'); }); - testWidgets('maxLength still works with other formatters (deprecated names)', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength still works with other formatters (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6141,8 +6361,8 @@ void main() { expect(textController.text, '#一#二#三#四#五'); }); - testWidgets("maxLength isn't enforced when maxLengthEnforcement.none.", (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking("maxLength isn't enforced when maxLengthEnforcement.none.", (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6156,8 +6376,8 @@ void main() { expect(textController.text, '0123456789101112'); }); - testWidgets('maxLength shows warning when maxLengthEnforcement.none.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength shows warning when maxLengthEnforcement.none.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); await tester.pumpWidget(boilerplate( @@ -6186,8 +6406,8 @@ void main() { expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); }); - testWidgets('maxLength shows warning in Material 3', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength shows warning in Material 3', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); final ThemeData theme = ThemeData.from( colorScheme: const ColorScheme.light().copyWith(error: Colors.deepPurpleAccent), useMaterial3: true, @@ -6218,8 +6438,8 @@ void main() { expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); }); - testWidgets('maxLength shows warning when maxLengthEnforcement.none with surrogate pairs.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength shows warning when maxLengthEnforcement.none with surrogate pairs.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); await tester.pumpWidget(boilerplate( @@ -6248,8 +6468,8 @@ void main() { expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); }); - testWidgets('maxLength shows warning when maxLengthEnforcement.none with grapheme clusters.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength shows warning when maxLengthEnforcement.none with grapheme clusters.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent); await tester.pumpWidget(boilerplate( @@ -6278,8 +6498,8 @@ void main() { expect(counterTextWidget.style!.color, isNot(equals(Colors.deepPurpleAccent))); }); - testWidgets('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input with surrogate pairs.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6293,8 +6513,8 @@ void main() { expect(textController.text, '${surrogatePair}012345678'); }); - testWidgets('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { - final TextEditingController textController = TextEditingController(); + testWidgetsWithLeakTracking('maxLength limits input with grapheme clusters.', (WidgetTester tester) async { + final TextEditingController textController = _textEditingController(); await tester.pumpWidget(boilerplate( child: TextField( @@ -6308,7 +6528,7 @@ void main() { expect(textController.text, '${graphemeCluster}012345678'); }); - testWidgets('setting maxLength shows counter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setting maxLength shows counter', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -6328,7 +6548,7 @@ void main() { expect(find.text('5/10'), findsOneWidget); }); - testWidgets('maxLength counter measures surrogate pairs as one character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('maxLength counter measures surrogate pairs as one character', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -6349,7 +6569,7 @@ void main() { expect(find.text('1/10'), findsOneWidget); }); - testWidgets('maxLength counter measures grapheme clusters as one character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('maxLength counter measures grapheme clusters as one character', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -6370,7 +6590,7 @@ void main() { expect(find.text('1/10'), findsOneWidget); }); - testWidgets('setting maxLength to TextField.noMaxLength shows only entered length', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setting maxLength to TextField.noMaxLength shows only entered length', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Material( child: Center( @@ -6390,7 +6610,7 @@ void main() { expect(find.text('5'), findsOneWidget); }); - testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passing a buildCounter shows returned widget', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Center( @@ -6413,7 +6633,7 @@ void main() { expect(find.text('5 of 10'), findsOneWidget); }); - testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField identifies as text field in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -6433,7 +6653,7 @@ void main() { semantics.dispose(); }); - testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled text field does not have tap action', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( const MaterialApp( @@ -6452,7 +6672,7 @@ void main() { semantics.dispose(); }); - testWidgets('Disabled text field semantics node still contains value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled text field semantics node still contains value', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -6460,7 +6680,7 @@ void main() { home: Material( child: Center( child: TextField( - controller: TextEditingController(text: 'text'), + controller: _textEditingController(text: 'text'), maxLength: 10, enabled: false, ), @@ -6473,7 +6693,7 @@ void main() { semantics.dispose(); }); - testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Readonly text field does not have tap action', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -6494,7 +6714,7 @@ void main() { semantics.dispose(); }); - testWidgets('Disabled text field hides helper and counter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled text field hides helper and counter', (WidgetTester tester) async { const String helperText = 'helper text'; const String counterText = 'counter text'; const String errorText = 'error text'; @@ -6541,8 +6761,8 @@ void main() { expect(errorWidget.style!.color, equals(Colors.transparent)); }); - testWidgets('Disabled text field has default M2 disabled text style for the input text', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Disabled text field has default M2 disabled text style for the input text', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); @@ -6563,8 +6783,8 @@ void main() { expect(editableText.style.color, Colors.black38); // Colors.black38 is the default disabled color for ThemeData.light(). }); - testWidgets('Disabled text field has default M3 disabled text style for the input text', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Disabled text field has default M3 disabled text style for the input text', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); @@ -6587,9 +6807,44 @@ void main() { expect(editableText.style.color, theme.textTheme.bodyLarge!.color!.withOpacity(0.38)); }); - testWidgets('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Provided style correctly resolves for material states', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + + final ThemeData theme = ThemeData.light(useMaterial3: true); + + Widget buildFrame(bool enabled) { + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: TextField( + controller: controller, + enabled: enabled, + style: MaterialStateTextStyle.resolveWith((Set<MaterialState> states) { + if (states.contains(MaterialState.disabled)) { + return const TextStyle(color: Colors.red); + } + return const TextStyle(color: Colors.blue); + }), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(false)); + EditableText editableText = tester.widget(find.byType(EditableText)); + expect(editableText.style.color, Colors.red); + await tester.pumpWidget(buildFrame(true)); + editableText = tester.widget(find.byType(EditableText)); + expect(editableText.style.color, Colors.blue); + }); + + testWidgetsWithLeakTracking('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -6628,7 +6883,7 @@ void main() { semantics.dispose(); }); - testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -6652,9 +6907,12 @@ void main() { semantics.dispose(); }); - testWidgets("Disabled TextField can't be traversed to.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Disabled TextField can't be traversed to.", (WidgetTester tester) async { final FocusNode focusNode1 = FocusNode(debugLabel: 'TextField 1'); + addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(debugLabel: 'TextField 2'); + addTearDown(focusNode2.dispose); + await tester.pumpWidget( MaterialApp( home: Material( @@ -6685,7 +6943,7 @@ void main() { expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode2.hasPrimaryFocus, isFalse); - expect(focusNode1.nextFocus(), isTrue); + expect(focusNode1.nextFocus(), isFalse); await tester.pump(); expect(focusNode1.hasPrimaryFocus, isTrue); @@ -6696,12 +6954,12 @@ void main() { late TextEditingController controller; setUp( () { - controller = TextEditingController(); + controller = _textEditingController(); }); Future<void> setupWidget(WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - controller = TextEditingController(); + final FocusNode focusNode = _focusNode(); + controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -6719,7 +6977,7 @@ void main() { await tester.pump(); } - testWidgets('Shift test 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shift test 1', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'a big house'; await tester.enterText(find.byType(TextField), testValue); @@ -6736,7 +6994,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Shift test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shift test 2', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'abcdefghi'; @@ -6754,7 +7012,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Control Shift test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Control Shift test', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'their big house'; await tester.enterText(find.byType(TextField), testValue); @@ -6771,7 +7029,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, 5); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Down and up test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Down and up test', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'a big house'; await tester.enterText(find.byType(TextField), testValue); @@ -6798,7 +7056,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Down and up test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Down and up test 2', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 await tester.enterText(find.byType(TextField), testValue); @@ -6854,8 +7112,8 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Read only keyboard selection test', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'readonly'); + testWidgetsWithLeakTracking('Read only keyboard selection test', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'readonly'); await tester.pumpWidget( overlay( child: TextField( @@ -6875,9 +7133,9 @@ void main() { }, variant: KeySimulatorTransitModeVariant.all()); }, skip: areKeyEventsHandledByPlatform); // [intended] only applies to platforms where we handle key events. - testWidgets('Copy paste test', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Copy paste test', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(); final TextField textField = TextField( controller: controller, @@ -6953,9 +7211,9 @@ void main() { ); // Regression test for https://github.com/flutter/flutter/issues/78219 - testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(); final TextField textField = TextField( controller: controller, obscureText: true, @@ -7005,9 +7263,9 @@ void main() { variant: KeySimulatorTransitModeVariant.all(), ); - testWidgets('Cut test', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Cut test', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(); final TextField textField = TextField( controller: controller, @@ -7084,9 +7342,9 @@ void main() { variant: KeySimulatorTransitModeVariant.all() ); - testWidgets('Select all test', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Select all test', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(); final TextField textField = TextField( controller: controller, @@ -7135,9 +7393,9 @@ void main() { variant: KeySimulatorTransitModeVariant.all() ); - testWidgets('Delete test', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Delete test', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); + final TextEditingController controller = _textEditingController(); final TextField textField = TextField( controller: controller, @@ -7189,13 +7447,13 @@ void main() { variant: KeySimulatorTransitModeVariant.all(), ); - testWidgets('Changing positions of text fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing positions of text fields', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + final FocusNode focusNode = _focusNode(); final List<RawKeyEvent> events = <RawKeyEvent>[]; - final TextEditingController c1 = TextEditingController(); - final TextEditingController c2 = TextEditingController(); + final TextEditingController c1 = _textEditingController(); + final TextEditingController c2 = _textEditingController(); final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); @@ -7284,12 +7542,12 @@ void main() { variant: KeySimulatorTransitModeVariant.all() ); - testWidgets('Changing focus test', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Changing focus test', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); final List<RawKeyEvent> events = <RawKeyEvent>[]; - final TextEditingController c1 = TextEditingController(); - final TextEditingController c2 = TextEditingController(); + final TextEditingController c1 = _textEditingController(); + final TextEditingController c2 = _textEditingController(); final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); @@ -7361,8 +7619,8 @@ void main() { variant: KeySimulatorTransitModeVariant.all() ); - testWidgets('Caret works when maxLines is null', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Caret works when maxLines is null', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( @@ -7388,9 +7646,9 @@ void main() { expect(controller.selection.baseOffset, 0); }); - testWidgets('TextField baseline alignment no-strut', (WidgetTester tester) async { - final TextEditingController controllerA = TextEditingController(text: 'A'); - final TextEditingController controllerB = TextEditingController(text: 'B'); + testWidgetsWithLeakTracking('TextField baseline alignment no-strut', (WidgetTester tester) async { + final TextEditingController controllerA = _textEditingController(text: 'A'); + final TextEditingController controllerB = _textEditingController(text: 'B'); final Key keyA = UniqueKey(); final Key keyB = UniqueKey(); @@ -7450,9 +7708,9 @@ void main() { expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); }); - testWidgets('TextField baseline alignment', (WidgetTester tester) async { - final TextEditingController controllerA = TextEditingController(text: 'A'); - final TextEditingController controllerB = TextEditingController(text: 'B'); + testWidgetsWithLeakTracking('TextField baseline alignment', (WidgetTester tester) async { + final TextEditingController controllerA = _textEditingController(text: 'A'); + final TextEditingController controllerB = _textEditingController(text: 'B'); final Key keyA = UniqueKey(); final Key keyB = UniqueKey(); @@ -7511,9 +7769,9 @@ void main() { expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); }); - testWidgets('TextField semantics include label when unfocused and label/hint when focused if input is empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics include label when unfocused and label/hint when focused if input is empty', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(text: ''); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7543,9 +7801,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics alway include label and not hint when input value is not empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics alway include label and not hint when input value is not empty', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(text: 'value'); + final TextEditingController controller = _textEditingController(text: 'value'); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7575,9 +7833,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics always include label when no hint is given', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics always include label when no hint is given', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(text: 'value'); + final TextEditingController controller = _textEditingController(text: 'value'); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7606,9 +7864,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics only include hint when it is visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics only include hint when it is visible', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(text: 'value'); + final TextEditingController controller = _textEditingController(text: 'value'); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7645,9 +7903,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7778,9 +8036,9 @@ void main() { }); // Regressing test for https://github.com/flutter/flutter/issues/99763 - testWidgets('Update textField semantics when obscureText changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update textField semantics when obscureText changes', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(_ObscureTextTestWidget(controller: controller)); controller.text = 'Hello'; @@ -7831,9 +8089,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics, enableInteractiveSelection = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics, enableInteractiveSelection = false', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -7874,9 +8132,9 @@ void main() { semantics.dispose(); }); - testWidgets('TextField semantics for selections', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics for selections', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController() + final TextEditingController controller = _textEditingController() ..text = 'Hello'; final Key key = UniqueKey(); @@ -7965,10 +8223,10 @@ void main() { semantics.dispose(); }); - testWidgets('TextField change selection with semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField change selection with semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; - final TextEditingController controller = TextEditingController() + final TextEditingController controller = _textEditingController() ..text = 'Hello'; final Key key = UniqueKey(); @@ -8062,14 +8320,14 @@ void main() { semantics.dispose(); }); - testWidgets('Can activate TextField with explicit controller via semantics ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can activate TextField with explicit controller via semantics ', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17801 const String textInTextField = 'Hello'; final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; - final TextEditingController controller = TextEditingController() + final TextEditingController controller = _textEditingController() ..text = textInTextField; final Key key = UniqueKey(); @@ -8134,12 +8392,12 @@ void main() { semantics.dispose(); }); - testWidgets('When clipboard empty, no semantics paste option', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When clipboard empty, no semantics paste option', (WidgetTester tester) async { const String textInTextField = 'Hello'; final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; - final TextEditingController controller = TextEditingController() + final TextEditingController controller = _textEditingController() ..text = textInTextField; final Key key = UniqueKey(); @@ -8211,7 +8469,7 @@ void main() { // https://github.com/flutter/flutter/pull/57139#issuecomment-629048058 }, skip: isBrowser); // [intended] see above. - testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField throws when not descended from a Material widget', (WidgetTester tester) async { const Widget textField = TextField(); await tester.pumpWidget(textField); final dynamic exception = tester.takeException(); @@ -8219,8 +8477,9 @@ void main() { expect(exception.toString(), startsWith('No Material widget found.')); }); - testWidgets('TextField loses focus when disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField loses focus when disabled', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'TextField Focus Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( boilerplate( @@ -8286,7 +8545,7 @@ void main() { expect(focusNode.hasFocus, isTrue); }); - testWidgets('TextField displays text with text direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField displays text with text direction', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -8330,9 +8589,9 @@ void main() { expect(topLeft.dx, equals(160.0)); }); - testWidgets('TextField semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -8422,9 +8681,9 @@ void main() { semantics.dispose(); }); - testWidgets('InputDecoration counterText can have a semanticCounterText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration counterText can have a semanticCounterText', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -8471,9 +8730,9 @@ void main() { semantics.dispose(); }); - testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InputDecoration errorText semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final Key key = UniqueKey(); await tester.pumpWidget( @@ -8514,8 +8773,8 @@ void main() { semantics.dispose(); }); - testWidgets('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'Just some text'); + testWidgetsWithLeakTracking('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(text: 'Just some text'); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -8539,12 +8798,13 @@ void main() { expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top)); }); - testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/20485 final Key textField1 = UniqueKey(); final Key textField2 = UniqueKey(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); double? minOffset; double? maxOffset; @@ -8612,12 +8872,13 @@ void main() { expect(maxOffset, 200.0); }); - testWidgets('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/20485 final Key textField1 = UniqueKey(); final Key textField2 = UniqueKey(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); double? minOffset; double? maxOffset; @@ -8683,7 +8944,7 @@ void main() { expect(maxOffset, 50.0); }); - testWidgets('onTap is called upon tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap is called upon tap', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget( overlay( @@ -8706,7 +8967,7 @@ void main() { expect(tapCount, 3); }); - testWidgets('onTap is not called, field is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap is not called, field is disabled', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget( overlay( @@ -8726,7 +8987,7 @@ void main() { expect(tapCount, 0); }); - testWidgets('Includes cursor for TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Includes cursor for TextField', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/24612 Widget buildFrame({ @@ -8802,7 +9063,7 @@ void main() { expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0 + CARET_GAP); }); - testWidgets('TextField style is merged with theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField style is merged with theme', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/23994 final ThemeData themeData = ThemeData( @@ -8853,7 +9114,7 @@ void main() { expect(editableText.style.color, isNull); }); - testWidgets('TextField style is merged with theme in Material 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField style is merged with theme in Material 3', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/23994 final ThemeData themeData = ThemeData( @@ -8908,7 +9169,7 @@ void main() { expect(editableText.style.color, isNull); }); - testWidgets('style enforces required fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('style enforces required fields', (WidgetTester tester) async { Widget buildFrame(TextStyle style) { return MaterialApp( home: Material( @@ -8939,10 +9200,10 @@ void main() { expect(tester.takeException(), isNotNull); }); - testWidgets( + testWidgetsWithLeakTracking( 'tap moves cursor to the edge of the word it tapped', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -8974,10 +9235,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'tap with a mouse does not move cursor to the edge of the word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9010,8 +9271,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('tap moves cursor to the position tapped', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('tap moves cursor to the position tapped', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9041,10 +9302,10 @@ void main() { expect(find.byType(TextButton), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets( + testWidgetsWithLeakTracking( 'two slow taps do not trigger a word selection', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -9079,10 +9340,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Tapping on a collapsed selection toggles the toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -9166,10 +9427,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Tapping on a non-collapsed selection toggles the toolbar and retains the selection', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -9211,8 +9472,8 @@ void main() { const TextSelection(baseOffset: 24, extentOffset: 35), ); - // Selected text shows 3 toolbar buttons. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + // Selected text shows 4 toolbar buttons. + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Tap the selected word to hide the toolbar and retain the selection. await tester.tapAt(vPos); @@ -9230,7 +9491,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 24, extentOffset: 35), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Tap past the selected word to move the cursor and hide the toolbar. await tester.tapAt(ePos); @@ -9242,10 +9503,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap selects word and first tap of double tap moves cursor (iOS)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9284,14 +9545,14 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); - // Selected text shows 3 toolbar buttons. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + // Selected text shows 5 toolbar buttons. + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('iOS selectWordEdge works correctly', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( @@ -9321,10 +9582,10 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 0)); }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets( + testWidgetsWithLeakTracking( 'double tap does not select word on read-only obscured field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9370,10 +9631,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap selects word and first tap of double tap moves cursor and shows toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9417,10 +9678,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Custom toolbar test - Android text selection controls', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9451,10 +9712,10 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets( + testWidgetsWithLeakTracking( 'Custom toolbar test - Cupertino text selection controls', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9485,7 +9746,7 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets('selectionControls is passed to EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selectionControls is passed to EditableText', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -9502,10 +9763,10 @@ void main() { expect(widget.selectionControls, equals(materialTextSelectionControls)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Can double click + drag with a mouse to select word by word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -9551,10 +9812,10 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Can double tap + drag to select word by word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -9617,11 +9878,11 @@ void main() { 'all good people\n' // 22 + 16 => 38 'to come to the aid\n' // 38 + 19 => 57 'of their country.'; // 57 + 17 => 74 - testWidgets( + testWidgetsWithLeakTracking( 'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge', (WidgetTester tester) async { // TODO(Renzo-Olivares): Enable for iOS, currently broken because selection overlay blocks the TextSelectionGestureDetector https://github.com/flutter/flutter/issues/123415. - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( @@ -9675,10 +9936,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple tap to select a paragraph on mobile platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( @@ -9731,10 +9992,121 @@ void main() { variant: TargetPlatformVariant.mobile(), ); - testWidgets( + testWidgetsWithLeakTracking( + 'Triple click at the beginning of a line should not select the previous paragraph', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132126 + final TextEditingController controller = _textEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + maxLines: null, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextField), testValueB); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueB); + + final Offset thirdLinePos = textOffsetToPosition(tester, 38); + + // Click on text field to gain focus, and move the selection. + final TestGesture gesture = await tester.startGesture(thirdLinePos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.isCollapsed, true); + expect(controller.selection.baseOffset, 38); + + // Here we click on same position again, to register a double click. This will select + // the word at the clicked position. + await gesture.down(thirdLinePos); + await gesture.up(); + + expect(controller.selection.baseOffset, 38); + expect(controller.selection.extentOffset, 40); + + // Here we click on same position again, to register a triple click. This will select + // the paragraph at the clicked position. + await gesture.down(thirdLinePos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(controller.selection.baseOffset, 38); + expect(controller.selection.extentOffset, 57); + }, + variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), + ); + + testWidgetsWithLeakTracking( + 'Triple click at the end of text should select the previous paragraph', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132126. + final TextEditingController controller = _textEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + dragStartBehavior: DragStartBehavior.down, + controller: controller, + maxLines: null, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextField), testValueB); + await skipPastScrollingAnimation(tester); + expect(controller.value.text, testValueB); + + final Offset endOfTextPos = textOffsetToPosition(tester, 74); + + // Click on text field to gain focus, and move the selection. + final TestGesture gesture = await tester.startGesture(endOfTextPos, kind: PointerDeviceKind.mouse); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.isCollapsed, true); + expect(controller.selection.baseOffset, 74); + + // Here we click on same position again, to register a double click. + await gesture.down(endOfTextPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + expect(controller.selection.baseOffset, 74); + expect(controller.selection.extentOffset, 74); + + // Here we click on same position again, to register a triple click. This will select + // the paragraph at the clicked position. + await gesture.down(endOfTextPos); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(controller.selection.baseOffset, 57); + expect(controller.selection.extentOffset, 74); + }, + variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), + ); + + testWidgetsWithLeakTracking( 'triple tap chains work on Non-Apple mobile platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -9822,10 +10194,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets( + testWidgetsWithLeakTracking( 'triple tap chains work on Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.', ); await tester.pumpWidget( @@ -9854,7 +10226,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); await tester.pumpAndSettle(kDoubleTapTimeout); @@ -9878,7 +10250,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Third tap shows the toolbar and selects the paragraph. await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); @@ -9887,7 +10259,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 36), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); await tester.tapAt(textfieldStart + const Offset(150.0, 50.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -9904,7 +10276,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 44, extentOffset: 50), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Third tap selects the paragraph and shows the toolbar. await tester.tapAt(textfieldStart + const Offset(150.0, 50.0)); @@ -9913,15 +10285,15 @@ void main() { controller.selection, const TextSelection(baseOffset: 36, extentOffset: 66), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'triple click chains work', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); await tester.pumpWidget( @@ -10039,10 +10411,10 @@ void main() { variant: TargetPlatformVariant.desktop(), ); - testWidgets( + testWidgetsWithLeakTracking( 'triple click after a click on desktop platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); await tester.pumpWidget( @@ -10107,10 +10479,10 @@ void main() { variant: TargetPlatformVariant.desktop(), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple tap to select all on a single-line textfield on mobile platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueB, ); final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; @@ -10162,10 +10534,10 @@ void main() { variant: TargetPlatformVariant.mobile(), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple click to select all on a single-line textfield on desktop platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); @@ -10218,10 +10590,10 @@ void main() { variant: TargetPlatformVariant.desktop(), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple click to select a line on Linux', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -10277,10 +10649,10 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.linux), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple click to select a paragraph', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -10336,10 +10708,10 @@ void main() { variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple click + drag to select line by line on Linux', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -10437,10 +10809,10 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.linux), ); - testWidgets( + testWidgetsWithLeakTracking( 'Can triple click + drag to select paragraph by paragraph', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -10538,10 +10910,10 @@ void main() { variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Going past triple click retains the selection on Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); await tester.pumpWidget( @@ -10625,10 +10997,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); await tester.pumpWidget( @@ -10739,10 +11111,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }), ); - testWidgets( + testWidgetsWithLeakTracking( 'Double click and triple click alternate on Windows', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: testValueA, ); await tester.pumpWidget( @@ -10857,10 +11229,10 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'double tap on top of cursor also selects word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -10908,11 +11280,11 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double double tap just shows the selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: '', + final TextEditingController controller = _textEditingController( + ); await tester.pumpWidget( MaterialApp( @@ -10943,11 +11315,11 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets( + testWidgetsWithLeakTracking( 'double long press just shows the selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: '', + final TextEditingController controller = _textEditingController( + ); await tester.pumpWidget( MaterialApp( @@ -10974,11 +11346,11 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets( + testWidgetsWithLeakTracking( 'A single tap hides the selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: '', + final TextEditingController controller = _textEditingController( + ); await tester.pumpWidget( MaterialApp( @@ -11005,8 +11377,8 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets('Drag selection hides the selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Drag selection hides the selection menu', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( @@ -11055,11 +11427,11 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'Long press on an autofocused field shows the selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: '', + final TextEditingController controller = _textEditingController( + ); await tester.pumpWidget( MaterialApp( @@ -11086,10 +11458,10 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu., ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap hold selects word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11105,6 +11477,7 @@ void main() { ); final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -11118,8 +11491,8 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); - // Selected text shows 3 toolbar buttons. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + // Selected text shows 4 toolbar buttons on iOS, and 3 on macOS. + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); await gesture.up(); await tester.pump(); @@ -11129,16 +11502,17 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); + // The toolbar is still showing. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'tap after a double tap select is not affected', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -11185,10 +11559,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press moves cursor to the exact long press position and shows toolbar when the field is focused', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11229,10 +11603,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press that starts on an unfocused TextField selects the word at the exact long press position and shows toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11248,6 +11622,7 @@ void main() { ); final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); await tester.pumpAndSettle(); @@ -11261,16 +11636,16 @@ void main() { // Collapsed toolbar shows 3 buttons. expect( find.byType(CupertinoButton), - isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3), + isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3) ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press selects word and shows toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11301,10 +11676,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press tap cannot initiate a double tap', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11347,10 +11722,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11425,10 +11800,10 @@ void main() { variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag on a focused TextField moves the cursor under the drag and shows toolbar on lift', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11501,10 +11876,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag on an unfocused TextField selects word-by-word and shows toolbar on lift', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -11520,6 +11895,7 @@ void main() { ); final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; final TestGesture gesture = await tester.startGesture(textfieldStart + const Offset(50.0, 9.0)); @@ -11564,16 +11940,13 @@ void main() { const TextSelection(baseOffset: 0, extentOffset: 23), ); // The toolbar now shows up. - expect( - find.byType(CupertinoButton), - isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3), - ); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -11661,8 +12034,8 @@ void main() { expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('long press drag can edge scroll on Apple platforms - unfocused TextField', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('long press drag can edge scroll on Apple platforms - unfocused TextField', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -11679,6 +12052,7 @@ void main() { ); final RenderEditable renderEditable = findRenderEditable(tester); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection( const TextSelection.collapsed(offset: 66), // Last character's position. @@ -11732,7 +12106,7 @@ void main() { const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream), ); // The toolbar now shows up. - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); lastCharEndpoint = renderEditable.getEndpointsForSelection( const TextSelection.collapsed(offset: 66), // Last character's position. @@ -11750,8 +12124,8 @@ void main() { expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('long press drag can edge scroll on Apple platforms - focused TextField', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('long press drag can edge scroll on Apple platforms - focused TextField', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -11847,8 +12221,8 @@ void main() { expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('mouse click and drag can edge scroll', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('mouse click and drag can edge scroll', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -11916,8 +12290,8 @@ void main() { expect(textOffsetToPosition(tester, 0).dx, lessThan(-100.0)); }, variant: TargetPlatformVariant.all()); - testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('keyboard selection change scrolls the field', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -11988,7 +12362,7 @@ void main() { ); testWidgets('long press drag can edge scroll vertically', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -12066,8 +12440,8 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('keyboard selection change scrolls the field vertically', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('keyboard selection change scrolls the field vertically', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -12140,8 +12514,8 @@ void main() { skip: isBrowser, // [intended] Browser handles arrow keys differently. ); - testWidgets('mouse click and drag can edge scroll vertically', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('mouse click and drag can edge scroll vertically', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', ); await tester.pumpWidget( @@ -12219,10 +12593,10 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets( + testWidgetsWithLeakTracking( 'long tap after a double tap select is not affected', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -12272,10 +12646,10 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap after a long tap is not affected', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. @@ -12325,15 +12699,15 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformMobile ? findsNWidgets(6) : findsNWidgets(3)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double click after a click on desktop platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12389,10 +12763,10 @@ void main() { variant: TargetPlatformVariant.desktop(), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap chains work', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12409,6 +12783,7 @@ void main() { ); final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -12422,7 +12797,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); // Double tap selecting the same word somewhere else is fine. await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); @@ -12442,7 +12817,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -12458,15 +12833,15 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformIOS ? findsNWidgets(6) : findsNWidgets(3)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double click chains work', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12555,8 +12930,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), ); - testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('double tapping a space selects the previous word on iOS', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: ' blah blah \n blah', ); await tester.pumpWidget( @@ -12626,8 +13001,8 @@ void main() { expect(controller.value.selection.extentOffset, 14); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('selecting a space selects the space on non-iOS platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('selecting a space selects the space on non-iOS platforms', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: ' blah blah', ); await tester.pumpWidget( @@ -12681,8 +13056,8 @@ void main() { expect(controller.value.selection.extentOffset, 1); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.android })); - testWidgets('selecting a space selects the space on Desktop platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('selecting a space selects the space on Desktop platforms', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: ' blah blah', ); await tester.pumpWidget( @@ -12753,8 +13128,8 @@ void main() { expect(controller.value.selection.extentOffset, 1); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux })); - testWidgets('Force press does not set selection on Android or Fuchsia touch devices', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Force press does not set selection on Android or Fuchsia touch devices', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12796,8 +13171,8 @@ void main() { expect(find.byType(TextButton), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia })); - testWidgets('Force press sets selection on desktop platforms that do not support it', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Force press sets selection on desktop platforms that do not support it', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12839,8 +13214,8 @@ void main() { expect(find.byType(TextButton), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('force press selects word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('force press selects word', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -12885,11 +13260,11 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); + expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(6)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('tap on non-force-press-supported devices work', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget(Container(key: GlobalKey())); @@ -12944,7 +13319,7 @@ void main() { // https://github.com/flutter/flutter/issues/43445 }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets('default TextField debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default TextField debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TextField().debugFillProperties(builder); @@ -12956,7 +13331,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('TextField implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); // Not checking controller, inputFormatters, focusNode @@ -13014,7 +13389,7 @@ void main() { ]); }); - testWidgets( + testWidgetsWithLeakTracking( 'strut basic single line', (WidgetTester tester) async { await tester.pumpWidget( @@ -13039,7 +13414,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut TextStyle increases height', (WidgetTester tester) async { await tester.pumpWidget( @@ -13084,7 +13459,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut basic multi line', (WidgetTester tester) async { await tester.pumpWidget( @@ -13108,7 +13483,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut no force small strut', (WidgetTester tester) async { await tester.pumpWidget( @@ -13139,7 +13514,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut no force large strut', (WidgetTester tester) async { await tester.pumpWidget( @@ -13168,7 +13543,7 @@ void main() { skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 ); - testWidgets( + testWidgetsWithLeakTracking( 'strut height override', (WidgetTester tester) async { await tester.pumpWidget( @@ -13197,7 +13572,7 @@ void main() { skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 ); - testWidgets( + testWidgetsWithLeakTracking( 'strut forces field taller', (WidgetTester tester) async { await tester.pumpWidget( @@ -13228,7 +13603,7 @@ void main() { skip: isBrowser, // TODO(mdebbar): https://github.com/flutter/flutter/issues/32243 ); - testWidgets('Caret center position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret center position', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: Theme( @@ -13271,7 +13646,7 @@ void main() { expect(topLeft.dx, equals(383)); }); - testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: Theme( @@ -13323,9 +13698,9 @@ void main() { expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position' }); - testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection handles are rendered and not faded away', (WidgetTester tester) async { const String testText = 'lorem ipsum'; - final TextEditingController controller = TextEditingController(text: testText); + final TextEditingController controller = _textEditingController(text: testText); await tester.pumpWidget( MaterialApp( @@ -13356,9 +13731,9 @@ void main() { expect(right.opacity.value, equals(1.0)); }); - testWidgets('iOS selection handles are rendered and not faded away', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iOS selection handles are rendered and not faded away', (WidgetTester tester) async { const String testText = 'lorem ipsum'; - final TextEditingController controller = TextEditingController(text: testText); + final TextEditingController controller = _textEditingController(text: testText); await tester.pumpWidget( MaterialApp( @@ -13387,9 +13762,9 @@ void main() { expect(right.opacity.value, equals(1.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('iPad Scribble selection change shows selection handles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('iPad Scribble selection change shows selection handles', (WidgetTester tester) async { const String testText = 'lorem ipsum'; - final TextEditingController controller = TextEditingController(text: testText); + final TextEditingController controller = _textEditingController(text: testText); await tester.pumpWidget( MaterialApp( @@ -13419,8 +13794,8 @@ void main() { expect(right.opacity.value, equals(1.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Tap shows handles but not toolbar', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13441,10 +13816,10 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); }); - testWidgets( + testWidgetsWithLeakTracking( 'Tap in empty text field does not show handles nor toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -13464,8 +13839,8 @@ void main() { }, ); - testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Long press shows handles and toolbar', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13486,10 +13861,10 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'Long press in empty text field shows handles and toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -13509,8 +13884,8 @@ void main() { }, ); - testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Double tap shows handles and toolbar', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13533,10 +13908,10 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'Double tap in empty text field shows toolbar but not handles', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( @@ -13558,10 +13933,10 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse tap does not show handles nor toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13590,10 +13965,10 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse long press does not show handles nor toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13622,10 +13997,10 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse double tap does not show handles nor toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13658,8 +14033,8 @@ void main() { }, ); - testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Does not show handles when updated from the web engine', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13700,8 +14075,8 @@ void main() { } }); - testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Tapping selection handles toggles the toolbar', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'abc def ghi', ); @@ -13741,8 +14116,9 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); }); - testWidgets('when TextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when TextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), @@ -13771,9 +14147,10 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/74566 - testWidgets('TextField and last input character are visible on the screen when the cursor is not shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField and last input character are visible on the screen when the cursor is not shown', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); final ScrollController textFieldScrollController = ScrollController(); + addTearDown(() { scrollController.dispose(); textFieldScrollController.dispose(); }); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), @@ -13816,7 +14193,7 @@ void main() { }); group('height', () { - testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async { + testWidgetsWithLeakTracking('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(), home: const Scaffold( @@ -13830,7 +14207,7 @@ void main() { expect(renderBox.size.height, greaterThanOrEqualTo(kMinInteractiveDimension)); }); - testWidgets("When text is very small, TextField still doesn't go below kMinInteractiveDimension height", (WidgetTester tester) async { + testWidgetsWithLeakTracking("When text is very small, TextField still doesn't go below kMinInteractiveDimension height", (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(), home: const Scaffold( @@ -13846,7 +14223,7 @@ void main() { expect(renderBox.size.height, kMinInteractiveDimension); }); - testWidgets('When isDense, TextField can go below kMinInteractiveDimension height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When isDense, TextField can go below kMinInteractiveDimension height', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData(), home: const Scaffold( @@ -13891,14 +14268,14 @@ void main() { ); } - testWidgets('By default, intrinsic height is at least kMinInteractiveDimension high', (WidgetTester tester) async { + testWidgetsWithLeakTracking('By default, intrinsic height is at least kMinInteractiveDimension high', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/54729 // If the intrinsic height does not match that of the height after // performLayout, this will fail. await tester.pumpWidget(buildTest(isDense: false)); }); - testWidgets('When isDense, intrinsic height can go below kMinInteractiveDimension height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When isDense, intrinsic height can go below kMinInteractiveDimension height', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/54729 // If the intrinsic height does not match that of the height after // performLayout, this will fail. @@ -13907,17 +14284,25 @@ void main() { }); }); testWidgets("Arrow keys don't move input focus", (WidgetTester tester) async { - final TextEditingController controller1 = TextEditingController(); - final TextEditingController controller2 = TextEditingController(); - final TextEditingController controller3 = TextEditingController(); - final TextEditingController controller4 = TextEditingController(); - final TextEditingController controller5 = TextEditingController(); + final TextEditingController controller1 = _textEditingController(); + final TextEditingController controller2 = _textEditingController(); + final TextEditingController controller3 = _textEditingController(); + final TextEditingController controller4 = _textEditingController(); + final TextEditingController controller5 = _textEditingController(); final FocusNode focusNode1 = FocusNode(debugLabel: 'Field 1'); final FocusNode focusNode2 = FocusNode(debugLabel: 'Field 2'); final FocusNode focusNode3 = FocusNode(debugLabel: 'Field 3'); final FocusNode focusNode4 = FocusNode(debugLabel: 'Field 4'); final FocusNode focusNode5 = FocusNode(debugLabel: 'Field 5'); + addTearDown(() { + focusNode1.dispose(); + focusNode2.dispose(); + focusNode3.dispose(); + focusNode4.dispose(); + focusNode5.dispose(); + }); + // Lay out text fields in a "+" formation, and focus the center one. await tester.pumpWidget(MaterialApp( theme: ThemeData(), @@ -13995,7 +14380,7 @@ void main() { expect(focusNode3.hasPrimaryFocus, isTrue); }); - testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async { bool scrollInvoked = false; await tester.pumpWidget( MaterialApp( @@ -14031,7 +14416,7 @@ void main() { expect(scrollInvoked, isFalse); }); - testWidgets("A buildCounter that returns null doesn't affect the size of the TextField", (WidgetTester tester) async { + testWidgetsWithLeakTracking("A buildCounter that returns null doesn't affect the size of the TextField", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/44909 final GlobalKey textField1Key = GlobalKey(); @@ -14066,7 +14451,7 @@ void main() { (WidgetTester tester) async { // This is a regression test for // https://github.com/flutter/flutter/issues/43787 - final TextEditingController controller = TextEditingController( + final TextEditingController controller = _textEditingController( text: 'This is a test that shows some odd behavior with Text Selection!', ); @@ -14102,8 +14487,8 @@ void main() { }, ); - testWidgets('clipboard status is checked via hasStrings without getting the full clipboard contents', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('clipboard status is checked via hasStrings without getting the full clipboard contents', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -14146,9 +14531,11 @@ void main() { expect(calledGetData, false); // hasStrings is checked in order to decide if the content can be pasted. expect(calledHasStrings, true); - }); + }, + skip: kIsWeb, // [intended] web doesn't call hasStrings. + ); - testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -14222,7 +14609,7 @@ void main() { await gesture.moveTo(center); }); - testWidgets('TextField icons change mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField icons change mouse cursor when hovered', (WidgetTester tester) async { // Test default cursor in icons area. await tester.pumpWidget( const MaterialApp( @@ -14310,8 +14697,8 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets('Text selection menu does not change mouse cursor when hovered', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Text selection menu does not change mouse cursor when hovered', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -14348,12 +14735,12 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('Caret rtl with changing width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret rtl with changing width', (WidgetTester tester) async { late StateSetter setState; bool isWide = false; const double wideWidth = 300.0; const double narrowWidth = 200.0; - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( boilerplate( child: StatefulBuilder( @@ -14407,8 +14794,8 @@ void main() { expect(cursorRight, inputWidth - kCaretGap); }); - testWidgets('Text selection menu hides after select all on desktop', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Text selection menu hides after select all on desktop', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -14458,8 +14845,8 @@ void main() { ); // Regressing test for https://github.com/flutter/flutter/issues/70625 - testWidgets('TextFields can inherit [FloatingLabelBehaviour] from InputDecorationTheme.', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('TextFields can inherit [FloatingLabelBehaviour] from InputDecorationTheme.', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); Widget textFieldBuilder({ FloatingLabelBehavior behavior = FloatingLabelBehavior.auto }) { return MaterialApp( theme: ThemeData( @@ -14532,7 +14919,7 @@ void main() { await tester.pumpAndSettle(); } - testWidgets('using none enforcement.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using none enforcement.', (WidgetTester tester) async { const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none; await setupWidget(tester, enforcement); @@ -14552,7 +14939,7 @@ void main() { expect(state.currentTextEditingValue.composing, TextRange.empty); }); - testWidgets('using enforced.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using enforced.', (WidgetTester tester) async { const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced; await setupWidget(tester, enforcement); @@ -14576,7 +14963,7 @@ void main() { expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); }); - testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using truncateAfterCompositionEnds.', (WidgetTester tester) async { const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds; await setupWidget(tester, enforcement); @@ -14600,7 +14987,7 @@ void main() { expect(state.currentTextEditingValue.composing, TextRange.empty); }); - testWidgets('using default behavior for different platforms.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using default behavior for different platforms.', (WidgetTester tester) async { await setupWidget(tester, null); final EditableTextState state = tester.state(find.byType(EditableText)); @@ -14633,7 +15020,7 @@ void main() { }); }); - testWidgets('TextField does not leak touch events when deadline has exceeded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField does not leak touch events when deadline has exceeded', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/118340. int textFieldTapCount = 0; int prefixTapCount = 0; @@ -14684,7 +15071,7 @@ void main() { expect(suffixTapCount, 1); }); - testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/39376. int textFieldTapCount = 0; @@ -14723,7 +15110,7 @@ void main() { expect(suffixTapCount, 1); }); - testWidgets('autofill info has hint text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autofill info has hint text', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -14746,7 +15133,7 @@ void main() { ); }); - testWidgets('TextField at rest does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField at rest does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -14760,8 +15147,8 @@ void main() { expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); }); - testWidgets('Focused TextField does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Focused TextField does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); await tester.pumpWidget( MaterialApp( home: Material( @@ -14777,8 +15164,8 @@ void main() { expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); }); - testWidgets('TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); await tester.pumpWidget( MaterialApp( home: Material( @@ -14815,8 +15202,8 @@ void main() { expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. - testWidgets('cursor blinking respects TickerMode', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('cursor blinking respects TickerMode', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); Widget builder({required bool tickerMode}) { return MaterialApp( home: Material( @@ -14895,8 +15282,8 @@ void main() { expect(editable.showCursor.value, isTrue); }); - testWidgets('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap to select with a keyboard (Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -14938,8 +15325,8 @@ void main() { expect(controller.selection.extentOffset, 4); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('can shift + tap to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); await tester.pumpWidget( @@ -14983,11 +15370,11 @@ void main() { expect(controller.selection.extentOffset, 4); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('shift tapping an unfocused field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('shift tapping an unfocused field', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); - final FocusNode focusNode = FocusNode(); + final FocusNode focusNode = _focusNode(); await tester.pumpWidget( MaterialApp( home: Material( @@ -15038,8 +15425,8 @@ void main() { expect(controller.selection.extentOffset, 20); }, variant: TargetPlatformVariant.all()); - testWidgets('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; @@ -15143,8 +15530,8 @@ void main() { expect(controller.selection.extentOffset, 26); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android @@ -15251,8 +15638,8 @@ void main() { expect(controller.selection.extentOffset, 26); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); - testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; @@ -15355,8 +15742,8 @@ void main() { expect(controller.selection.extentOffset, 14); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android @@ -15464,8 +15851,8 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); // Regression test for https://github.com/flutter/flutter/issues/101587. - testWidgets('Right clicking menu behavior', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Right clicking menu behavior', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( @@ -15543,8 +15930,8 @@ void main() { skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); - testWidgets('Cannot request focus when canRequestFocus is false', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Cannot request focus when canRequestFocus is false', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); // Default test. The canRequestFocus is true by default and the text field can be focused await tester.pumpWidget( @@ -15586,10 +15973,10 @@ void main() { }); group('Right click focus', () { - testWidgets('Can right click to focus multiple times', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can right click to focus multiple times', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/103228 - final FocusNode focusNode1 = FocusNode(); - final FocusNode focusNode2 = FocusNode(); + final FocusNode focusNode1 = _focusNode(); + final FocusNode focusNode2 = _focusNode(); final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); await tester.pumpWidget( @@ -15641,10 +16028,10 @@ void main() { expect(focusNode2.hasFocus, isFalse); }); - testWidgets('Can right click to focus on previously selected word on Apple platforms', (WidgetTester tester) async { - final FocusNode focusNode1 = FocusNode(); - final FocusNode focusNode2 = FocusNode(); - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('Can right click to focus on previously selected word on Apple platforms', (WidgetTester tester) async { + final FocusNode focusNode1 = _focusNode(); + final FocusNode focusNode2 = _focusNode(); + final TextEditingController controller = _textEditingController( text: 'first second', ); final UniqueKey key1 = UniqueKey(); @@ -15740,8 +16127,8 @@ void main() { expect(controller.selection.extentOffset, 5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Right clicking cannot request focus if canRequestFocus is false', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Right clicking cannot request focus if canRequestFocus is false', (WidgetTester tester) async { + final FocusNode focusNode = _focusNode(); final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -15770,8 +16157,8 @@ void main() { }); group('context menu', () { - testWidgets('builds AdaptiveTextSelectionToolbar by default', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: ''); + testWidgetsWithLeakTracking('builds AdaptiveTextSelectionToolbar by default', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( home: Material( @@ -15801,9 +16188,9 @@ void main() { skip: kIsWeb, // [intended] on web the browser handles the context menu. ); - testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); - final TextEditingController controller = TextEditingController(text: ''); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( home: Material( @@ -15839,9 +16226,9 @@ void main() { skip: kIsWeb, // [intended] on web the browser handles the context menu. ); - testWidgets('contextMenuBuilder changes from default to null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contextMenuBuilder changes from default to null', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); - final TextEditingController controller = TextEditingController(text: ''); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget(MaterialApp(home: Material(child: TextField(key: key, controller: controller)))); await tester.pump(); // Wait for autofocus to take effect. @@ -15978,10 +16365,10 @@ void main() { late ValueNotifier<MagnifierInfo> magnifierInfo; final Widget fakeMagnifier = Container(key: UniqueKey()); - testWidgets( + testWidgetsWithLeakTracking( 'Can drag handles to show, unshow, and update magnifier', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( overlay( child: TextField( @@ -16042,8 +16429,8 @@ void main() { expect(find.byKey(fakeMagnifier.key!), findsNothing); }); - testWidgets('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); await tester.pumpWidget( MaterialApp( home: Material( @@ -16143,8 +16530,8 @@ void main() { expect(find.byKey(fakeMagnifier.key!), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController(); final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; await tester.pumpWidget( MaterialApp( @@ -16207,7 +16594,7 @@ void main() { expect(find.byKey(fakeMagnifier.key!), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets('magnifier does not show when tapping outside field', (WidgetTester tester) async { + testWidgetsWithLeakTracking('magnifier does not show when tapping outside field', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/128321 await tester.pumpWidget( MaterialApp( @@ -16257,8 +16644,9 @@ void main() { }); group('TapRegion integration', () { - testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping outside loses focus on desktop', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -16291,8 +16679,9 @@ void main() { expect(focusNode.hasPrimaryFocus, isFalse); }, variant: TargetPlatformVariant.desktop()); - testWidgets("Tapping outside doesn't lose focus on mobile", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Tapping outside doesn't lose focus on mobile", (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -16326,9 +16715,10 @@ void main() { expect(focusNode.hasPrimaryFocus, kIsWeb ? isFalse : isTrue); }, variant: TargetPlatformVariant.mobile()); - testWidgets("Tapping on toolbar doesn't lose focus", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Tapping on toolbar doesn't lose focus", (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); - final TextEditingController controller = TextEditingController(text: 'A B C'); + final TextEditingController controller = _textEditingController(text: 'A B C'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -16380,8 +16770,9 @@ void main() { skip: isBrowser, // [intended] On the web, the toolbar isn't rendered by Flutter. ); - testWidgets("Tapping on input decorator doesn't lose focus", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Tapping on input decorator doesn't lose focus", (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -16419,8 +16810,9 @@ void main() { // PointerDownEvents can't be trackpad events, apparently, so we skip that one. for (final PointerDeviceKind pointerDeviceKind in PointerDeviceKind.values.toSet()..remove(PointerDeviceKind.trackpad)) { - testWidgets('Default TextField handling of onTapOutside follows platform conventions for ${pointerDeviceKind.name}', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default TextField handling of onTapOutside follows platform conventions for ${pointerDeviceKind.name}', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -16474,7 +16866,7 @@ void main() { } }); - testWidgets('Builds the corresponding default spell check toolbar by platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Builds the corresponding default spell check toolbar by platform', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; late final BuildContext builderContext; @@ -16526,7 +16918,7 @@ void main() { } }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets('Builds the corresponding default spell check configuration by platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Builds the corresponding default spell check configuration by platform', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; @@ -16584,8 +16976,8 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets('text selection toolbar is hidden on tap down', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( + testWidgetsWithLeakTracking('text selection toolbar is hidden on tap down', (WidgetTester tester) async { + final TextEditingController controller = _textEditingController( text: 'blah1 blah2', ); await tester.pumpWidget( diff --git a/packages/flutter/test/material/text_form_field_restoration_test.dart b/packages/flutter/test/material/text_form_field_restoration_test.dart index fd9d365e234ef..570e8fa11c001 100644 --- a/packages/flutter/test/material/text_form_field_restoration_test.dart +++ b/packages/flutter/test/material/text_form_field_restoration_test.dart @@ -4,12 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const String text = 'Hello World! How are you? Life is good!'; const String alternativeText = 'Everything is awesome!!'; void main() { - testWidgets('TextField restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField restoration', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -20,7 +21,7 @@ void main() { await restoreAndVerify(tester); }); - testWidgets('TextField restoration with external controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField restoration with external controller', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'root', @@ -33,7 +34,7 @@ void main() { await restoreAndVerify(tester); }); - testWidgets('State restoration (No Form ancestor) - onUserInteraction error text validation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('State restoration (No Form ancestor) - onUserInteraction error text validation', (WidgetTester tester) async { String? errorText(String? value) => '$value/error'; late GlobalKey<FormFieldState<String>> formState; @@ -91,7 +92,7 @@ void main() { expect(find.text(errorText('bar')!), findsOneWidget); }); - testWidgets('State Restoration (No Form ancestor) - validator sets the error text only when validate is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('State Restoration (No Form ancestor) - validator sets the error text only when validate is called', (WidgetTester tester) async { String? errorText(String? value) => '$value/error'; late GlobalKey<FormFieldState<String>> formState; diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index b46266268ce22..b3bdc07fc76b8 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -11,8 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart'; @@ -27,10 +26,11 @@ void main() { await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); }); - testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -103,10 +103,11 @@ void main() { skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); - testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -249,10 +250,40 @@ void main() { skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); - testWidgets('the desktop cut/copy/paste buttons are disabled for read-only obscured form fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking( + '$SelectionOverlay is not leaking', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + addTearDown(controller.dispose); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + controller: controller, + ), + ), + ), + ), + ); + + final Offset startBlah1 = textOffsetToPosition(tester, 0); + await tester.tapAt(startBlah1); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tapAt(startBlah1); + await tester.pumpAndSettle(); + await tester.pump(); + }, + skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. + ); + + testWidgetsWithLeakTracking('the desktop cut/copy/paste buttons are disabled for read-only obscured form fields', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -293,10 +324,11 @@ void main() { skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); - testWidgets('the desktop cut/copy buttons are disabled for obscured form fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the desktop cut/copy buttons are disabled for obscured form fields', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -344,7 +376,7 @@ void main() { skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); - testWidgets('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async { bool asserted; try { TextFormField( @@ -357,7 +389,7 @@ void main() { expect(asserted, false); }); - testWidgets('Passes textAlign to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes textAlign to underlying TextField', (WidgetTester tester) async { const TextAlign alignment = TextAlign.center; await tester.pumpWidget( @@ -379,7 +411,7 @@ void main() { expect(textFieldWidget.textAlign, alignment); }); - testWidgets('Passes scrollPhysics to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes scrollPhysics to underlying TextField', (WidgetTester tester) async { const ScrollPhysics scrollPhysics = ScrollPhysics(); await tester.pumpWidget( @@ -401,7 +433,7 @@ void main() { expect(textFieldWidget.scrollPhysics, scrollPhysics); }); - testWidgets('Passes textAlignVertical to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes textAlignVertical to underlying TextField', (WidgetTester tester) async { const TextAlignVertical textAlignVertical = TextAlignVertical.bottom; await tester.pumpWidget( @@ -423,7 +455,7 @@ void main() { expect(textFieldWidget.textAlignVertical, textAlignVertical); }); - testWidgets('Passes textInputAction to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes textInputAction to underlying TextField', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -443,7 +475,7 @@ void main() { expect(textFieldWidget.textInputAction, TextInputAction.next); }); - testWidgets('Passes onEditingComplete to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes onEditingComplete to underlying TextField', (WidgetTester tester) async { void onEditingComplete() { } await tester.pumpWidget( @@ -465,7 +497,7 @@ void main() { expect(textFieldWidget.onEditingComplete, onEditingComplete); }); - testWidgets('Passes cursor attributes to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes cursor attributes to underlying TextField', (WidgetTester tester) async { const double cursorWidth = 3.14; const double cursorHeight = 6.28; const Radius cursorRadius = Radius.circular(4); @@ -496,7 +528,7 @@ void main() { expect(textFieldWidget.cursorColor, cursorColor); }); - testWidgets('onFieldSubmit callbacks are called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onFieldSubmit callbacks are called', (WidgetTester tester) async { bool called = false; await tester.pumpWidget( @@ -517,7 +549,7 @@ void main() { expect(called, true); }); - testWidgets('onChanged callbacks are called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onChanged callbacks are called', (WidgetTester tester) async { late String value; await tester.pumpWidget( @@ -539,7 +571,7 @@ void main() { expect(value, 'Soup'); }); - testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autovalidateMode is passed to super', (WidgetTester tester) async { int validateCalled = 0; await tester.pumpWidget( @@ -564,7 +596,7 @@ void main() { expect(validateCalled, 2); }); - testWidgets('validate is called if widget is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('validate is called if widget is enabled', (WidgetTester tester) async { int validateCalled = 0; await tester.pumpWidget( @@ -591,7 +623,7 @@ void main() { }); - testWidgets('Disabled field hides helper and counter in M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled field hides helper and counter in M2', (WidgetTester tester) async { const String helperText = 'helper text'; const String counterText = 'counter text'; const String errorText = 'error text'; @@ -639,7 +671,7 @@ void main() { expect(errorWidget.style!.color, equals(Colors.transparent)); }); - testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passing a buildCounter shows returned widget', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Center( @@ -704,7 +736,7 @@ void main() { expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); }, skip: isBrowser); // [intended] We do not use Flutter-rendered context menu on the Web. - testWidgets('onTap is called upon tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap is called upon tap', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget( MaterialApp( @@ -731,7 +763,7 @@ void main() { expect(tapCount, 3); }); - testWidgets('onTapOutside is called upon tap outside', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTapOutside is called upon tap outside', (WidgetTester tester) async { int tapOutsideCount = 0; await tester.pumpWidget( MaterialApp( @@ -762,7 +794,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/54472. - testWidgets('reset resets the text fields value to the initialValue', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reset resets the text fields value to the initialValue', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -784,8 +816,33 @@ void main() { expect(find.text('initialValue'), findsOneWidget); }); + testWidgetsWithLeakTracking('reset resets the text fields value to the controller initial value', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'initialValue'); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextFormField( + controller: controller, + ), + ), + ), + ), + ); + + await tester.enterText(find.byType(TextFormField), 'changedValue'); + + final FormFieldState<String> state = tester.state<FormFieldState<String>>(find.byType(TextFormField)); + state.reset(); + + expect(find.text('changedValue'), findsNothing); + expect(find.text('initialValue'), findsOneWidget); + }); + // Regression test for https://github.com/flutter/flutter/issues/34847. - testWidgets("didChange resets the text field's value to empty when passed null", (WidgetTester tester) async { + testWidgetsWithLeakTracking("didChange resets the text field's value to empty when passed null", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -808,7 +865,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/34847. - testWidgets("reset resets the text field's value to empty when initialValue is null", (WidgetTester tester) async { + testWidgetsWithLeakTracking("reset resets the text field's value to empty when initialValue is null", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -831,7 +888,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/54472. - testWidgets('didChange changes text fields value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didChange changes text fields value', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -853,7 +910,7 @@ void main() { expect(find.text('changedValue'), findsOneWidget); }); - testWidgets('onChanged callbacks value and FormFieldState.value are sync', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onChanged callbacks value and FormFieldState.value are sync', (WidgetTester tester) async { bool called = false; late FormFieldState<String> state; @@ -880,7 +937,7 @@ void main() { expect(called, true); }); - testWidgets('autofillHints is passed to super', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autofillHints is passed to super', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -897,7 +954,7 @@ void main() { expect(widget.autofillHints, equals(const <String>[AutofillHints.countryName])); }); - testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autovalidateMode is passed to super', (WidgetTester tester) async { int validateCalled = 0; await tester.pumpWidget( @@ -922,7 +979,7 @@ void main() { expect(validateCalled, 1); }); - testWidgets('textSelectionControls is passed to super', (WidgetTester tester) async { + testWidgetsWithLeakTracking('textSelectionControls is passed to super', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -939,7 +996,7 @@ void main() { expect(widget.selectionControls, equals(materialTextSelectionControls)); }); - testWidgets('TextFormField respects hintTextDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextFormField respects hintTextDirection', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Directionality( @@ -981,8 +1038,9 @@ void main() { expect(textDirection, TextDirection.rtl); }); - testWidgets('Passes scrollController to underlying TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes scrollController to underlying TextField', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( @@ -1003,7 +1061,7 @@ void main() { expect(textFieldWidget.scrollController, scrollController); }); - testWidgets('TextFormField changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextFormField changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -1067,10 +1125,11 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/101587. - testWidgets('Right clicking menu behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Right clicking menu behavior', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -1148,7 +1207,7 @@ void main() { skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); - testWidgets('spellCheckConfiguration passes through to EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('spellCheckConfiguration passes through to EditableText', (WidgetTester tester) async { final SpellCheckConfiguration mySpellCheckConfiguration = SpellCheckConfiguration( spellCheckService: DefaultSpellCheckService(), misspelledTextStyle: TextField.materialMisspelledTextStyle, @@ -1178,7 +1237,7 @@ void main() { ); }); - testWidgets('magnifierConfiguration passes through to EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('magnifierConfiguration passes through to EditableText', (WidgetTester tester) async { final TextMagnifierConfiguration myTextMagnifierConfiguration = TextMagnifierConfiguration( magnifierBuilder: (BuildContext context, MagnifierController controller, ValueNotifier<MagnifierInfo> notifier) { return const Placeholder(); @@ -1199,8 +1258,9 @@ void main() { expect(editableText.magnifierConfiguration, equals(myTextMagnifierConfiguration)); }); - testWidgets('Passes undoController to undoController TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes undoController to undoController TextField', (WidgetTester tester) async { final UndoHistoryController undoController = UndoHistoryController(value: UndoHistoryValue.empty); + addTearDown(undoController.dispose); await tester.pumpWidget( MaterialApp( @@ -1221,7 +1281,7 @@ void main() { expect(textFieldWidget.undoController, undoController); }); - testWidgets('Passes cursorOpacityAnimates to cursorOpacityAnimates TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes cursorOpacityAnimates to cursorOpacityAnimates TextField', (WidgetTester tester) async { const bool cursorOpacityAnimates = true; await tester.pumpWidget( @@ -1243,7 +1303,7 @@ void main() { expect(textFieldWidget.cursorOpacityAnimates, cursorOpacityAnimates); }); - testWidgets('Passes contentInsertionConfiguration to contentInsertionConfiguration TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes contentInsertionConfiguration to contentInsertionConfiguration TextField', (WidgetTester tester) async { final ContentInsertionConfiguration contentInsertionConfiguration = ContentInsertionConfiguration(onContentInserted: (KeyboardInsertedContent value) {}); @@ -1266,7 +1326,7 @@ void main() { expect(textFieldWidget.contentInsertionConfiguration, contentInsertionConfiguration); }); - testWidgets('Passes clipBehavior to clipBehavior TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes clipBehavior to clipBehavior TextField', (WidgetTester tester) async { const Clip clipBehavior = Clip.antiAlias; await tester.pumpWidget( @@ -1288,7 +1348,7 @@ void main() { expect(textFieldWidget.clipBehavior, clipBehavior); }); - testWidgets('Passes scribbleEnabled to scribbleEnabled TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes scribbleEnabled to scribbleEnabled TextField', (WidgetTester tester) async { const bool scribbleEnabled = false; await tester.pumpWidget( @@ -1310,7 +1370,7 @@ void main() { expect(textFieldWidget.scribbleEnabled, scribbleEnabled); }); - testWidgets('Passes canRequestFocus to canRequestFocus TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes canRequestFocus to canRequestFocus TextField', (WidgetTester tester) async { const bool canRequestFocus = false; await tester.pumpWidget( @@ -1332,7 +1392,7 @@ void main() { expect(textFieldWidget.canRequestFocus, canRequestFocus); }); - testWidgets('Passes onAppPrivateCommand to onAppPrivateCommand TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes onAppPrivateCommand to onAppPrivateCommand TextField', (WidgetTester tester) async { void onAppPrivateCommand(String p0, Map<String, dynamic> p1) {} await tester.pumpWidget( @@ -1354,7 +1414,7 @@ void main() { expect(textFieldWidget.onAppPrivateCommand, onAppPrivateCommand); }); - testWidgets('Passes selectionHeightStyle to selectionHeightStyle TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes selectionHeightStyle to selectionHeightStyle TextField', (WidgetTester tester) async { const BoxHeightStyle selectionHeightStyle = BoxHeightStyle.max; await tester.pumpWidget( @@ -1376,7 +1436,7 @@ void main() { expect(textFieldWidget.selectionHeightStyle, selectionHeightStyle); }); - testWidgets('Passes selectionWidthStyle to selectionWidthStyle TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes selectionWidthStyle to selectionWidthStyle TextField', (WidgetTester tester) async { const BoxWidthStyle selectionWidthStyle = BoxWidthStyle.max; await tester.pumpWidget( @@ -1398,7 +1458,7 @@ void main() { expect(textFieldWidget.selectionWidthStyle, selectionWidthStyle); }); - testWidgets('Passes dragStartBehavior to dragStartBehavior TextField', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Passes dragStartBehavior to dragStartBehavior TextField', (WidgetTester tester) async { const DragStartBehavior dragStartBehavior = DragStartBehavior.down; await tester.pumpWidget( @@ -1420,7 +1480,7 @@ void main() { expect(textFieldWidget.dragStartBehavior, dragStartBehavior); }); - testWidgets('Error color for cursor while validating', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Error color for cursor while validating', (WidgetTester tester) async { const Color errorColor = Color(0xff123456); await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -1443,4 +1503,41 @@ void main() { await tester.pump(); expect(textField.cursorColor, errorColor); }); + + testWidgetsWithLeakTracking('TextFormField onChanged is called when the form is reset', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/123009. + final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>(); + final GlobalKey<FormState> formKey = GlobalKey<FormState>(); + String value = 'initialValue'; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Form( + key: formKey, + child: TextFormField( + key: stateKey, + initialValue: value, + onChanged: (String newValue) { + value = newValue; + }, + ), + ), + ), + )); + + // Initial value is 'initialValue'. + expect(stateKey.currentState!.value, 'initialValue'); + expect(value, 'initialValue'); + + // Change value to 'changedValue'. + await tester.enterText(find.byType(TextField), 'changedValue'); + expect(stateKey.currentState!.value,'changedValue'); + expect(value, 'changedValue'); + + // Should be back to 'initialValue' when the form is reset. + formKey.currentState!.reset(); + await tester.pump(); + expect(stateKey.currentState!.value,'initialValue'); + expect(value, 'initialValue'); + }); } diff --git a/packages/flutter/test/material/text_selection_test.dart b/packages/flutter/test/material/text_selection_test.dart index 37c22e7ecbfb8..bcef8636f7d2b 100644 --- a/packages/flutter/test/material/text_selection_test.dart +++ b/packages/flutter/test/material/text_selection_test.dart @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize, textOffsetToPosition; @@ -45,11 +46,15 @@ void main() { }) { final TextEditingController controller = TextEditingController(text: text) ..selection = selection ?? const TextSelection.collapsed(offset: -1); + addTearDown(controller.dispose); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + return MaterialApp( home: EditableText( key: key, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.black, backgroundCursorColor: Colors.black, @@ -57,13 +62,13 @@ void main() { ); } - testWidgets('should return false when there is no text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should return false when there is no text', (WidgetTester tester) async { final GlobalKey<EditableTextState> key = GlobalKey(); await tester.pumpWidget(createEditableText(key: key)); expect(materialTextSelectionControls.canSelectAll(key.currentState!), false); }); - testWidgets('should return true when there is text and collapsed selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should return true when there is text and collapsed selection', (WidgetTester tester) async { final GlobalKey<EditableTextState> key = GlobalKey(); await tester.pumpWidget(createEditableText( key: key, @@ -72,7 +77,7 @@ void main() { expect(materialTextSelectionControls.canSelectAll(key.currentState!), true); }); - testWidgets('should return true when there is text and partial uncollapsed selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should return true when there is text and partial uncollapsed selection', (WidgetTester tester) async { final GlobalKey<EditableTextState> key = GlobalKey(); await tester.pumpWidget(createEditableText( key: key, @@ -82,7 +87,7 @@ void main() { expect(materialTextSelectionControls.canSelectAll(key.currentState!), true); }); - testWidgets('should return false when there is text and full selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should return false when there is text and full selection', (WidgetTester tester) async { final GlobalKey<EditableTextState> key = GlobalKey(); await tester.pumpWidget(createEditableText( key: key, @@ -94,8 +99,9 @@ void main() { }); group('Text selection menu overflow (Android)', () { - testWidgets('All menu items show when they fit.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('All menu items show when they fit.', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'abc def ghi'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -156,12 +162,13 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets("When menu items don't fit, an overflow menu is used.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("When menu items don't fit, an overflow menu is used.", (WidgetTester tester) async { // Set the screen size to more narrow, so that Select all can't fit. tester.view.physicalSize = const Size(1000, 800); addTearDown(tester.view.reset); final TextEditingController controller = TextEditingController(text: 'abc def ghi'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -230,12 +237,13 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets('A smaller menu bumps more items to the overflow menu.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A smaller menu bumps more items to the overflow menu.', (WidgetTester tester) async { // Set the screen size so narrow that only Cut and Copy can fit. tester.view.physicalSize = const Size(800, 800); addTearDown(tester.view.reset); final TextEditingController controller = TextEditingController(text: 'abc def ghi'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -295,12 +303,13 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets('When the menu renders below the text, the overflow menu back button is at the top.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When the menu renders below the text, the overflow menu back button is at the top.', (WidgetTester tester) async { // Set the screen size to more narrow, so that Select all can't fit. tester.view.physicalSize = const Size(1000, 800); addTearDown(tester.view.reset); final TextEditingController controller = TextEditingController(text: 'abc def ghi'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -369,12 +378,13 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets('When the menu items change, the menu is closed and _closedWidth reset.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When the menu items change, the menu is closed and _closedWidth reset.', (WidgetTester tester) async { // Set the screen size to more narrow, so that Select all can't fit. tester.view.physicalSize = const Size(1000, 800); addTearDown(tester.view.reset); final TextEditingController controller = TextEditingController(text: 'abc def ghi'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android, useMaterial3: false), home: Directionality( @@ -477,8 +487,9 @@ void main() { }); group('menu position', () { - testWidgets('When renders below a block of text, menu appears below bottom endpoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('When renders below a block of text, menu appears below bottom endpoint', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -549,11 +560,12 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), ); - testWidgets( + testWidgetsWithLeakTracking( 'When selecting multiple lines over max lines', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Directionality( @@ -627,7 +639,7 @@ void main() { }); group('material handles', () { - testWidgets('draws transparent handle correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('draws transparent handle correctly', (WidgetTester tester) async { await tester.pumpWidget(RepaintBoundary( child: Theme( data: ThemeData( @@ -661,7 +673,7 @@ void main() { ); }); - testWidgets('works with 3 positional parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with 3 positional parameters', (WidgetTester tester) async { await tester.pumpWidget(Theme( data: ThemeData( textSelectionTheme: const TextSelectionThemeData( @@ -694,10 +706,11 @@ void main() { }); }); - testWidgets('Paste only appears when clipboard has contents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paste only appears when clipboard has contents', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Material( diff --git a/packages/flutter/test/material/text_selection_theme_test.dart b/packages/flutter/test/material/text_selection_theme_test.dart index bc80531b162b7..8234f29daec4c 100644 --- a/packages/flutter/test/material/text_selection_theme_test.dart +++ b/packages/flutter/test/material/text_selection_theme_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('TextSelectionThemeData copyWith, ==, hashCode basics', () { @@ -27,7 +26,7 @@ void main() { expect(theme.selectionHandleColor, null); }); - testWidgets('Default TextSelectionThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default TextSelectionThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TextSelectionThemeData().debugFillProperties(builder); @@ -39,7 +38,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('TextSelectionThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSelectionThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TextSelectionThemeData( cursorColor: Color(0xffeeffaa), @@ -59,12 +58,60 @@ void main() { ]); }); - testWidgets('Empty textSelectionTheme will use defaults', (WidgetTester tester) async { - final ThemeData theme = ThemeData(); - final bool material3 = theme.useMaterial3; - final Color defaultCursorColor = material3 ? theme.colorScheme.primary : const Color(0xff2196f3); - final Color defaultSelectionColor = material3 ? theme.colorScheme.primary.withOpacity(0.40) : const Color(0x662196f3); - final Color defaultSelectionHandleColor = material3 ? theme.colorScheme.primary : const Color(0xff2196f3); + testWidgetsWithLeakTracking('Material2 - Empty textSelectionTheme will use defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); + const Color defaultCursorColor = Color(0xff2196f3); + const Color defaultSelectionColor = Color(0x662196f3); + const Color defaultSelectionHandleColor = Color(0xff2196f3); + + EditableText.debugDeterministicCursor = true; + addTearDown(() { + EditableText.debugDeterministicCursor = false; + }); + // Test TextField's cursor & selection color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextField(autofocus: true), + ), + ), + ); + await tester.pump(); + await tester.pumpAndSettle(); + + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, defaultCursorColor); + expect(renderEditable.selectionColor, defaultSelectionColor); + + // Test the selection handle color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Builder( + builder: (BuildContext context) { + return materialTextSelectionControls.buildHandle( + context, + TextSelectionHandleType.left, + 10.0, + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + final RenderBox handle = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); + expect(handle, paints..path(color: defaultSelectionHandleColor)); + }); + + testWidgetsWithLeakTracking('Material3 - Empty textSelectionTheme will use defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + final Color defaultCursorColor = theme.colorScheme.primary; + final Color defaultSelectionColor = theme.colorScheme.primary.withOpacity(0.40); + final Color defaultSelectionHandleColor = theme.colorScheme.primary; EditableText.debugDeterministicCursor = true; addTearDown(() { @@ -90,6 +137,7 @@ void main() { // Test the selection handle color. await tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Builder( builder: (BuildContext context) { @@ -108,7 +156,7 @@ void main() { expect(handle, paints..path(color: defaultSelectionHandleColor)); }); - testWidgets('ThemeData.textSelectionTheme will be used if provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.textSelectionTheme will be used if provided', (WidgetTester tester) async { const TextSelectionThemeData textSelectionTheme = TextSelectionThemeData( cursorColor: Color(0xffaabbcc), selectionColor: Color(0x88888888), @@ -161,7 +209,7 @@ void main() { expect(handle, paints..path(color: textSelectionTheme.selectionHandleColor)); }); - testWidgets('TextSelectionTheme widget will override ThemeData.textSelectionTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSelectionTheme widget will override ThemeData.textSelectionTheme', (WidgetTester tester) async { const TextSelectionThemeData defaultTextSelectionTheme = TextSelectionThemeData( cursorColor: Color(0xffaabbcc), selectionColor: Color(0x88888888), @@ -223,7 +271,7 @@ void main() { expect(handle, paints..path(color: widgetTextSelectionTheme.selectionHandleColor)); }); - testWidgets('TextField parameters will override theme settings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextField parameters will override theme settings', (WidgetTester tester) async { const TextSelectionThemeData defaultTextSelectionTheme = TextSelectionThemeData( cursorColor: Color(0xffaabbcc), selectionHandleColor: Color(0x00ccbbaa), @@ -272,7 +320,7 @@ void main() { expect(renderSelectable.cursorColor, cursorColor.withAlpha(0)); }); - testWidgets('TextSelectionThem overrides DefaultSelectionStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSelectionThem overrides DefaultSelectionStyle', (WidgetTester tester) async { const Color themeSelectionColor = Color(0xffaabbcc); const Color themeCursorColor = Color(0x00ccbbaa); const Color defaultSelectionColor = Color(0xffaa1111); diff --git a/packages/flutter/test/material/text_selection_toolbar_test.dart b/packages/flutter/test/material/text_selection_toolbar_test.dart index 33183cd826a4e..857ed6d0fcb3c 100644 --- a/packages/flutter/test/material/text_selection_toolbar_test.dart +++ b/packages/flutter/test/material/text_selection_toolbar_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; const double _kToolbarContentDistance = 8.0; @@ -75,7 +75,7 @@ void main() { Finder findOverflowButton() => findPrivate('_TextSelectionToolbarOverflowButton'); - testWidgets('puts children in an overflow menu if they overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('puts children in an overflow menu if they overflow', (WidgetTester tester) async { late StateSetter setState; final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox()); @@ -123,7 +123,7 @@ void main() { expect(findOverflowButton(), findsOneWidget); }); - testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at anchorAbove if it fits', (WidgetTester tester) async { late StateSetter setState; const double height = 44.0; const double anchorBelowY = 500.0; @@ -172,7 +172,7 @@ void main() { expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance)); }); - testWidgets('can create and use a custom toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can create and use a custom toolbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -203,4 +203,93 @@ void main() { expect(find.text('Paste'), findsNothing); expect(find.text('Select all'), findsNothing); }, skip: kIsWeb); // [intended] We don't show the toolbar on the web. + + for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) { + testWidgetsWithLeakTracking('default background color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme, + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbar( + anchorAbove: Offset.zero, + anchorBelow: Offset.zero, + children: <Widget>[ + TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + onPressed: () {}, + child: const Text('Custom button'), + ), + ], + ), + ), + ), + ), + ); + + Finder findToolbarContainer() { + return find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'), + matching: find.byType(Material), + ); + } + expect(findToolbarContainer(), findsAtLeastNWidgets(1)); + + final Material toolbarContainer = tester.widget(findToolbarContainer().first); + expect( + toolbarContainer.color, + // The default colors are hardcoded and don't take the default value of + // the theme's surface color. + switch (colorScheme.brightness) { + Brightness.light => const Color(0xffffffff), + Brightness.dark => const Color(0xff424242), + }, + ); + }); + + testWidgetsWithLeakTracking('custom background color', (WidgetTester tester) async { + const Color customBackgroundColor = Colors.red; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme.copyWith( + surface: customBackgroundColor, + ), + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbar( + anchorAbove: Offset.zero, + anchorBelow: Offset.zero, + children: <Widget>[ + TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + onPressed: () {}, + child: const Text('Custom button'), + ), + ], + ), + ), + ), + ), + ); + + Finder findToolbarContainer() { + return find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'), + matching: find.byType(Material), + ); + } + expect(findToolbarContainer(), findsAtLeastNWidgets(1)); + + final Material toolbarContainer = tester.widget(findToolbarContainer().first); + expect( + toolbarContainer.color, + customBackgroundColor, + ); + }); + } } diff --git a/packages/flutter/test/material/text_selection_toolbar_text_button_test.dart b/packages/flutter/test/material/text_selection_toolbar_text_button_test.dart index e584366920e0e..24de64a8000d2 100644 --- a/packages/flutter/test/material/text_selection_toolbar_text_button_test.dart +++ b/packages/flutter/test/material/text_selection_toolbar_text_button_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('position in the toolbar changes width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('position in the toolbar changes width', (WidgetTester tester) async { late StateSetter setState; int index = 1; int total = 3; @@ -60,4 +61,134 @@ void main() { expect(onlySize.width, greaterThan(firstSize.width)); expect(onlySize.width, greaterThan(lastSize.width)); }); + + for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) { + testWidgetsWithLeakTracking('foreground color by default', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme, + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + child: const Text('button'), + ), + ), + ), + ), + ); + + expect(find.byType(TextButton), findsOneWidget); + + final TextButton textButton = tester.widget(find.byType(TextButton)); + // The foreground color is hardcoded to black or white by default, not the + // default value from ColorScheme.onSurface. + expect( + textButton.style!.foregroundColor!.resolve(<MaterialState>{}), + switch (colorScheme.brightness) { + Brightness.light => const Color(0xff000000), + Brightness.dark => const Color(0xffffffff), + }, + ); + }); + + testWidgetsWithLeakTracking('custom foreground color', (WidgetTester tester) async { + const Color customForegroundColor = Colors.red; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme.copyWith( + onSurface: customForegroundColor, + ), + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + child: const Text('button'), + ), + ), + ), + ), + ); + + expect(find.byType(TextButton), findsOneWidget); + + final TextButton textButton = tester.widget(find.byType(TextButton)); + expect( + textButton.style!.foregroundColor!.resolve(<MaterialState>{}), + customForegroundColor, + ); + }); + + testWidgetsWithLeakTracking('background color by default', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/133027 + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme, + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + child: const Text('button'), + ), + ), + ), + ), + ); + + expect(find.byType(TextButton), findsOneWidget); + + final TextButton textButton = tester.widget(find.byType(TextButton)); + // The background color is hardcoded to transparent by default so the buttons + // are the color of the container behind them. For example TextSelectionToolbar + // hardcodes the color value, and TextSelectionToolbarTextButton that are its + // children should be that color. + expect( + textButton.style!.backgroundColor!.resolve(<MaterialState>{}), + Colors.transparent, + ); + }); + + testWidgetsWithLeakTracking('textButtonTheme should not override default background color', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/133027 + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: colorScheme, + textButtonTheme: const TextButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll<Color>(Colors.blue), + ), + ), + ), + home: Scaffold( + body: Center( + child: TextSelectionToolbarTextButton( + padding: TextSelectionToolbarTextButton.getPadding(0, 1), + child: const Text('button'), + ), + ), + ), + ), + ); + + expect(find.byType(TextButton), findsOneWidget); + + final TextButton textButton = tester.widget(find.byType(TextButton)); + // The background color is hardcoded to transparent by default so the buttons + // are the color of the container behind them. For example TextSelectionToolbar + // hardcodes the color value, and TextSelectionToolbarTextButton that are its + // children should be that color. + expect( + textButton.style!.backgroundColor!.resolve(<MaterialState>{}), + Colors.transparent, + ); + }); + } } diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index a69ff727cf270..4c9e7794845e8 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('Theme data control test', () { @@ -83,7 +84,7 @@ void main() { expect(fallbackTheme.typography, Typography.material2021(colorScheme: fallbackTheme.colorScheme)); }); - testWidgets('Defaults to MaterialTapTargetBehavior.padded on mobile platforms and MaterialTapTargetBehavior.shrinkWrap on desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defaults to MaterialTapTargetBehavior.padded on mobile platforms and MaterialTapTargetBehavior.shrinkWrap on desktop', (WidgetTester tester) async { final ThemeData themeData = ThemeData(platform: defaultTargetPlatform); switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -370,7 +371,7 @@ void main() { expect(theme.applyElevationOverlayColor, true); }); - testWidgets('ThemeData.from a light color scheme sets appropriate values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.from a light color scheme sets appropriate values', (WidgetTester tester) async { const ColorScheme lightColors = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: lightColors); @@ -385,7 +386,7 @@ void main() { expect(theme.applyElevationOverlayColor, isFalse); }); - testWidgets('ThemeData.from a dark color scheme sets appropriate values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.from a dark color scheme sets appropriate values', (WidgetTester tester) async { const ColorScheme darkColors = ColorScheme.dark(); final ThemeData theme = ThemeData.from(colorScheme: darkColors); @@ -401,7 +402,7 @@ void main() { expect(theme.applyElevationOverlayColor, isTrue); }); - testWidgets('splashFactory is InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('splashFactory is InkSparkle only for Android non-web when useMaterial3 is true', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); // Basic check that this theme is in fact using material 3. @@ -423,7 +424,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('splashFactory is InkSplash for every platform scenario, including Android non-web, when useMaterial3 is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('splashFactory is InkSplash for every platform scenario, including Android non-web, when useMaterial3 is false', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); switch (debugDefaultTargetPlatformOverride!) { @@ -437,7 +438,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('VisualDensity.adaptivePlatformDensity returns adaptive values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('VisualDensity.adaptivePlatformDensity returns adaptive values', (WidgetTester tester) async { switch (debugDefaultTargetPlatformOverride!) { case TargetPlatform.android: case TargetPlatform.iOS: @@ -450,7 +451,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('VisualDensity.getDensityForPlatform returns adaptive values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('VisualDensity.getDensityForPlatform returns adaptive values', (WidgetTester tester) async { switch (debugDefaultTargetPlatformOverride!) { case TargetPlatform.android: case TargetPlatform.iOS: @@ -463,7 +464,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('VisualDensity in ThemeData defaults to "compact" on desktop and "standard" on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('VisualDensity in ThemeData defaults to "compact" on desktop and "standard" on mobile', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); switch (debugDefaultTargetPlatformOverride!) { case TargetPlatform.android: @@ -477,7 +478,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('VisualDensity in ThemeData defaults to the right thing when a platform is supplied to it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('VisualDensity in ThemeData defaults to the right thing when a platform is supplied to it', (WidgetTester tester) async { final ThemeData themeData = ThemeData(platform: debugDefaultTargetPlatformOverride! == TargetPlatform.android ? TargetPlatform.linux : TargetPlatform.android); switch (debugDefaultTargetPlatformOverride!) { case TargetPlatform.iOS: @@ -491,7 +492,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('Ensure Visual Density effective constraints are clamped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ensure Visual Density effective constraints are clamped', (WidgetTester tester) async { const BoxConstraints square = BoxConstraints.tightFor(width: 35, height: 35); BoxConstraints expanded = const VisualDensity(horizontal: 4.0, vertical: 4.0).effectiveConstraints(square); expect(expanded.minWidth, equals(35)); @@ -519,7 +520,7 @@ void main() { expect(expanded.maxHeight, equals(4)); }); - testWidgets('Ensure Visual Density effective constraints expand and contract', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ensure Visual Density effective constraints expand and contract', (WidgetTester tester) async { const BoxConstraints square = BoxConstraints(); final BoxConstraints expanded = const VisualDensity(horizontal: 4.0, vertical: 4.0).effectiveConstraints(square); expect(expanded.minWidth, equals(16)); @@ -537,7 +538,7 @@ void main() { group('Theme extensions', () { const Key containerKey = Key('container'); - testWidgets('can be obtained', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can be obtained', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -564,7 +565,7 @@ void main() { expect(theme.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 50)); }); - testWidgets('can use copyWith', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use copyWith', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( @@ -587,7 +588,7 @@ void main() { expect(theme.extension<MyThemeExtensionA>()!.color2, Colors.amber); }); - testWidgets('can lerp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can lerp', (WidgetTester tester) async { const MyThemeExtensionA extensionA1 = MyThemeExtensionA( color1: Colors.black, color2: Colors.amber, @@ -663,7 +664,7 @@ void main() { expect(lerped.extension<MyThemeExtensionB>()!.textStyle, const TextStyle(fontSize: 100)); // Not lerped }); - testWidgets('should return null on extension not found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should return null on extension not found', (WidgetTester tester) async { final ThemeData theme = ThemeData( extensions: const <ThemeExtension<dynamic>>{}, ); @@ -695,7 +696,7 @@ void main() { expect(hoverColorBlack.hashCode != hoverColorWhite.hashCode, true); }); - testWidgets('ThemeData.copyWith correctly creates new ThemeData with all copied arguments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.copyWith correctly creates new ThemeData with all copied arguments', (WidgetTester tester) async { final SliderThemeData sliderTheme = SliderThemeData.fromPrimaryColors( primaryColor: Colors.black, primaryColorDark: Colors.black, @@ -808,7 +809,6 @@ void main() { toggleButtonsTheme: const ToggleButtonsThemeData(textStyle: TextStyle(color: Colors.black)), tooltipTheme: const TooltipThemeData(height: 100), // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator: AndroidOverscrollIndicator.glow, toggleableActiveColor: Colors.black, selectedRowColor: Colors.black, errorColor: Colors.black, @@ -927,7 +927,6 @@ void main() { tooltipTheme: const TooltipThemeData(height: 100), // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, toggleableActiveColor: Colors.white, selectedRowColor: Colors.white, errorColor: Colors.white, @@ -1029,7 +1028,6 @@ void main() { tooltipTheme: otherTheme.tooltipTheme, // DEPRECATED (newest deprecations at the bottom) - androidOverscrollIndicator: otherTheme.androidOverscrollIndicator, toggleableActiveColor: otherTheme.toggleableActiveColor, selectedRowColor: otherTheme.selectedRowColor, errorColor: otherTheme.errorColor, @@ -1132,7 +1130,6 @@ void main() { expect(themeDataCopy.tooltipTheme, equals(otherTheme.tooltipTheme)); // DEPRECATED (newest deprecations at the bottom) - expect(themeDataCopy.androidOverscrollIndicator, equals(otherTheme.androidOverscrollIndicator)); expect(themeDataCopy.toggleableActiveColor, equals(otherTheme.toggleableActiveColor)); expect(themeDataCopy.selectedRowColor, equals(otherTheme.selectedRowColor)); expect(themeDataCopy.errorColor, equals(otherTheme.errorColor)); @@ -1140,7 +1137,7 @@ void main() { expect(themeDataCopy.bottomAppBarColor, equals(otherTheme.bottomAppBarColor)); }); - testWidgets('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async { // This test makes sure that the ThemeData debug output doesn't get too // verbose, which has been a problem in the past. @@ -1155,12 +1152,12 @@ void main() { expect(lightTheme.toString().length, lessThan(200)); }); - testWidgets('ThemeData brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { const ColorScheme lightColors = ColorScheme.light(); expect(() => ThemeData(colorScheme: lightColors, brightness: Brightness.dark), throwsAssertionError); }); - testWidgets('ThemeData.copyWith brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.copyWith brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { const ColorScheme lightColors = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: lightColors).copyWith(brightness: Brightness.dark); @@ -1266,7 +1263,6 @@ void main() { 'toggleButtonsTheme', 'tooltipTheme', // DEPRECATED (newest deprecations at the bottom) - 'androidOverscrollIndicator', 'toggleableActiveColor', 'selectedRowColor', 'errorColor', diff --git a/packages/flutter/test/material/theme_defaults_test.dart b/packages/flutter/test/material/theme_defaults_test.dart index db8eec4b7e792..a6b94213a4722 100644 --- a/packages/flutter/test/material/theme_defaults_test.dart +++ b/packages/flutter/test/material/theme_defaults_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Duration defaultButtonDuration = Duration(milliseconds: 200); @@ -14,9 +15,8 @@ void main() { const ShapeBorder defaultFABShapeM3 = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); const EdgeInsets defaultFABPadding = EdgeInsets.zero; - testWidgets('theme: ThemeData.light(), enabled: true', (WidgetTester tester) async { - final ThemeData theme = ThemeData.light(); - final bool material3 = theme.useMaterial3; + testWidgetsWithLeakTracking('Material2 - theme: ThemeData.light(), enabled: true', (WidgetTester tester) async { + final ThemeData theme = ThemeData.light(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: theme, @@ -31,21 +31,48 @@ void main() { final RawMaterialButton raw = tester.widget<RawMaterialButton>(find.byType(RawMaterialButton)); expect(raw.enabled, true); - expect(raw.textStyle!.color, material3 ? theme.colorScheme.onPrimaryContainer : const Color(0xffffffff)); - expect(raw.fillColor, material3 ? theme.colorScheme.primaryContainer : const Color(0xff2196f3)); + expect(raw.textStyle!.color, const Color(0xffffffff)); + expect(raw.fillColor, const Color(0xff2196f3)); expect(raw.elevation, 6.0); - expect(raw.highlightElevation, material3 ? 6.0 : 12.0); + expect(raw.highlightElevation, 12.0); expect(raw.disabledElevation, 6.0); expect(raw.constraints, defaultFABConstraints); expect(raw.padding, defaultFABPadding); - expect(raw.shape, material3 ? defaultFABShapeM3 : defaultFABShape); + expect(raw.shape, defaultFABShape); expect(raw.animationDuration, defaultButtonDuration); expect(raw.materialTapTargetSize, MaterialTapTargetSize.padded); }); - testWidgets('theme: ThemeData.light(), enabled: false', (WidgetTester tester) async { - final ThemeData theme = ThemeData.light(); - final bool material3 = theme.useMaterial3; + testWidgetsWithLeakTracking('Material3 - theme: ThemeData.light(), enabled: true', (WidgetTester tester) async { + final ThemeData theme = ThemeData.light(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: FloatingActionButton( + onPressed: () { }, // button.enabled == true + child: const Icon(Icons.add), + ), + ), + ), + ); + + final RawMaterialButton raw = tester.widget<RawMaterialButton>(find.byType(RawMaterialButton)); + expect(raw.enabled, true); + expect(raw.textStyle!.color, theme.colorScheme.onPrimaryContainer); + expect(raw.fillColor, theme.colorScheme.primaryContainer); + expect(raw.elevation, 6.0); + expect(raw.highlightElevation, 6.0); + expect(raw.disabledElevation, 6.0); + expect(raw.constraints, defaultFABConstraints); + expect(raw.padding, defaultFABPadding); + expect(raw.shape, defaultFABShapeM3); + expect(raw.animationDuration, defaultButtonDuration); + expect(raw.materialTapTargetSize, MaterialTapTargetSize.padded); + }); + + testWidgetsWithLeakTracking('Material2 - theme: ThemeData.light(), enabled: false', (WidgetTester tester) async { + final ThemeData theme = ThemeData.light(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: theme, @@ -60,16 +87,46 @@ void main() { final RawMaterialButton raw = tester.widget<RawMaterialButton>(find.byType(RawMaterialButton)); expect(raw.enabled, false); - expect(raw.textStyle!.color, material3 ? theme.colorScheme.onPrimaryContainer : const Color(0xffffffff)); - expect(raw.fillColor, material3 ? theme.colorScheme.primaryContainer : const Color(0xff2196f3)); + expect(raw.textStyle!.color, const Color(0xffffffff)); + expect(raw.fillColor, const Color(0xff2196f3)); + // highlightColor, disabled button can't be pressed + // splashColor, disabled button doesn't splash + expect(raw.elevation, 6.0); + expect(raw.highlightElevation, 12.0); + expect(raw.disabledElevation, 6.0); + expect(raw.constraints, defaultFABConstraints); + expect(raw.padding, defaultFABPadding); + expect(raw.shape, defaultFABShape); + expect(raw.animationDuration, defaultButtonDuration); + expect(raw.materialTapTargetSize, MaterialTapTargetSize.padded); + }); + + testWidgetsWithLeakTracking('Material3 - theme: ThemeData.light(), enabled: false', (WidgetTester tester) async { + final ThemeData theme = ThemeData.light(useMaterial3: true); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Center( + child: FloatingActionButton( + onPressed: null, // button.enabled == false + child: Icon(Icons.add), + ), + ), + ), + ); + + final RawMaterialButton raw = tester.widget<RawMaterialButton>(find.byType(RawMaterialButton)); + expect(raw.enabled, false); + expect(raw.textStyle!.color, theme.colorScheme.onPrimaryContainer); + expect(raw.fillColor, theme.colorScheme.primaryContainer); // highlightColor, disabled button can't be pressed // splashColor, disabled button doesn't splash expect(raw.elevation, 6.0); - expect(raw.highlightElevation, material3 ? 6.0 : 12.0); + expect(raw.highlightElevation, 6.0); expect(raw.disabledElevation, 6.0); expect(raw.constraints, defaultFABConstraints); expect(raw.padding, defaultFABPadding); - expect(raw.shape, material3 ? defaultFABShapeM3 : defaultFABShape); + expect(raw.shape, defaultFABShapeM3); expect(raw.animationDuration, defaultButtonDuration); expect(raw.materialTapTargetSize, MaterialTapTargetSize.padded); }); diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart index 3a907941c33aa..1100bbe0c3a9f 100644 --- a/packages/flutter/test/material/theme_test.dart +++ b/packages/flutter/test/material/theme_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { const TextTheme defaultGeometryTheme = Typography.englishLike2014; @@ -19,7 +20,7 @@ void main() { expect(tween.lerp(0.25), equals(ThemeData.lerp(light, dark, 0.25))); }); - testWidgets('PopupMenu inherits app theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu inherits app theme', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -47,7 +48,7 @@ void main() { expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark)); }); - testWidgets('Theme overrides selection style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme overrides selection style', (WidgetTester tester) async { final Key key = UniqueKey(); const Color defaultSelectionColor = Color(0x11111111); const Color defaultCursorColor = Color(0x22222222); @@ -96,8 +97,7 @@ void main() { expect(tester.widget<EditableText>(find.byType(EditableText)).cursorColor, themeCursorColor); }); - testWidgets('Fallback theme', (WidgetTester tester) async { - // For material 2 + testWidgetsWithLeakTracking('Material2 - Fallback theme', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( Theme( @@ -112,8 +112,9 @@ void main() { ); expect(Theme.of(capturedContext), equals(ThemeData.localize(ThemeData.fallback(useMaterial3: false), defaultGeometryTheme))); + }); - // For material 3 + testWidgetsWithLeakTracking('Material3 - Fallback theme', (WidgetTester tester) async { late BuildContext capturedContextM3; await tester.pumpWidget( Theme( @@ -130,7 +131,7 @@ void main() { expect(Theme.of(capturedContextM3), equals(ThemeData.localize(ThemeData.fallback(useMaterial3: true), defaultGeometryThemeM3))); }); - testWidgets('ThemeData.localize memoizes the result', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ThemeData.localize memoizes the result', (WidgetTester tester) async { final ThemeData light = ThemeData.light(); final ThemeData dark = ThemeData.dark(); @@ -153,14 +154,17 @@ void main() { ); }); - testWidgets('ThemeData with null typography uses proper defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - ThemeData with null typography uses proper defaults', (WidgetTester tester) async { final ThemeData m2Theme = ThemeData(useMaterial3: false); expect(m2Theme.typography, Typography.material2014()); + }); + + testWidgetsWithLeakTracking('Material3 - ThemeData with null typography uses proper defaults', (WidgetTester tester) async { final ThemeData m3Theme = ThemeData(useMaterial3: true); expect(m3Theme.typography, Typography.material2021(colorScheme: m3Theme.colorScheme)); }); - testWidgets('PopupMenu inherits shadowed app theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PopupMenu inherits shadowed app theme', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/5572 final Key popupMenuButtonKey = UniqueKey(); await tester.pumpWidget( @@ -192,7 +196,7 @@ void main() { expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.light)); }); - testWidgets('DropdownMenu inherits shadowed app theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DropdownMenu inherits shadowed app theme', (WidgetTester tester) async { final Key dropdownMenuButtonKey = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -228,7 +232,7 @@ void main() { } }); - testWidgets('ModalBottomSheet inherits shadowed app theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ModalBottomSheet inherits shadowed app theme', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.dark), @@ -261,7 +265,7 @@ void main() { expect(Theme.of(tester.element(find.text('bottomSheet'))).brightness, equals(Brightness.light)); }); - testWidgets('Dialog inherits shadowed app theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dialog inherits shadowed app theme', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -295,7 +299,7 @@ void main() { expect(Theme.of(tester.element(find.text('dialog'))).brightness, equals(Brightness.light)); }); - testWidgets("Scaffold inherits theme's scaffoldBackgroundColor", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Scaffold inherits theme's scaffoldBackgroundColor", (WidgetTester tester) async { const Color green = Color(0xFF00FF00); await tester.pumpWidget( @@ -337,7 +341,7 @@ void main() { expect(materials[1].color, green); // dialog scaffold }); - testWidgets('IconThemes are applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconThemes are applied', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(iconTheme: const IconThemeData(color: Colors.green, size: 10.0)), @@ -370,7 +374,7 @@ void main() { expect(glyphText.text.style!.fontSize, 20.0); }); - testWidgets( + testWidgetsWithLeakTracking( 'Same ThemeData reapplied does not trigger descendants rebuilds', (WidgetTester tester) async { testBuildCalled = 0; @@ -405,7 +409,7 @@ void main() { }, ); - testWidgets('Text geometry set in Theme has higher precedence than that of Localizations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text geometry set in Theme has higher precedence than that of Localizations', (WidgetTester tester) async { const double kMagicFontSize = 4321.0; final ThemeData fallback = ThemeData.fallback(); final ThemeData customTheme = fallback.copyWith( @@ -436,7 +440,7 @@ void main() { expect(actualFontSize, kMagicFontSize); }); - testWidgets('Default Theme provides all basic TextStyle properties - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Default Theme provides all basic TextStyle properties', (WidgetTester tester) async { late ThemeData theme; await tester.pumpWidget(Theme( data: ThemeData(useMaterial3: false), @@ -494,7 +498,7 @@ void main() { expect(theme.textTheme.displayLarge!.debugLabel, '(englishLike displayLarge 2014).merge(blackMountainView displayLarge)'); }); - testWidgets('Default Theme provides all basic TextStyle properties - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Default Theme provides all basic TextStyle properties', (WidgetTester tester) async { late ThemeData theme; await tester.pumpWidget(Theme( data: ThemeData(useMaterial3: true), @@ -579,9 +583,8 @@ void main() { context = null; }); - testWidgets('Default light theme has defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Default light theme has defaults', (WidgetTester tester) async { final CupertinoThemeData themeM2 = await testTheme(tester, ThemeData(useMaterial3: false)); - final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData(useMaterial3: true)); expect(themeM2.brightness, Brightness.light); expect(themeM2.primaryColor, Colors.blue); @@ -589,6 +592,10 @@ void main() { expect(themeM2.primaryContrastingColor, Colors.white); expect(themeM2.textTheme.textStyle.fontFamily, '.SF Pro Text'); expect(themeM2.textTheme.textStyle.fontSize, 17.0); + }); + + testWidgetsWithLeakTracking('Material3 - Default light theme has defaults', (WidgetTester tester) async { + final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData(useMaterial3: true)); expect(themeM3.brightness, Brightness.light); expect(themeM3.primaryColor, const Color(0xff6750a4)); @@ -598,9 +605,8 @@ void main() { expect(themeM3.textTheme.textStyle.fontSize, 17.0); }); - testWidgets('Dark theme has defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Dark theme has defaults', (WidgetTester tester) async { final CupertinoThemeData themeM2 = await testTheme(tester, ThemeData.dark(useMaterial3: false)); - final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData.dark(useMaterial3: true)); expect(themeM2.brightness, Brightness.dark); expect(themeM2.primaryColor, Colors.blue); @@ -608,6 +614,10 @@ void main() { expect(themeM2.scaffoldBackgroundColor, Colors.grey[850]); expect(themeM2.textTheme.textStyle.fontFamily, '.SF Pro Text'); expect(themeM2.textTheme.textStyle.fontSize, 17.0); + }); + + testWidgetsWithLeakTracking('Material3 - Dark theme has defaults', (WidgetTester tester) async { + final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData.dark(useMaterial3: true)); expect(themeM3.brightness, Brightness.dark); expect(themeM3.primaryColor, const Color(0xffd0bcff)); @@ -617,7 +627,7 @@ void main() { expect(themeM3.textTheme.textStyle.fontSize, 17.0); }); - testWidgets('MaterialTheme overrides the brightness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialTheme overrides the brightness', (WidgetTester tester) async { await testTheme(tester, ThemeData.dark()); expect(CupertinoTheme.brightnessOf(context!), Brightness.dark); @@ -638,7 +648,7 @@ void main() { expect(CupertinoTheme.brightnessOf(context!), Brightness.light); }); - testWidgets('Can override material theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Can override material theme', (WidgetTester tester) async { final CupertinoThemeData themeM2 = await testTheme(tester, ThemeData( cupertinoOverrideTheme: const CupertinoThemeData( scaffoldBackgroundColor: CupertinoColors.lightBackgroundGray, @@ -654,7 +664,9 @@ void main() { expect(themeM2.scaffoldBackgroundColor, CupertinoColors.lightBackgroundGray); expect(themeM2.textTheme.textStyle.fontFamily, '.SF Pro Text'); expect(themeM2.textTheme.textStyle.fontSize, 17.0); + }); + testWidgetsWithLeakTracking('Material3 - Can override material theme', (WidgetTester tester) async { final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData( cupertinoOverrideTheme: const CupertinoThemeData( scaffoldBackgroundColor: CupertinoColors.lightBackgroundGray, @@ -672,7 +684,7 @@ void main() { expect(themeM3.textTheme.textStyle.fontSize, 17.0); }); - testWidgets('Can override properties that are independent of material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Can override properties that are independent of material', (WidgetTester tester) async { final CupertinoThemeData themeM2 = await testTheme(tester, ThemeData( cupertinoOverrideTheme: const CupertinoThemeData( // The bar colors ignore all things material except brightness. @@ -684,7 +696,9 @@ void main() { expect(themeM2.primaryColor, Colors.blue); // MaterialBasedCupertinoThemeData should also function like a normal CupertinoThemeData. expect(themeM2.barBackgroundColor, CupertinoColors.black); + }); + testWidgetsWithLeakTracking('Material3 - Can override properties that are independent of material', (WidgetTester tester) async { final CupertinoThemeData themeM3 = await testTheme(tester, ThemeData( cupertinoOverrideTheme: const CupertinoThemeData( // The bar colors ignore all things material except brightness. @@ -698,7 +712,7 @@ void main() { expect(themeM3.barBackgroundColor, CupertinoColors.black); }); - testWidgets('Changing material theme triggers rebuilds - M2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Changing material theme triggers rebuilds', (WidgetTester tester) async { CupertinoThemeData themeM2 = await testTheme(tester, ThemeData( useMaterial3: false, primarySwatch: Colors.red, @@ -716,7 +730,7 @@ void main() { expect(themeM2.primaryColor, Colors.orange); }); - testWidgets('Changing material theme triggers rebuilds - M3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Changing material theme triggers rebuilds', (WidgetTester tester) async { CupertinoThemeData themeM3 = await testTheme(tester, ThemeData( useMaterial3: true, colorScheme: const ColorScheme.light( @@ -738,15 +752,15 @@ void main() { expect(themeM3.primaryColor, Colors.orange); }); - testWidgets( + testWidgetsWithLeakTracking( "CupertinoThemeData does not override material theme's icon theme", (WidgetTester tester) async { const Color materialIconColor = Colors.blue; const Color cupertinoIconColor = Colors.black; await testTheme(tester, ThemeData( - iconTheme: const IconThemeData(color: materialIconColor), - cupertinoOverrideTheme: const CupertinoThemeData(primaryColor: cupertinoIconColor), + iconTheme: const IconThemeData(color: materialIconColor), + cupertinoOverrideTheme: const CupertinoThemeData(primaryColor: cupertinoIconColor), )); expect(buildCount, 1); @@ -754,7 +768,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Changing cupertino theme override triggers rebuilds', (WidgetTester tester) async { CupertinoThemeData theme = await testTheme(tester, ThemeData( @@ -779,7 +793,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Cupertino theme override blocks derivative changes', (WidgetTester tester) async { CupertinoThemeData theme = await testTheme(tester, ThemeData( @@ -806,8 +820,8 @@ void main() { }, ); - testWidgets( - 'Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden - M2', + testWidgetsWithLeakTracking( + 'Material2 - Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden', (WidgetTester tester) async { CupertinoThemeData theme = await testTheme(tester, ThemeData( useMaterial3: false, @@ -835,8 +849,8 @@ void main() { }, ); - testWidgets( - 'Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden - M3', + testWidgetsWithLeakTracking( + 'Material3 - Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden', (WidgetTester tester) async { CupertinoThemeData theme = await testTheme(tester, ThemeData( useMaterial3: true, @@ -868,8 +882,8 @@ void main() { }, ); - testWidgets( - 'copyWith only copies the overrides, not the material or cupertino derivatives - M2', + testWidgetsWithLeakTracking( + 'Material2 - copyWith only copies the overrides, not the material or cupertino derivatives', (WidgetTester tester) async { final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData( useMaterial3: false, @@ -895,8 +909,8 @@ void main() { }, ); - testWidgets( - 'copyWith only copies the overrides, not the material or cupertino derivatives - M3', + testWidgetsWithLeakTracking( + 'Material3 - copyWith only copies the overrides, not the material or cupertino derivatives', (WidgetTester tester) async { final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData( useMaterial3: true, @@ -922,8 +936,8 @@ void main() { }, ); - testWidgets( - "Material themes with no cupertino overrides can also be copyWith'ed - M2", + testWidgetsWithLeakTracking( + "Material2 - Material themes with no cupertino overrides can also be copyWith'ed", (WidgetTester tester) async { final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData( useMaterial3: false, @@ -945,8 +959,8 @@ void main() { }, ); - testWidgets( - "Material themes with no cupertino overrides can also be copyWith'ed - M3", + testWidgetsWithLeakTracking( + "Material3 - Material themes with no cupertino overrides can also be copyWith'ed", (WidgetTester tester) async { final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData( useMaterial3: true, @@ -1140,6 +1154,7 @@ class _TextStyleProxy implements TextStyle { TextAlign? textAlign, TextDirection? textDirection, double textScaleFactor = 1.0, + TextScaler textScaler = TextScaler.noScaling, String? ellipsis, int? maxLines, ui.TextHeightBehavior? textHeightBehavior, @@ -1155,7 +1170,7 @@ class _TextStyleProxy implements TextStyle { } @override - ui.TextStyle getTextStyle({ double textScaleFactor = 1.0 }) { + ui.TextStyle getTextStyle({ double textScaleFactor = 1.0, TextScaler textScaler = TextScaler.noScaling }) { throw UnimplementedError(); } diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 1c1360c9cf71d..cb875d6fe4f45 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -11,29 +11,286 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; void main() { - for (final MaterialType materialType in MaterialType.values) { - final String selectTimeString; - final String enterTimeString; - final String cancelString; - const String okString = 'OK'; - const String amString = 'AM'; - const String pmString = 'PM'; - switch (materialType) { - case MaterialType.material2: - selectTimeString = 'SELECT TIME'; - enterTimeString = 'ENTER TIME'; - cancelString = 'CANCEL'; - case MaterialType.material3: - selectTimeString = 'Select time'; - enterTimeString = 'Enter time'; - cancelString = 'Cancel'; + const String okString = 'OK'; + const String amString = 'AM'; + const String pmString = 'PM'; + Material getMaterialFromDialog(WidgetTester tester) { + return tester.widget<Material>(find.descendant(of: find.byType(Dialog), matching: find.byType(Material)).first); + } + + testWidgetsWithLeakTracking('Material2 - Dialog size - dial mode', (WidgetTester tester) async { + addTearDown(tester.view.reset); + + const Size timePickerPortraitSize = Size(310, 468); + const Size timePickerLandscapeSize = Size(524, 342); + const Size timePickerLandscapeSizeM2 = Size(508, 300); + const EdgeInsets padding = EdgeInsets.fromLTRB(8, 18, 8, 8); + double width; + double height; + + // portrait + tester.view.physicalSize = const Size(800, 800.5); + tester.view.devicePixelRatio = 1; + await mediaQueryBoilerplate(tester, materialType: MaterialType.material2); + + width = timePickerPortraitSize.width + padding.horizontal; + height = timePickerPortraitSize.height + padding.vertical; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + + await tester.tap(find.text(okString)); // dismiss the dialog + await tester.pumpAndSettle(); + + // landscape + tester.view.physicalSize = const Size(800.5, 800); + tester.view.devicePixelRatio = 1; + await mediaQueryBoilerplate( + tester, + alwaysUse24HourFormat: true, + materialType: MaterialType.material2, + ); + + width = timePickerLandscapeSize.width + padding.horizontal; + height = timePickerLandscapeSizeM2.height + padding.vertical; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + }); + + testWidgets('Material2 - Dialog size - input mode', (WidgetTester tester) async { + const TimePickerEntryMode entryMode = TimePickerEntryMode.input; + const Size timePickerInputSize = Size(312, 216); + const Size dayPeriodPortraitSize = Size(52, 80); + const EdgeInsets padding = EdgeInsets.fromLTRB(8, 18, 8, 8); + final double height = timePickerInputSize.height + padding.vertical; + double width; + + await mediaQueryBoilerplate( + tester, + entryMode: entryMode, + materialType: MaterialType.material2, + ); + + width = timePickerInputSize.width + padding.horizontal; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + + await tester.tap(find.text(okString)); // dismiss the dialog + await tester.pumpAndSettle(); + + await mediaQueryBoilerplate( + tester, + alwaysUse24HourFormat: true, + entryMode: entryMode, + materialType: MaterialType.material2, + ); + width = timePickerInputSize.width - dayPeriodPortraitSize.width - 12 + padding.horizontal + 16; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + }); + + testWidgets('Material2 - respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: MaterialType.material2); + + final List<String> labels00To22 = List<String>.generate(12, (int index) { + return (index * 2).toString().padLeft(2, '0'); + }); + final CustomPaint dialPaint = tester.widget(findDialPaint); + final dynamic dialPainter = dialPaint.painter; + // ignore: avoid_dynamic_calls + final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; + // ignore: avoid_dynamic_calls + expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22); + + // ignore: avoid_dynamic_calls + final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; + // ignore: avoid_dynamic_calls + expect(selectedLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22); + }); + + testWidgets('Material3 - Dialog size - dial mode', (WidgetTester tester) async { + addTearDown(tester.view.reset); + + const Size timePickerPortraitSize = Size(310, 468); + const Size timePickerLandscapeSize = Size(524, 342); + const EdgeInsets padding = EdgeInsets.all(24.0); + double width; + double height; + + // portrait + tester.view.physicalSize = const Size(800, 800.5); + tester.view.devicePixelRatio = 1; + await mediaQueryBoilerplate(tester, materialType: MaterialType.material3); + + width = timePickerPortraitSize.width + padding.horizontal; + height = timePickerPortraitSize.height + padding.vertical; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + + await tester.tap(find.text(okString)); // dismiss the dialog + await tester.pumpAndSettle(); + + // landscape + tester.view.physicalSize = const Size(800.5, 800); + tester.view.devicePixelRatio = 1; + await mediaQueryBoilerplate( + tester, + alwaysUse24HourFormat: true, + materialType: MaterialType.material3, + ); + + width = timePickerLandscapeSize.width + padding.horizontal; + height = timePickerLandscapeSize.height + padding.vertical; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + }); + + testWidgets('Material3 - Dialog size - input mode', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + const TimePickerEntryMode entryMode = TimePickerEntryMode.input; + const double textScaleFactor = 1.0; + const Size timePickerMinInputSize = Size(312, 216); + const Size dayPeriodPortraitSize = Size(52, 80); + const EdgeInsets padding = EdgeInsets.all(24.0); + final double height = timePickerMinInputSize.height * textScaleFactor + padding.vertical; + double width; + + await mediaQueryBoilerplate( + tester, + entryMode: entryMode, + materialType: MaterialType.material3, + ); + + width = timePickerMinInputSize.width - (theme.useMaterial3 ? 32 : 0) + padding.horizontal; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + + await tester.tap(find.text(okString)); // dismiss the dialog + await tester.pumpAndSettle(); + + await mediaQueryBoilerplate( + tester, + alwaysUse24HourFormat: true, + entryMode: entryMode, + materialType: MaterialType.material3, + ); + + width = timePickerMinInputSize.width - dayPeriodPortraitSize.width - 12 + padding.horizontal; + expect( + tester.getSize(find.byWidget(getMaterialFromDialog(tester))), + Size(width, height), + ); + }); + + testWidgets('Material3 - respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: MaterialType.material3); + + final List<String> labels00To23 = List<String>.generate(24, (int index) { + return index == 0 ? '00' : index.toString(); + }); + final List<bool> inner0To23 = List<bool>.generate(24, (int index) => index >= 12); + + final CustomPaint dialPaint = tester.widget(findDialPaint); + final dynamic dialPainter = dialPaint.painter; + // ignore: avoid_dynamic_calls + final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; + // ignore: avoid_dynamic_calls + expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23); + // ignore: avoid_dynamic_calls + expect(primaryLabels.map<bool>((dynamic tp) => tp.inner as bool), inner0To23); + + // ignore: avoid_dynamic_calls + final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; + // ignore: avoid_dynamic_calls + expect(selectedLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23); + // ignore: avoid_dynamic_calls + expect(selectedLabels.map<bool>((dynamic tp) => tp.inner as bool), inner0To23); + }); + + testWidgets('Material3 - Dial background uses correct default color', (WidgetTester tester) async { + ThemeData theme = ThemeData(useMaterial3: true); + Widget buildTimePicker(ThemeData themeData) { + return MaterialApp( + theme: themeData, + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () { + showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ); + }, + ); + }, + ), + ), + ), + ); } + await tester.pumpWidget(buildTimePicker(theme)); + + // Open the time picker dialog. + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + // Test default dial background color. + RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); + expect( + dial, + paints + ..circle(color: theme.colorScheme.surfaceVariant) // Dial background color. + ..circle(color: Color(theme.colorScheme.primary.value)), // Dial hand color. + ); + + await tester.tap(find.text(okString)); // dismiss the dialog + await tester.pumpAndSettle(); + + // Test dial background color when theme color scheme is changed. + theme = theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + surfaceVariant: const Color(0xffff0000), + ), + ); + await tester.pumpWidget(buildTimePicker(theme)); + + // Open the time picker dialog. + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); + expect( + dial, + paints + ..circle(color: const Color(0xffff0000)) // Dial background color. + ..circle(color: Color(theme.colorScheme.primary.value)), // Dial hand color. + ); + }); + + for (final MaterialType materialType in MaterialType.values) { group('Dial (${materialType.name})', () { testWidgets('tap-select an hour', (WidgetTester tester) async { TimeOfDay? result; @@ -280,21 +537,32 @@ void main() { }); group('Dialog (${materialType.name})', () { - Material getMaterialFromDialog(WidgetTester tester) { - return tester.widget<Material>(find.descendant(of: find.byType(Dialog), matching: find.byType(Material)).first); - } + testWidgets('Material2 - Widgets have correct label capitalization', (WidgetTester tester) async { + await startPicker(tester, (TimeOfDay? time) {}, materialType: MaterialType.material2); + expect(find.text('SELECT TIME'), findsOneWidget); + expect(find.text('CANCEL'), findsOneWidget); + }); - testWidgets('Widgets have correct label capitalization', (WidgetTester tester) async { - await startPicker(tester, (TimeOfDay? time) {}, materialType: materialType); - expect(find.text(selectTimeString), findsOneWidget); - expect(find.text(cancelString), findsOneWidget); + testWidgets('Material3 - Widgets have correct label capitalization', (WidgetTester tester) async { + await startPicker(tester, (TimeOfDay? time) {}, materialType: MaterialType.material3); + expect(find.text('Select time'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); }); - testWidgets('Widgets have correct label capitalization in input mode', (WidgetTester tester) async { + testWidgets('Material2 - Widgets have correct label capitalization in input mode', (WidgetTester tester) async { await startPicker(tester, (TimeOfDay? time) {}, - entryMode: TimePickerEntryMode.input, materialType: materialType); - expect(find.text(enterTimeString), findsOneWidget); - expect(find.text(cancelString), findsOneWidget); + entryMode: TimePickerEntryMode.input, materialType: MaterialType.material2 + ); + expect(find.text('ENTER TIME'), findsOneWidget); + expect(find.text('CANCEL'), findsOneWidget); + }); + + testWidgets('Material3 - Widgets have correct label capitalization in input mode', (WidgetTester tester) async { + await startPicker(tester, (TimeOfDay? time) {}, + entryMode: TimePickerEntryMode.input, materialType: MaterialType.material3 + ); + expect(find.text('Enter time'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); }); testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async { @@ -314,211 +582,6 @@ void main() { expect(selectedLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11); }); - switch (materialType) { - case MaterialType.material2: - testWidgets('Dialog size - dial mode', (WidgetTester tester) async { - addTearDown(tester.view.reset); - - const Size timePickerPortraitSize = Size(310, 468); - const Size timePickerLandscapeSize = Size(524, 342); - const Size timePickerLandscapeSizeM2 = Size(508, 300); - const EdgeInsets padding = EdgeInsets.fromLTRB(8, 18, 8, 8); - double width; - double height; - - // portrait - tester.view.physicalSize = const Size(800, 800.5); - tester.view.devicePixelRatio = 1; - await mediaQueryBoilerplate(tester, materialType: materialType); - - width = timePickerPortraitSize.width + padding.horizontal; - height = timePickerPortraitSize.height + padding.vertical; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - - await tester.tap(find.text(okString)); // dismiss the dialog - await tester.pumpAndSettle(); - - // landscape - tester.view.physicalSize = const Size(800.5, 800); - tester.view.devicePixelRatio = 1; - await mediaQueryBoilerplate( - tester, - alwaysUse24HourFormat: true, - materialType: materialType, - ); - - width = timePickerLandscapeSize.width + padding.horizontal; - height = timePickerLandscapeSizeM2.height + padding.vertical; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - }); - - testWidgets('Dialog size - input mode', (WidgetTester tester) async { - const TimePickerEntryMode entryMode = TimePickerEntryMode.input; - const Size timePickerInputSize = Size(312, 216); - const Size dayPeriodPortraitSize = Size(52, 80); - const EdgeInsets padding = EdgeInsets.fromLTRB(8, 18, 8, 8); - final double height = timePickerInputSize.height + padding.vertical; - double width; - - await mediaQueryBoilerplate( - tester, - entryMode: entryMode, - materialType: materialType, - ); - - width = timePickerInputSize.width + padding.horizontal; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - - await tester.tap(find.text(okString)); // dismiss the dialog - await tester.pumpAndSettle(); - - await mediaQueryBoilerplate( - tester, - alwaysUse24HourFormat: true, - entryMode: entryMode, - materialType: materialType, - ); - width = timePickerInputSize.width - dayPeriodPortraitSize.width - 12 + padding.horizontal + 16; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - }); - - testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { - await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: materialType); - - final List<String> labels00To22 = List<String>.generate(12, (int index) { - return (index * 2).toString().padLeft(2, '0'); - }); - final CustomPaint dialPaint = tester.widget(findDialPaint); - final dynamic dialPainter = dialPaint.painter; - // ignore: avoid_dynamic_calls - final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; - // ignore: avoid_dynamic_calls - expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22); - - // ignore: avoid_dynamic_calls - final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; - // ignore: avoid_dynamic_calls - expect(selectedLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22); - }); - case MaterialType.material3: - testWidgets('Dialog size - dial mode', (WidgetTester tester) async { - addTearDown(tester.view.reset); - - const Size timePickerPortraitSize = Size(310, 468); - const Size timePickerLandscapeSize = Size(524, 342); - const EdgeInsets padding = EdgeInsets.all(24.0); - double width; - double height; - - // portrait - tester.view.physicalSize = const Size(800, 800.5); - tester.view.devicePixelRatio = 1; - await mediaQueryBoilerplate(tester, materialType: materialType); - - width = timePickerPortraitSize.width + padding.horizontal; - height = timePickerPortraitSize.height + padding.vertical; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - - await tester.tap(find.text(okString)); // dismiss the dialog - await tester.pumpAndSettle(); - - // landscape - tester.view.physicalSize = const Size(800.5, 800); - tester.view.devicePixelRatio = 1; - await mediaQueryBoilerplate( - tester, - alwaysUse24HourFormat: true, - materialType: materialType, - ); - - width = timePickerLandscapeSize.width + padding.horizontal; - height = timePickerLandscapeSize.height + padding.vertical; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - }); - - testWidgets('Dialog size - input mode', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); - const TimePickerEntryMode entryMode = TimePickerEntryMode.input; - const double textScaleFactor = 1.0; - const Size timePickerMinInputSize = Size(312, 216); - const Size dayPeriodPortraitSize = Size(52, 80); - const EdgeInsets padding = EdgeInsets.all(24.0); - final double height = timePickerMinInputSize.height * textScaleFactor + padding.vertical; - double width; - - await mediaQueryBoilerplate( - tester, - entryMode: entryMode, - materialType: materialType, - ); - - width = timePickerMinInputSize.width - (theme.useMaterial3 ? 32 : 0) + padding.horizontal; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - - await tester.tap(find.text(okString)); // dismiss the dialog - await tester.pumpAndSettle(); - - await mediaQueryBoilerplate( - tester, - alwaysUse24HourFormat: true, - entryMode: entryMode, - materialType: materialType, - ); - - width = timePickerMinInputSize.width - dayPeriodPortraitSize.width - 12 + padding.horizontal; - expect( - tester.getSize(find.byWidget(getMaterialFromDialog(tester))), - Size(width, height), - ); - }); - - testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { - await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: materialType); - - final List<String> labels00To23 = List<String>.generate(24, (int index) { - return index == 0 ? '00' : index.toString(); - }); - final List<bool> inner0To23 = List<bool>.generate(24, (int index) => index >= 12); - - final CustomPaint dialPaint = tester.widget(findDialPaint); - final dynamic dialPainter = dialPaint.painter; - // ignore: avoid_dynamic_calls - final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; - // ignore: avoid_dynamic_calls - expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23); - // ignore: avoid_dynamic_calls - expect(primaryLabels.map<bool>((dynamic tp) => tp.inner as bool), inner0To23); - - // ignore: avoid_dynamic_calls - final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; - // ignore: avoid_dynamic_calls - expect(selectedLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23); - // ignore: avoid_dynamic_calls - expect(selectedLabels.map<bool>((dynamic tp) => tp.inner as bool), inner0To23); - }); - } - testWidgets('when change orientation, should reflect in render objects', (WidgetTester tester) async { addTearDown(tester.view.reset); @@ -605,6 +668,167 @@ void main() { expect(tester.getBottomLeft(find.text(okString)).dx, 800 - ltrOkRight); }); + group('Barrier dismissible', () { + late PickerObserver rootObserver; + + setUp(() { + rootObserver = PickerObserver(); + }); + + testWidgets('Barrier is dismissible with default parameter', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.pickerCount, 1); + + // Tap on the barrier. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.pickerCount, 0); + }); + + testWidgets('Barrier is not dismissible with barrierDismissible is false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorObservers: <NavigatorObserver>[rootObserver], + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => + showTimePicker( + context: context, + barrierDismissible: false, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(rootObserver.pickerCount, 1); + + // Tap on the barrier, which shouldn't do anything this time. + await tester.tapAt(const Offset(10.0, 10.0)); + await tester.pumpAndSettle(); + expect(rootObserver.pickerCount, 1); + }); + }); + + testWidgets('Barrier color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.black54); + + // Dismiss the dialog. + await tester.tapAt(const Offset(10.0, 10.0)); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showTimePicker( + context: context, + barrierColor: Colors.pink, + initialTime: const TimeOfDay(hour: 7, minute: 0), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink); + }); + + testWidgets('Barrier Label', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () => showTimePicker( + context: context, + barrierLabel: 'Custom Label', + initialTime: const TimeOfDay(hour: 7, minute: 0), + ), + ); + }, + ), + ), + ), + ), + ); + + // Open the dialog. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).semanticsLabel, 'Custom Label'); + }); + testWidgets('uses root navigator by default', (WidgetTester tester) async { final PickerObserver rootObserver = PickerObserver(); final PickerObserver nestedObserver = PickerObserver(); @@ -708,10 +932,12 @@ void main() { expect(find.text(helperText), findsOneWidget); }); - testWidgets('OK Cancel button and helpText layout', (WidgetTester tester) async { + testWidgets('Material2 - OK Cancel button and helpText layout', (WidgetTester tester) async { + const String selectTimeString = 'SELECT TIME'; + const String cancelString = 'CANCEL'; Widget buildFrame(TextDirection textDirection) { return MaterialApp( - theme: ThemeData(useMaterial3: materialType == MaterialType.material3), + theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Builder( @@ -742,21 +968,13 @@ void main() { await tester.tap(find.text('X')); await tester.pumpAndSettle(); - switch (materialType) { - case MaterialType.material2: - expect(tester.getTopLeft(find.text(selectTimeString)), equals(const Offset(154, 155))); - expect(tester.getBottomRight(find.text(selectTimeString)), equals( - const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? const Offset(280.5, 165) : const Offset(281, 165), - )); - expect(tester.getBottomRight(find.text(okString)).dx, 644); - expect(tester.getBottomLeft(find.text(okString)).dx, 616); - expect(tester.getBottomRight(find.text(cancelString)).dx, 582); - case MaterialType.material3: - expect(tester.getTopLeft(find.text(selectTimeString)), equals(const Offset(138, 129))); - expect(tester.getBottomRight(find.text(selectTimeString)), equals(const Offset(295.0, 149.0))); - expect(tester.getBottomLeft(find.text(okString)).dx, 615.5); - expect(tester.getBottomRight(find.text(cancelString)).dx, 578); - } + expect(tester.getTopLeft(find.text(selectTimeString)), equals(const Offset(154, 155))); + expect(tester.getBottomRight(find.text(selectTimeString)), equals( + const Offset(280.5, 165), + )); + expect(tester.getBottomRight(find.text(okString)).dx, 644); + expect(tester.getBottomLeft(find.text(okString)).dx, 616); + expect(tester.getBottomRight(find.text(cancelString)).dx, 582); await tester.tap(find.text(okString)); await tester.pumpAndSettle(); @@ -765,23 +983,87 @@ void main() { await tester.tap(find.text('X')); await tester.pumpAndSettle(); - switch (materialType) { - case MaterialType.material2: - expect(tester.getTopLeft(find.text(selectTimeString)), equals( - const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK') ? const Offset(519.5, 155) : const Offset(519, 155), - )); - expect(tester.getBottomRight(find.text(selectTimeString)), equals(const Offset(646, 165))); - expect(tester.getBottomLeft(find.text(okString)).dx, 156); - expect(tester.getBottomRight(find.text(okString)).dx, 184); - expect(tester.getBottomLeft(find.text(cancelString)).dx, 218); - case MaterialType.material3: - expect(tester.getTopLeft(find.text(selectTimeString)), equals(const Offset(505.0, 129.0))); - expect(tester.getBottomRight(find.text(selectTimeString)), equals(const Offset(662, 149))); - expect(tester.getBottomLeft(find.text(okString)).dx, 155.5); - expect(tester.getBottomRight(find.text(okString)).dx, 184.5); - expect(tester.getBottomLeft(find.text(cancelString)).dx, 222); + expect(tester.getTopLeft(find.text(selectTimeString)), equals( + const Offset(519.5, 155), + )); + expect(tester.getBottomRight(find.text(selectTimeString)), equals(const Offset(646, 165))); + expect(tester.getBottomLeft(find.text(okString)).dx, 156); + expect(tester.getBottomRight(find.text(okString)).dx, 184); + expect(tester.getBottomLeft(find.text(cancelString)).dx, 218); + + await tester.tap(find.text(okString)); + await tester.pumpAndSettle(); + }); + + testWidgets('Material3 - OK Cancel button and helpText layout', (WidgetTester tester) async { + const String selectTimeString = 'Select time'; + const String cancelString = 'Cancel'; + Widget buildFrame(TextDirection textDirection) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Material( + child: Center( + child: Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('X'), + onPressed: () { + showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 7, minute: 0), + builder: (BuildContext context, Widget? child) { + return Directionality( + textDirection: textDirection, + child: child!, + ); + }, + ); + }, + ); + }, + ), + ), + ), + ); } + await tester.pumpWidget(buildFrame(TextDirection.ltr)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text(selectTimeString)), equals(const Offset(138, 129))); + expect( + tester.getBottomRight(find.text(selectTimeString)), + const Offset(294.75, 149.0), + ); + expect( + tester.getBottomLeft(find.text(okString)).dx, + moreOrLessEquals(615.9, epsilon: 0.001), + ); + expect(tester.getBottomRight(find.text(cancelString)).dx, 578); + + await tester.tap(find.text(okString)); + await tester.pumpAndSettle(); + + await tester.pumpWidget(buildFrame(TextDirection.rtl)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect( + tester.getTopLeft(find.text(selectTimeString)), + equals(const Offset(505.25, 129.0)), + ); + expect(tester.getBottomRight(find.text(selectTimeString)), equals(const Offset(662, 149))); + expect( + tester.getBottomLeft(find.text(okString)).dx, + moreOrLessEquals(155.9, epsilon: 0.001), + ); + expect( + tester.getBottomRight(find.text(okString)).dx, + moreOrLessEquals(184.1, epsilon: 0.001), + ); + expect(tester.getBottomLeft(find.text(cancelString)).dx, 222); + await tester.tap(find.text(okString)); await tester.pumpAndSettle(); }); @@ -996,9 +1278,9 @@ void main() { semantics.dispose(); }); - testWidgets('provides semantics information for header and footer', (WidgetTester tester) async { + testWidgets('Material2 - provides semantics information for header and footer', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: materialType); + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: MaterialType.material2); expect(semantics, isNot(includesNodeWith(label: ':'))); expect( @@ -1011,7 +1293,32 @@ void main() { hasLength(1), reason: '07 appears once in the header', ); - expect(semantics, includesNodeWith(label: cancelString)); + expect(semantics, includesNodeWith(label: 'CANCEL')); + expect(semantics, includesNodeWith(label: okString)); + + // In 24-hour mode we don't have AM/PM control. + expect(semantics, isNot(includesNodeWith(label: amString))); + expect(semantics, isNot(includesNodeWith(label: pmString))); + + semantics.dispose(); + }); + + testWidgets('Material3 - provides semantics information for header and footer', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, materialType: MaterialType.material3); + + expect(semantics, isNot(includesNodeWith(label: ':'))); + expect( + semantics.nodesWith(value: 'Select minutes 00'), + hasLength(1), + reason: '00 appears once in the header', + ); + expect( + semantics.nodesWith(value: 'Select hours 07'), + hasLength(1), + reason: '07 appears once in the header', + ); + expect(semantics, includesNodeWith(label: 'Cancel')); expect(semantics, includesNodeWith(label: okString)); // In 24-hour mode we don't have AM/PM control. @@ -1676,6 +1983,14 @@ class PickerObserver extends NavigatorObserver { } super.didPush(route, previousRoute); } + + @override + void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { + if (route is DialogRoute) { + pickerCount--; + } + super.didPop(route, previousRoute); + } } Future<void> mediaQueryBoilerplate( diff --git a/packages/flutter/test/material/time_picker_theme_test.dart b/packages/flutter/test/material/time_picker_theme_test.dart index dd2e5576908be..5b5394995b4fd 100644 --- a/packages/flutter/test/material/time_picker_theme_test.dart +++ b/packages/flutter/test/material/time_picker_theme_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('TimePickerThemeData copyWith, ==, hashCode basics', () { @@ -22,25 +21,31 @@ void main() { test('TimePickerThemeData null fields by default', () { const TimePickerThemeData timePickerTheme = TimePickerThemeData(); expect(timePickerTheme.backgroundColor, null); - expect(timePickerTheme.hourMinuteTextColor, null); - expect(timePickerTheme.hourMinuteColor, null); - expect(timePickerTheme.dayPeriodTextColor, null); + expect(timePickerTheme.cancelButtonStyle, null); + expect(timePickerTheme.confirmButtonStyle, null); + expect(timePickerTheme.dayPeriodBorderSide, null); expect(timePickerTheme.dayPeriodColor, null); - expect(timePickerTheme.dialHandColor, null); + expect(timePickerTheme.dayPeriodShape, null); + expect(timePickerTheme.dayPeriodTextColor, null); + expect(timePickerTheme.dayPeriodTextStyle, null); expect(timePickerTheme.dialBackgroundColor, null); + expect(timePickerTheme.dialHandColor, null); expect(timePickerTheme.dialTextColor, null); + expect(timePickerTheme.dialTextStyle, null); + expect(timePickerTheme.elevation, null); expect(timePickerTheme.entryModeIconColor, null); - expect(timePickerTheme.hourMinuteTextStyle, null); - expect(timePickerTheme.dayPeriodTextStyle, null); expect(timePickerTheme.helpTextStyle, null); - expect(timePickerTheme.shape, null); + expect(timePickerTheme.hourMinuteColor, null); expect(timePickerTheme.hourMinuteShape, null); - expect(timePickerTheme.dayPeriodShape, null); - expect(timePickerTheme.dayPeriodBorderSide, null); + expect(timePickerTheme.hourMinuteTextColor, null); + expect(timePickerTheme.hourMinuteTextStyle, null); expect(timePickerTheme.inputDecorationTheme, null); + expect(timePickerTheme.entryModeIconColor, null); + expect(timePickerTheme.padding, null); + expect(timePickerTheme.shape, null); }); - testWidgets('Default TimePickerThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default TimePickerThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TimePickerThemeData().debugFillProperties(builder); @@ -52,25 +57,39 @@ void main() { expect(description, <String>[]); }); - testWidgets('TimePickerThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TimePickerThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TimePickerThemeData( - backgroundColor: Color(0xFFFFFFFF), - hourMinuteTextColor: Color(0xFFFFFFFF), - hourMinuteColor: Color(0xFFFFFFFF), - dayPeriodTextColor: Color(0xFFFFFFFF), - dayPeriodColor: Color(0xFFFFFFFF), - dialHandColor: Color(0xFFFFFFFF), - dialBackgroundColor: Color(0xFFFFFFFF), - dialTextColor: Color(0xFFFFFFFF), - entryModeIconColor: Color(0xFFFFFFFF), - hourMinuteTextStyle: TextStyle(), - dayPeriodTextStyle: TextStyle(), - helpTextStyle: TextStyle(), - shape: RoundedRectangleBorder(), - hourMinuteShape: RoundedRectangleBorder(), - dayPeriodShape: RoundedRectangleBorder(), - dayPeriodBorderSide: BorderSide(), + backgroundColor: Color(0xfffffff0), + cancelButtonStyle: ButtonStyle(foregroundColor: MaterialStatePropertyAll<Color>(Color(0xfffffff1))), + confirmButtonStyle: ButtonStyle(foregroundColor: MaterialStatePropertyAll<Color>(Color(0xfffffff2))), + dayPeriodBorderSide: BorderSide(color: Color(0xfffffff3)), + dayPeriodColor: Color(0xfffffff4), + dayPeriodShape: RoundedRectangleBorder( + side: BorderSide(color: Color(0xfffffff5)), + ), + dayPeriodTextColor: Color(0xfffffff6), + dayPeriodTextStyle: TextStyle(color: Color(0xfffffff7)), + dialBackgroundColor: Color(0xfffffff8), + dialHandColor: Color(0xfffffff9), + dialTextColor: Color(0xfffffffa), + dialTextStyle: TextStyle(color: Color(0xfffffffb)), + elevation: 1.0, + entryModeIconColor: Color(0xfffffffc), + helpTextStyle: TextStyle(color: Color(0xfffffffd)), + hourMinuteColor: Color(0xfffffffe), + hourMinuteShape: RoundedRectangleBorder( + side: BorderSide(color: Color(0xffffffff)), + ), + hourMinuteTextColor: Color(0xfffffff0), + hourMinuteTextStyle: TextStyle(color: Color(0xfffffff1)), + inputDecorationTheme: InputDecorationTheme( + labelStyle: TextStyle(color: Color(0xfffffff2)), + ), + padding: EdgeInsets.all(1.0), + shape: RoundedRectangleBorder( + side: BorderSide(color: Color(0xfffffff3)), + ), ).debugFillProperties(builder); final List<String> description = builder.properties @@ -78,109 +97,90 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description, <String>[ - 'backgroundColor: Color(0xffffffff)', - 'dayPeriodBorderSide: BorderSide', - 'dayPeriodColor: Color(0xffffffff)', - 'dayPeriodShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', - 'dayPeriodTextColor: Color(0xffffffff)', - 'dayPeriodTextStyle: TextStyle(<all styles inherited>)', - 'dialBackgroundColor: Color(0xffffffff)', - 'dialHandColor: Color(0xffffffff)', - 'dialTextColor: Color(0xffffffff)', - 'entryModeIconColor: Color(0xffffffff)', - 'helpTextStyle: TextStyle(<all styles inherited>)', - 'hourMinuteColor: Color(0xffffffff)', - 'hourMinuteShape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', - 'hourMinuteTextColor: Color(0xffffffff)', - 'hourMinuteTextStyle: TextStyle(<all styles inherited>)', - 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)' - ]); + expect(description, equalsIgnoringHashCodes(<String>[ + 'backgroundColor: Color(0xfffffff0)', + 'cancelButtonStyle: ButtonStyle#00000(foregroundColor: MaterialStatePropertyAll(Color(0xfffffff1)))', + 'confirmButtonStyle: ButtonStyle#00000(foregroundColor: MaterialStatePropertyAll(Color(0xfffffff2)))', + 'dayPeriodBorderSide: BorderSide(color: Color(0xfffffff3))', + 'dayPeriodColor: Color(0xfffffff4)', + 'dayPeriodShape: RoundedRectangleBorder(BorderSide(color: Color(0xfffffff5)), BorderRadius.zero)', + 'dayPeriodTextColor: Color(0xfffffff6)', + 'dayPeriodTextStyle: TextStyle(inherit: true, color: Color(0xfffffff7))', + 'dialBackgroundColor: Color(0xfffffff8)', + 'dialHandColor: Color(0xfffffff9)', + 'dialTextColor: Color(0xfffffffa)', + 'dialTextStyle: TextStyle(inherit: true, color: Color(0xfffffffb))', + 'elevation: 1.0', + 'entryModeIconColor: Color(0xfffffffc)', + 'helpTextStyle: TextStyle(inherit: true, color: Color(0xfffffffd))', + 'hourMinuteColor: Color(0xfffffffe)', + 'hourMinuteShape: RoundedRectangleBorder(BorderSide(color: Color(0xffffffff)), BorderRadius.zero)', + 'hourMinuteTextColor: Color(0xfffffff0)', + 'hourMinuteTextStyle: TextStyle(inherit: true, color: Color(0xfffffff1))', + 'inputDecorationTheme: InputDecorationTheme#ff861(labelStyle: TextStyle(inherit: true, color: Color(0xfffffff2)))', + 'padding: EdgeInsets.all(1.0)', + 'shape: RoundedRectangleBorder(BorderSide(color: Color(0xfffffff3)), BorderRadius.zero)' + ])); }); - testWidgets('Passing no TimePickerThemeData uses defaults', (WidgetTester tester) async { - final ThemeData defaultTheme = ThemeData(); - final bool material3 = defaultTheme.useMaterial3; + testWidgetsWithLeakTracking('Material2 - Passing no TimePickerThemeData uses defaults', (WidgetTester tester) async { + final ThemeData defaultTheme = ThemeData(useMaterial3: false); await tester.pumpWidget(_TimePickerLauncher(themeData: defaultTheme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(const Duration(seconds: 1)); final Material dialogMaterial = _dialogMaterial(tester); expect(dialogMaterial.color, defaultTheme.colorScheme.surface); - expect(dialogMaterial.shape, material3 - ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))) - : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) + expect( + dialogMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), ); final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); expect( dial, - material3 - ? (paints - ..circle(color: defaultTheme.colorScheme.surfaceVariant.withOpacity(0.08)) // Dial background color. - ..circle(color: Color(defaultTheme.colorScheme.primary.value))) - : (paints - ..circle(color: defaultTheme.colorScheme.onSurface.withOpacity(0.08)) // Dial background color. - ..circle(color: Color(defaultTheme.colorScheme.primary.value))), // Dial hand color. + paints + ..circle(color: defaultTheme.colorScheme.onSurface.withOpacity(0.08)) // Dial background color. + ..circle(color: Color(defaultTheme.colorScheme.primary.value)) ); final RenderParagraph hourText = _textRenderParagraph(tester, '7'); expect( hourText.text.style, - material3 - ? (Typography.material2021().englishLike.displayLarge! - .merge(Typography.material2021().black.displayLarge) - .copyWith(color: defaultTheme.colorScheme.onPrimaryContainer, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.displayMedium! - .merge(Typography.material2014().black.displayMedium) - .copyWith(color: defaultTheme.colorScheme.primary)), + Typography.material2014().englishLike.displayMedium! + .merge(Typography.material2014().black.displayMedium) + .copyWith(color: defaultTheme.colorScheme.primary) ); final RenderParagraph minuteText = _textRenderParagraph(tester, '15'); expect( minuteText.text.style, - material3 - ? (Typography.material2021().englishLike.displayLarge! - .merge(Typography.material2021().black.displayLarge) - .copyWith(color: defaultTheme.colorScheme.onSurface, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.displayMedium! - .merge(Typography.material2014().black.displayMedium) - .copyWith(color: defaultTheme.colorScheme.onSurface)), + Typography.material2014().englishLike.displayMedium! + .merge(Typography.material2014().black.displayMedium) + .copyWith(color: defaultTheme.colorScheme.onSurface), ); final RenderParagraph amText = _textRenderParagraph(tester, 'AM'); expect( amText.text.style, - material3 - ? (Typography.material2021().englishLike.titleMedium! - .merge(Typography.material2021().black.titleMedium) - .copyWith(color: defaultTheme.colorScheme.onTertiaryContainer, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.titleMedium! - .merge(Typography.material2014().black.titleMedium) - .copyWith(color: defaultTheme.colorScheme.primary)), + Typography.material2014().englishLike.titleMedium! + .merge(Typography.material2014().black.titleMedium) + .copyWith(color: defaultTheme.colorScheme.primary), ); final RenderParagraph pmText = _textRenderParagraph(tester, 'PM'); expect( pmText.text.style, - material3 - ? (Typography.material2021().englishLike.titleMedium! - .merge(Typography.material2021().black.titleMedium) - .copyWith(color: defaultTheme.colorScheme.onTertiaryContainer, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.titleMedium! - .merge(Typography.material2014().black.titleMedium) - .copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.6))), + Typography.material2014().englishLike.titleMedium! + .merge(Typography.material2014().black.titleMedium) + .copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.6)), ); - final RenderParagraph helperText = _textRenderParagraph(tester, material3 ? 'Select time' : 'SELECT TIME'); + final RenderParagraph helperText = _textRenderParagraph(tester, 'SELECT TIME'); expect( helperText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .copyWith(color: defaultTheme.colorScheme.onSurface, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.labelSmall! - .merge(Typography.material2014().black.labelSmall)), + Typography.material2014().englishLike.labelSmall! + .merge(Typography.material2014().black.labelSmall), ); final CustomPaint dialPaint = tester.widget(findDialPaint); @@ -190,44 +190,36 @@ void main() { expect( // ignore: avoid_dynamic_calls primaryLabels.first.painter.text.style, - material3 - ? (Typography.material2021().englishLike.bodyLarge! - .merge(Typography.material2021().black.bodyLarge) - .copyWith(color: defaultTheme.colorScheme.onSurface, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.bodyLarge! - .merge(Typography.material2014().black.bodyLarge) - .copyWith(color: defaultTheme.colorScheme.onSurface)), + Typography.material2014().englishLike.bodyLarge! + .merge(Typography.material2014().black.bodyLarge) + .copyWith(color: defaultTheme.colorScheme.onSurface), ); // ignore: avoid_dynamic_calls final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; expect( // ignore: avoid_dynamic_calls selectedLabels.first.painter.text.style, - material3 - ? (Typography.material2021().englishLike.bodyLarge! - .merge(Typography.material2021().black.bodyLarge) - .copyWith(color: defaultTheme.colorScheme.onPrimary, decorationColor: defaultTheme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.bodyLarge! - .merge(Typography.material2014().white.bodyLarge) - .copyWith(color: defaultTheme.colorScheme.onPrimary)), + Typography.material2014().englishLike.bodyLarge! + .merge(Typography.material2014().white.bodyLarge) + .copyWith(color: defaultTheme.colorScheme.onPrimary), ); final Material hourMaterial = _textMaterial(tester, '7'); - expect(hourMaterial.color, material3 ? defaultTheme.colorScheme.primaryContainer : defaultTheme.colorScheme.primary.withOpacity(0.12)); - expect(hourMaterial.shape, material3 - ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))) - : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) + expect(hourMaterial.color, defaultTheme.colorScheme.primary.withOpacity(0.12)); + expect( + hourMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), ); final Material minuteMaterial = _textMaterial(tester, '15'); - expect(minuteMaterial.color, material3 ? defaultTheme.colorScheme.surfaceVariant : defaultTheme.colorScheme.onSurface.withOpacity(0.12)); - expect(minuteMaterial.shape, material3 - ? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))) - : const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))) + expect(minuteMaterial.color, defaultTheme.colorScheme.onSurface.withOpacity(0.12)); + expect( + minuteMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), ); final Material amMaterial = _textMaterial(tester, 'AM'); - expect(amMaterial.color, material3 ? defaultTheme.colorScheme.tertiaryContainer : defaultTheme.colorScheme.primary.withOpacity(0.12)); + expect(amMaterial.color, defaultTheme.colorScheme.primary.withOpacity(0.12)); final Material pmMaterial = _textMaterial(tester, 'PM'); expect(pmMaterial.color, Colors.transparent); @@ -239,61 +231,269 @@ void main() { final Material dayPeriodMaterial = _dayPeriodMaterial(tester); expect( dayPeriodMaterial.shape, - material3 - ? RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - side: BorderSide(color: defaultTheme.colorScheme.outline), - ) : RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - side: BorderSide(color: expectedBorderColor), - ), + RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + side: BorderSide(color: expectedBorderColor), + ), ); final Container dayPeriodDivider = _dayPeriodDivider(tester); expect( dayPeriodDivider.decoration, - material3 - ? BoxDecoration(border: Border(left: BorderSide(color: defaultTheme.colorScheme.outline))) - : BoxDecoration(border: Border(left: BorderSide(color: expectedBorderColor))), + BoxDecoration(border: Border(left: BorderSide(color: expectedBorderColor))), ); final IconButton entryModeIconButton = _entryModeIconButton(tester); expect( entryModeIconButton.color, - material3 ? null : defaultTheme.colorScheme.onSurface.withOpacity(0.6), + defaultTheme.colorScheme.onSurface.withOpacity(0.6), ); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'CANCEL'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + + final ButtonStyle confirmButtonStyle = _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); }); + testWidgetsWithLeakTracking('Material3 - Passing no TimePickerThemeData uses defaults', (WidgetTester tester) async { + final ThemeData defaultTheme = ThemeData(useMaterial3: true); + await tester.pumpWidget(_TimePickerLauncher(themeData: defaultTheme)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Material dialogMaterial = _dialogMaterial(tester); + expect(dialogMaterial.color, defaultTheme.colorScheme.surface); + expect( + dialogMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))), + ); + + final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); + expect( + dial, + paints + ..circle(color: defaultTheme.colorScheme.surfaceVariant) // Dial background color. + ..circle(color: Color(defaultTheme.colorScheme.primary.value)), // Dial hand color. + ); + + final RenderParagraph hourText = _textRenderParagraph(tester, '7'); + expect( + hourText.text.style, + Typography.material2021().englishLike.displayMedium! + .merge(Typography.material2021().black.displayMedium) + .copyWith( + color: defaultTheme.colorScheme.onPrimaryContainer, + decorationColor: defaultTheme.colorScheme.onSurface + ), + ); + + final RenderParagraph minuteText = _textRenderParagraph(tester, '15'); + expect( + minuteText.text.style, + Typography.material2021().englishLike.displayMedium! + .merge(Typography.material2021().black.displayMedium) + .copyWith( + color: defaultTheme.colorScheme.onSurface, + decorationColor: defaultTheme.colorScheme.onSurface + ), + ); + + final RenderParagraph amText = _textRenderParagraph(tester, 'AM'); + expect( + amText.text.style, + Typography.material2021().englishLike.titleMedium! + .merge(Typography.material2021().black.titleMedium) + .copyWith( + color: defaultTheme.colorScheme.onTertiaryContainer, + decorationColor: defaultTheme.colorScheme.onSurface, + ), + ); + + final RenderParagraph pmText = _textRenderParagraph(tester, 'PM'); + expect( + pmText.text.style, + Typography.material2021().englishLike.titleMedium! + .merge(Typography.material2021().black.titleMedium) + .copyWith( + color: defaultTheme.colorScheme.onSurfaceVariant, + decorationColor: defaultTheme.colorScheme.onSurface, + ) + ); + + final RenderParagraph helperText = _textRenderParagraph(tester, 'Select time'); + expect( + helperText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .copyWith( + color: defaultTheme.colorScheme.onSurface, + decorationColor: defaultTheme.colorScheme.onSurface, + ), + ); + + final CustomPaint dialPaint = tester.widget(findDialPaint); + final dynamic dialPainter = dialPaint.painter; + // ignore: avoid_dynamic_calls + final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; + expect( + // ignore: avoid_dynamic_calls + primaryLabels.first.painter.text.style, + Typography.material2021().englishLike.bodyLarge! + .merge(Typography.material2021().black.bodyLarge) + .copyWith( + color: defaultTheme.colorScheme.onSurface, + decorationColor: defaultTheme.colorScheme.onSurface, + ), + ); + // ignore: avoid_dynamic_calls + final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; + expect( + // ignore: avoid_dynamic_calls + selectedLabels.first.painter.text.style, + Typography.material2021().englishLike.bodyLarge! + .merge(Typography.material2021().black.bodyLarge) + .copyWith( + color: defaultTheme.colorScheme.onPrimary, + decorationColor: defaultTheme.colorScheme.onSurface, + ), + ); + + final Material hourMaterial = _textMaterial(tester, '7'); + expect(hourMaterial.color, defaultTheme.colorScheme.primaryContainer); + expect( + hourMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))) + ); + + final Material minuteMaterial = _textMaterial(tester, '15'); + expect(minuteMaterial.color, defaultTheme.colorScheme.surfaceVariant); + expect( + minuteMaterial.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))), + ); + + final Material amMaterial = _textMaterial(tester, 'AM'); + expect(amMaterial.color, defaultTheme.colorScheme.tertiaryContainer); + + final Material pmMaterial = _textMaterial(tester, 'PM'); + expect(pmMaterial.color, Colors.transparent); + + final Material dayPeriodMaterial = _dayPeriodMaterial(tester); + expect( + dayPeriodMaterial.shape, + RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + side: BorderSide(color: defaultTheme.colorScheme.outline), + ), + ); + + final Container dayPeriodDivider = _dayPeriodDivider(tester); + expect( + dayPeriodDivider.decoration, + BoxDecoration(border: Border(left: BorderSide(color: defaultTheme.colorScheme.outline))), + ); + + final IconButton entryModeIconButton = _entryModeIconButton(tester); + expect(entryModeIconButton.color, null); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'Cancel'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + + final ButtonStyle confirmButtonStyle = _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + }); + + testWidgets('Material2 - Passing no TimePickerThemeData uses defaults - input mode', (WidgetTester tester) async { + final ThemeData defaultTheme = ThemeData(useMaterial3: false); + await tester.pumpWidget(_TimePickerLauncher(themeData: defaultTheme, entryMode: TimePickerEntryMode.input)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final InputDecoration hourDecoration = _textField(tester, '7').decoration!; + expect(hourDecoration.filled, true); + expect( + hourDecoration.fillColor, + MaterialStateColor.resolveWith((Set<MaterialState> states) => + defaultTheme.colorScheme.onSurface.withOpacity(0.12)) + ); + expect( + hourDecoration.enabledBorder, + const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)) + ); + expect( + hourDecoration.errorBorder, + OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)) + ); + expect( + hourDecoration.focusedBorder, + OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2)) + ); + expect( + hourDecoration.focusedErrorBorder, + OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)) + ); + expect( + hourDecoration.hintStyle, + Typography.material2014().englishLike.displayMedium! + .merge(defaultTheme.textTheme.displayMedium!.copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.36))) + ); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'CANCEL'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + + final ButtonStyle confirmButtonStyle= _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + }); - testWidgets('Passing no TimePickerThemeData uses defaults - input mode', (WidgetTester tester) async { - final ThemeData defaultTheme = ThemeData(); - final bool material3 = defaultTheme.useMaterial3; + testWidgets('Material3 - Passing no TimePickerThemeData uses defaults - input mode', (WidgetTester tester) async { + final ThemeData defaultTheme = ThemeData(useMaterial3: true); await tester.pumpWidget(_TimePickerLauncher(themeData: defaultTheme, entryMode: TimePickerEntryMode.input)); await tester.tap(find.text('X')); await tester.pumpAndSettle(const Duration(seconds: 1)); final InputDecoration hourDecoration = _textField(tester, '7').decoration!; expect(hourDecoration.filled, true); - expect(hourDecoration.fillColor, material3 - ? defaultTheme.colorScheme.surfaceVariant - : MaterialStateColor.resolveWith((Set<MaterialState> states) => defaultTheme.colorScheme.onSurface.withOpacity(0.12))); - expect(hourDecoration.enabledBorder, material3 ? const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(8.0)), borderSide: BorderSide(color: Colors.transparent)) : const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent))); - expect(hourDecoration.errorBorder, material3 ? OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(8.0)), borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2.0)) : OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2))); - expect(hourDecoration.focusedBorder, material3 ? OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(8.0)), borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2.0)) : OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2))); - expect(hourDecoration.focusedErrorBorder, material3 ? OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(8.0)), borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2.0)) : OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2))); + expect(hourDecoration.fillColor, defaultTheme.colorScheme.surfaceVariant); + expect( + hourDecoration.enabledBorder, + const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide(color: Colors.transparent)), + ); + expect( + hourDecoration.errorBorder, + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2.0)), + ); + expect( + hourDecoration.focusedBorder, + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2.0)), + ); + expect( + hourDecoration.focusedErrorBorder, + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2.0)), + ); expect( hourDecoration.hintStyle, - material3 - ? TextStyle(color: defaultTheme.colorScheme.onSurface.withOpacity(0.36)) - : (Typography.material2014().englishLike.displayMedium! - .merge(defaultTheme.textTheme.displayMedium!.copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.36)))), + TextStyle(color: defaultTheme.colorScheme.onSurface.withOpacity(0.36)) ); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'Cancel'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); + + final ButtonStyle confirmButtonStyle = _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString())); }); - testWidgets('Time picker uses values from TimePickerThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Time picker uses values from TimePickerThemeData', (WidgetTester tester) async { final TimePickerThemeData timePickerTheme = _timePickerTheme(); - final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme); - final bool material3 = theme.useMaterial3; + final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme, useMaterial3: false); await tester.pumpWidget(_TimePickerLauncher(themeData: theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -313,69 +513,45 @@ void main() { final RenderParagraph hourText = _textRenderParagraph(tester, '7'); expect( hourText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _selectedColor, decorationColor: const Color(0xff1c1b1f))) - : (Typography.material2014().englishLike.bodyMedium! - .merge(Typography.material2014().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _selectedColor)), + Typography.material2014().englishLike.bodyMedium! + .merge(Typography.material2014().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _selectedColor), ); final RenderParagraph minuteText = _textRenderParagraph(tester, '15'); expect( minuteText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _unselectedColor, decorationColor: const Color(0xff1c1b1f))) - : (Typography.material2014().englishLike.bodyMedium! - .merge(Typography.material2014().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _unselectedColor)), + Typography.material2014().englishLike.bodyMedium! + .merge(Typography.material2014().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _unselectedColor), ); final RenderParagraph amText = _textRenderParagraph(tester, 'AM'); expect( amText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _selectedColor, decorationColor: const Color(0xff1c1b1f))) - : (Typography.material2014().englishLike.titleMedium! - .merge(Typography.material2014().black.titleMedium) - .merge(timePickerTheme.dayPeriodTextStyle) - .copyWith(color: _selectedColor)), + Typography.material2014().englishLike.titleMedium! + .merge(Typography.material2014().black.titleMedium) + .merge(timePickerTheme.dayPeriodTextStyle) + .copyWith(color: _selectedColor), ); final RenderParagraph pmText = _textRenderParagraph(tester, 'PM'); expect( pmText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .merge(timePickerTheme.hourMinuteTextStyle) - .copyWith(color: _unselectedColor, decorationColor: const Color(0xff1c1b1f))) - : (Typography.material2014().englishLike.titleMedium! - .merge(Typography.material2014().black.titleMedium) - .merge(timePickerTheme.dayPeriodTextStyle) - .copyWith(color: _unselectedColor)), + Typography.material2014().englishLike.titleMedium! + .merge(Typography.material2014().black.titleMedium) + .merge(timePickerTheme.dayPeriodTextStyle) + .copyWith(color: _unselectedColor), ); - final RenderParagraph helperText = _textRenderParagraph(tester, material3 ? 'Select time' : 'SELECT TIME'); + final RenderParagraph helperText = _textRenderParagraph(tester, 'SELECT TIME'); expect( helperText.text.style, - material3 - ? (Typography.material2021().englishLike.bodyMedium! - .merge(Typography.material2021().black.bodyMedium) - .merge(timePickerTheme.helpTextStyle).copyWith(color: theme.colorScheme.onSurface, decorationColor: theme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.bodyMedium! - .merge(Typography.material2014().black.bodyMedium) - .merge(timePickerTheme.helpTextStyle)), + Typography.material2014().englishLike.bodyMedium! + .merge(Typography.material2014().black.bodyMedium) + .merge(timePickerTheme.helpTextStyle), ); final CustomPaint dialPaint = tester.widget(findDialPaint); @@ -385,26 +561,18 @@ void main() { expect( // ignore: avoid_dynamic_calls primaryLabels.first.painter.text.style, - material3 - ? (Typography.material2021().englishLike.bodyLarge! - .merge(Typography.material2021().black.bodyLarge) - .copyWith(color: _unselectedColor, decorationColor: theme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.bodyLarge! - .merge(Typography.material2014().black.bodyLarge) - .copyWith(color: _unselectedColor)), + Typography.material2014().englishLike.bodyLarge! + .merge(Typography.material2014().black.bodyLarge) + .copyWith(color: _unselectedColor), ); // ignore: avoid_dynamic_calls final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; expect( // ignore: avoid_dynamic_calls selectedLabels.first.painter.text.style, - material3 - ? (Typography.material2021().englishLike.bodyLarge! - .merge(Typography.material2021().black.bodyLarge) - .copyWith(color: _selectedColor, decorationColor: theme.colorScheme.onSurface)) - : (Typography.material2014().englishLike.bodyLarge! - .merge(Typography.material2014().white.bodyLarge) - .copyWith(color: _selectedColor)), + Typography.material2014().englishLike.bodyLarge! + .merge(Typography.material2014().white.bodyLarge) + .copyWith(color: _selectedColor), ); final Material hourMaterial = _textMaterial(tester, '7'); @@ -436,8 +604,137 @@ void main() { final IconButton entryModeIconButton = _entryModeIconButton(tester); expect( entryModeIconButton.color, - material3 ? null : timePickerTheme.entryModeIconColor, + timePickerTheme.entryModeIconColor, ); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'CANCEL'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(timePickerTheme.cancelButtonStyle.toString())); + + final ButtonStyle confirmButtonStyle = _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(timePickerTheme.confirmButtonStyle.toString())); + }); + + testWidgetsWithLeakTracking('Material3 - Time picker uses values from TimePickerThemeData', (WidgetTester tester) async { + final TimePickerThemeData timePickerTheme = _timePickerTheme(); + final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme, useMaterial3: true); + await tester.pumpWidget(_TimePickerLauncher(themeData: theme)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Material dialogMaterial = _dialogMaterial(tester); + expect(dialogMaterial.color, timePickerTheme.backgroundColor); + expect(dialogMaterial.shape, timePickerTheme.shape); + + final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint)); + expect( + dial, + paints + ..circle(color: Color(timePickerTheme.dialBackgroundColor!.value)) // Dial background color. + ..circle(color: Color(timePickerTheme.dialHandColor!.value)), // Dial hand color. + ); + + final RenderParagraph hourText = _textRenderParagraph(tester, '7'); + expect( + hourText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _selectedColor, decorationColor: const Color(0xff1c1b1f)), + ); + + final RenderParagraph minuteText = _textRenderParagraph(tester, '15'); + expect( + minuteText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _unselectedColor, decorationColor: const Color(0xff1c1b1f)), + ); + + final RenderParagraph amText = _textRenderParagraph(tester, 'AM'); + expect( + amText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _selectedColor, decorationColor: const Color(0xff1c1b1f)), + ); + + final RenderParagraph pmText = _textRenderParagraph(tester, 'PM'); + expect( + pmText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .merge(timePickerTheme.hourMinuteTextStyle) + .copyWith(color: _unselectedColor, decorationColor: const Color(0xff1c1b1f)), + ); + + final RenderParagraph helperText = _textRenderParagraph(tester, 'Select time'); + expect( + helperText.text.style, + Typography.material2021().englishLike.bodyMedium! + .merge(Typography.material2021().black.bodyMedium) + .merge(timePickerTheme.helpTextStyle).copyWith( + color: theme.colorScheme.onSurface, + decorationColor: theme.colorScheme.onSurface + ), + ); + + final CustomPaint dialPaint = tester.widget(findDialPaint); + final dynamic dialPainter = dialPaint.painter; + // ignore: avoid_dynamic_calls + final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>; + expect( + // ignore: avoid_dynamic_calls + primaryLabels.first.painter.text.style, + Typography.material2021().englishLike.bodyLarge! + .merge(Typography.material2021().black.bodyLarge) + .copyWith(color: _unselectedColor, decorationColor: theme.colorScheme.onSurface), + ); + // ignore: avoid_dynamic_calls + final List<dynamic> selectedLabels = dialPainter.selectedLabels as List<dynamic>; + expect( + // ignore: avoid_dynamic_calls + selectedLabels.first.painter.text.style, + Typography.material2021().englishLike.bodyLarge! + .merge(Typography.material2021().black.bodyLarge) + .copyWith(color: _selectedColor, decorationColor: theme.colorScheme.onSurface), + ); + + final Material hourMaterial = _textMaterial(tester, '7'); + expect(hourMaterial.color, _selectedColor); + expect(hourMaterial.shape, timePickerTheme.hourMinuteShape); + + final Material minuteMaterial = _textMaterial(tester, '15'); + expect(minuteMaterial.color, _unselectedColor); + expect(minuteMaterial.shape, timePickerTheme.hourMinuteShape); + + final Material amMaterial = _textMaterial(tester, 'AM'); + expect(amMaterial.color, _selectedColor); + + final Material pmMaterial = _textMaterial(tester, 'PM'); + expect(pmMaterial.color, _unselectedColor); + + final Material dayPeriodMaterial = _dayPeriodMaterial(tester); + expect( + dayPeriodMaterial.shape, + timePickerTheme.dayPeriodShape!.copyWith(side: timePickerTheme.dayPeriodBorderSide), + ); + + final Container dayPeriodDivider = _dayPeriodDivider(tester); + expect( + dayPeriodDivider.decoration, + BoxDecoration(border: Border(left: timePickerTheme.dayPeriodBorderSide!)), + ); + + final IconButton entryModeIconButton = _entryModeIconButton(tester); + expect(entryModeIconButton.color, null); + + final ButtonStyle cancelButtonStyle = _actionButtonStyle(tester, 'Cancel'); + expect(cancelButtonStyle.toString(), equalsIgnoringHashCodes(timePickerTheme.cancelButtonStyle.toString())); + + final ButtonStyle confirmButtonStyle = _actionButtonStyle(tester, 'OK'); + expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(timePickerTheme.confirmButtonStyle.toString())); }); testWidgets('Time picker uses values from TimePickerThemeData with InputDecorationTheme - input mode', (WidgetTester tester) async { @@ -479,6 +776,8 @@ TimePickerThemeData _timePickerTheme({bool includeInputDecoration = false}) { final MaterialStateColor materialStateColor = MaterialStateColor.resolveWith(getColor); return TimePickerThemeData( backgroundColor: Colors.orange, + cancelButtonStyle: TextButton.styleFrom(primary: Colors.red), + confirmButtonStyle: TextButton.styleFrom(primary: Colors.green), hourMinuteTextColor: materialStateColor, hourMinuteColor: materialStateColor, dayPeriodTextColor: materialStateColor, @@ -573,3 +872,7 @@ final Finder findDialPaint = find.descendant( of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'), matching: find.byWidgetPredicate((Widget w) => w is CustomPaint), ); + +ButtonStyle _actionButtonStyle(WidgetTester tester, String text) { + return tester.widget<TextButton>(find.widgetWithText(TextButton, text)).style!; +} diff --git a/packages/flutter/test/material/time_test.dart b/packages/flutter/test/material/time_test.dart index e577a9b1a03d9..97bb94617a145 100644 --- a/packages/flutter/test/material/time_test.dart +++ b/packages/flutter/test/material/time_test.dart @@ -4,10 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('TimeOfDay.format', () { - testWidgets('respects alwaysUse24HourFormat option', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects alwaysUse24HourFormat option', (WidgetTester tester) async { Future<String> pumpTest(bool alwaysUse24HourFormat) async { late String formattedValue; await tester.pumpWidget(MaterialApp( @@ -27,7 +28,7 @@ void main() { }); }); - testWidgets('hourOfPeriod returns correct value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hourOfPeriod returns correct value', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/59158. expect(const TimeOfDay(minute: 0, hour: 0).hourOfPeriod, 12); expect(const TimeOfDay(minute: 0, hour: 1).hourOfPeriod, 1); @@ -56,11 +57,13 @@ void main() { }); group('RestorableTimeOfDay tests', () { - testWidgets('value is not accessible when not registered', (WidgetTester tester) async { - expect(() => RestorableTimeOfDay(const TimeOfDay(hour: 20, minute: 4)).value, throwsAssertionError); + testWidgetsWithLeakTracking('value is not accessible when not registered', (WidgetTester tester) async { + final RestorableTimeOfDay property = RestorableTimeOfDay(const TimeOfDay(hour: 20, minute: 4)); + addTearDown(property.dispose); + expect(() => property.value, throwsAssertionError); }); - testWidgets('work when not in restoration scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('work when not in restoration scope', (WidgetTester tester) async { await tester.pumpWidget(const _RestorableWidget()); final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget)); @@ -77,7 +80,7 @@ void main() { expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2)); }); - testWidgets('restart and restore', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restart and restore', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -105,7 +108,7 @@ void main() { expect(state.timeOfDay.value, const TimeOfDay(hour: 2, minute: 2)); }); - testWidgets('restore to older state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restore to older state', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -136,7 +139,7 @@ void main() { expect(state.timeOfDay.value, const TimeOfDay(hour: 10, minute: 5)); }); - testWidgets('call notifiers when value changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('call notifiers when value changes', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -193,4 +196,10 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi @override String get restorationId => 'widget'; + + @override + void dispose() { + timeOfDay.dispose(); + super.dispose(); + } } diff --git a/packages/flutter/test/material/toggle_buttons_test.dart b/packages/flutter/test/material/toggle_buttons_test.dart index ebf81c391f15c..0a31859bd8f29 100644 --- a/packages/flutter/test/material/toggle_buttons_test.dart +++ b/packages/flutter/test/material/toggle_buttons_test.dart @@ -11,8 +11,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; const double _defaultBorderWidth = 1.0; @@ -37,7 +36,7 @@ Widget boilerplate({ } void main() { - testWidgets('Initial toggle state is reflected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial toggle state is reflected', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { return tester.widget<DefaultTextStyle>(find.descendant( of: find.widgetWithText(TextButton, text), @@ -68,7 +67,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'onPressed is triggered on button tap', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { @@ -127,7 +126,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'onPressed that is null disables buttons', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { @@ -179,7 +178,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'children and isSelected properties have to be the same length', (WidgetTester tester) async { await expectLater( @@ -206,7 +205,7 @@ void main() { }, ); - testWidgets('Default text style is applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default text style is applied', (WidgetTester tester) async { final ThemeData theme = ThemeData(); await tester.pumpWidget( boilerplate( @@ -237,7 +236,7 @@ void main() { expect(textStyle.decoration, theme.textTheme.bodyMedium!.decoration); }); - testWidgets('Custom text style except color is applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom text style except color is applied', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: ToggleButtons( @@ -274,7 +273,7 @@ void main() { expect(textStyle.color, isNot(Colors.orange)); }); - testWidgets('Default BoxConstraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default BoxConstraints', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: ToggleButtons( @@ -300,7 +299,7 @@ void main() { expect(thirdRect.height, 48.0); }); - testWidgets('Custom BoxConstraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom BoxConstraints', (WidgetTester tester) async { // Test for minimum constraints await tester.pumpWidget( boilerplate( @@ -361,7 +360,7 @@ void main() { expect(thirdRect.height, 10.0); }); - testWidgets( + testWidgetsWithLeakTracking( 'Default text/icon colors for enabled, selected and disabled states', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { @@ -453,7 +452,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Custom text/icon colors for enabled, selected and disabled states', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { @@ -539,7 +538,7 @@ void main() { }, ); - testWidgets('Default button fillColor - unselected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default button fillColor - unselected', (WidgetTester tester) async { final ThemeData theme = ThemeData(); await tester.pumpWidget( boilerplate( @@ -566,7 +565,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Default button fillColor - selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default button fillColor - selected', (WidgetTester tester) async { final ThemeData theme = ThemeData(); await tester.pumpWidget( boilerplate( @@ -593,7 +592,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Default button fillColor - disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default button fillColor - disabled', (WidgetTester tester) async { final ThemeData theme = ThemeData(); await tester.pumpWidget( boilerplate( @@ -619,7 +618,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Custom button fillColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom button fillColor', (WidgetTester tester) async { const Color customFillColor = Colors.green; await tester.pumpWidget( boilerplate( @@ -644,7 +643,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Custom button fillColor - Non MaterialState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom button fillColor - Non MaterialState', (WidgetTester tester) async { Material buttonColor(String text) { return tester.widget<Material>( find.descendant( @@ -695,7 +694,7 @@ void main() { expect(buttonColor('Second child').color, theme.colorScheme.surface.withOpacity(0.0)); }); - testWidgets('Custom button fillColor - MaterialState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom button fillColor - MaterialState', (WidgetTester tester) async { Material buttonColor(String text) { return tester.widget<Material>( find.descendant( @@ -754,7 +753,7 @@ void main() { expect(buttonColor('Second child').color, defaultFillColor); }); - testWidgets('Default InkWell colors - unselected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default InkWell colors - unselected', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -817,9 +816,11 @@ void main() { expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.12))); await hoverGesture.removePointer(); + + focusNode.dispose(); }); - testWidgets('Default InkWell colors - selected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default InkWell colors - selected', (WidgetTester tester) async { final ThemeData theme = ThemeData(); final FocusNode focusNode = FocusNode(); await tester.pumpWidget( @@ -882,9 +883,11 @@ void main() { expect(inkFeatures, paints..rect(color: theme.colorScheme.primary.withOpacity(0.12))); await hoverGesture.removePointer(); + + focusNode.dispose(); }); - testWidgets('Custom InkWell colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom InkWell colors', (WidgetTester tester) async { const Color splashColor = Color(0xff4caf50); const Color highlightColor = Color(0xffcddc39); const Color hoverColor = Color(0xffffeb3b); @@ -951,9 +954,11 @@ void main() { expect(inkFeatures, paints..rect(color: focusColor)); await hoverGesture.removePointer(); + + focusNode.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'Default border width and border colors for enabled, selected and disabled states', (WidgetTester tester) async { final ThemeData theme = ThemeData(); @@ -1041,7 +1046,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Custom border width and border colors for enabled, selected and disabled states', (WidgetTester tester) async { const Color borderColor = Color(0xff4caf50); @@ -1138,7 +1143,7 @@ void main() { }, ); - testWidgets('Height of segmented control is determined by tallest widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Height of segmented control is determined by tallest widget', (WidgetTester tester) async { final List<Widget> children = <Widget>[ Container( constraints: const BoxConstraints.tightFor(height: 100.0), @@ -1170,7 +1175,7 @@ void main() { } }); - testWidgets('Sizes of toggle buttons rebuilds with the correct dimensions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sizes of toggle buttons rebuilds with the correct dimensions', (WidgetTester tester) async { final List<Widget> children = <Widget>[ Container( constraints: const BoxConstraints.tightFor( @@ -1269,7 +1274,7 @@ void main() { } }); - testWidgets('ToggleButtons text baseline alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtons text baseline alignment', (WidgetTester tester) async { // The point size of the fonts must be a multiple of 4 until // https://github.com/flutter/flutter/issues/122066 is resolved. await tester.pumpWidget( @@ -1319,7 +1324,7 @@ void main() { expect(firstToggleButtonDy, textDy - 5.0); }); - testWidgets('Directionality test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality test', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Directionality( @@ -1367,7 +1372,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Properly draws borders based on state', (WidgetTester tester) async { final ThemeData theme = ThemeData(); @@ -1447,7 +1452,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Properly draws borders based on state when direction is vertical and verticalDirection is down.', (WidgetTester tester) async { final ThemeData theme = ThemeData(); @@ -1534,7 +1539,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'VerticalDirection test when direction is vertical.', (WidgetTester tester) async { await tester.pumpWidget( @@ -1560,7 +1565,7 @@ void main() { }, ); - testWidgets('Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( useMaterial3: false, @@ -1588,7 +1593,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0)); }); - testWidgets('Tap target size is configurable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap target size is configurable', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( useMaterial3: false, @@ -1616,7 +1621,7 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0)); }); - testWidgets('Tap target size is configurable for vertical axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap target size is configurable for vertical axis', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( child: ToggleButtons( @@ -1645,7 +1650,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/73725 - testWidgets('Border radius paint test when there is only one button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Border radius paint test when there is only one button', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( boilerplate( @@ -1690,7 +1695,7 @@ void main() { ); }); - testWidgets('Border radius paint test when Radius.x or Radius.y equal 0.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Border radius paint test when Radius.x or Radius.y equal 0.0', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( useMaterial3: false, @@ -1718,7 +1723,7 @@ void main() { ); }); - testWidgets('ToggleButtons implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtons implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); ToggleButtons( @@ -1756,7 +1761,7 @@ void main() { ]); }); - testWidgets('ToggleButtons changes mouse cursor when the button is hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtons changes mouse cursor when the button is hovered', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: MouseRegion( @@ -1819,7 +1824,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('ToggleButtons focus, hover, and highlight elevations are 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtons focus, hover, and highlight elevations are 0', (WidgetTester tester) async { final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; await tester.pumpWidget( boilerplate( @@ -1863,9 +1868,13 @@ void main() { expect(toggleButtonElevation('two'), 0); await hoverGesture.removePointer(); + + for (final FocusNode n in focusNodes) { + n.dispose(); + } }); - testWidgets('Toggle buttons height matches MaterialTapTargetSize.padded height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle buttons height matches MaterialTapTargetSize.padded height', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: ToggleButtons( @@ -1888,7 +1897,7 @@ void main() { expect(thirdRect.height, 48.0); }); - testWidgets('Toggle buttons constraints size does not affect minimum input padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle buttons constraints size does not affect minimum input padding', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/97302 final SemanticsTester semantics = SemanticsTester(tester); @@ -1975,7 +1984,7 @@ void main() { semantics.dispose(); }); - testWidgets('Toggle buttons have correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Toggle buttons have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/material/toggle_buttons_theme_test.dart b/packages/flutter/test/material/toggle_buttons_theme_test.dart index 5dfb0701911f1..a041f1d7679ec 100644 --- a/packages/flutter/test/material/toggle_buttons_theme_test.dart +++ b/packages/flutter/test/material/toggle_buttons_theme_test.dart @@ -6,8 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Widget boilerplate({required Widget child}) { return Directionality( @@ -64,7 +63,7 @@ void main() { expect(theme.data.borderWidth, null); }); - testWidgets('Default ToggleButtonsThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default ToggleButtonsThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ToggleButtonsThemeData().debugFillProperties(builder); @@ -76,7 +75,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('ToggleButtonsThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ToggleButtonsThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ToggleButtonsThemeData( textStyle: TextStyle(fontSize: 10), @@ -121,7 +120,7 @@ void main() { ]); }); - testWidgets('Theme text style, except color, is applied', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme text style, except color, is applied', (WidgetTester tester) async { await tester.pumpWidget( Material( child: boilerplate( @@ -164,7 +163,7 @@ void main() { expect(textStyle.color, isNot(Colors.orange)); }); - testWidgets('Custom BoxConstraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom BoxConstraints', (WidgetTester tester) async { // Test for minimum constraints await tester.pumpWidget( Material( @@ -238,7 +237,7 @@ void main() { expect(thirdRect.height, 10.0); }); - testWidgets( + testWidgetsWithLeakTracking( 'Theme text/icon colors for enabled, selected and disabled states', (WidgetTester tester) async { TextStyle buttonTextStyle(String text) { @@ -342,7 +341,7 @@ void main() { }, ); - testWidgets('Theme button fillColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme button fillColor', (WidgetTester tester) async { const Color customFillColor = Colors.green; await tester.pumpWidget( Material( @@ -371,7 +370,7 @@ void main() { expect(material.type, MaterialType.button); }); - testWidgets('Custom Theme button fillColor in different states', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom Theme button fillColor in different states', (WidgetTester tester) async { Material buttonColor(String text) { return tester.widget<Material>( find.descendant( @@ -438,7 +437,7 @@ void main() { expect(buttonColor('Second child').color, disabledFillColor); }); - testWidgets('Theme InkWell colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme InkWell colors', (WidgetTester tester) async { const Color splashColor = Color(0xff4caf50); const Color highlightColor = Color(0xffcddc39); const Color hoverColor = Color(0xffffeb3b); @@ -512,9 +511,11 @@ void main() { expect(inkFeatures, paints..rect(color: focusColor)); await hoverGesture.removePointer(); + + focusNode.dispose(); }); - testWidgets( + testWidgetsWithLeakTracking( 'Theme border width and border colors for enabled, selected and disabled states', (WidgetTester tester) async { const Color borderColor = Color(0xff4caf50); diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 84d637190d762..6b568c56b490a 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -10,9 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../foundation/leak_tracking.dart'; -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -28,12 +26,15 @@ Finder _findTooltipContainer(String tooltipText) { void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - center', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -82,6 +83,9 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - center with padding outside overlay', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -89,7 +93,7 @@ void main() { padding: const EdgeInsets.all(20), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -141,6 +145,9 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - top left', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -148,7 +155,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -195,12 +202,15 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - center prefer above fits', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -248,12 +258,15 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - center prefer above does not fit', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -312,12 +325,15 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -364,6 +380,9 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -371,7 +390,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -421,6 +440,9 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -428,7 +450,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -534,12 +556,15 @@ void main() { testWidgetsWithLeakTracking('Custom tooltip margin', (WidgetTester tester) async { const double customMarginValue = 10.0; final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: tooltipKey, @@ -770,6 +795,9 @@ void main() { testWidgetsWithLeakTracking('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -777,7 +805,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: tooltipKey, @@ -836,7 +864,7 @@ void main() { expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows})); - testWidgetsWithLeakTracking('Can tooltip decoration be customized', (WidgetTester tester) async { + testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); const Decoration customDecoration = ShapeDecoration( shape: StadiumBorder(), @@ -924,7 +952,7 @@ void main() { await gesture.up(); }); - testWidgets('Tooltip dismiss countdown begins on long press release', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip dismiss countdown begins on long press release', (WidgetTester tester) async { // Specs: https://github.com/flutter/flutter/issues/4182 const Duration showDuration = Duration(seconds: 1); const Duration eternity = Duration(days: 9999); @@ -1394,7 +1422,7 @@ void main() { await tester.pump(waitDuration); }); - testWidgetsWithLeakTracking('Does tooltip contribute semantics', (WidgetTester tester) async { + testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); @@ -1868,7 +1896,7 @@ void main() { expect(onTriggeredCalled, false); }); - testWidgets('dismissAllToolTips dismisses hovered tooltips', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dismissAllToolTips dismisses hovered tooltips', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -1904,7 +1932,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Hovered tooltips do not dismiss after showDuration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hovered tooltips do not dismiss after showDuration', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -2279,7 +2307,7 @@ void main() { expect(element.dirty, isFalse); }); - testWidgets('Tooltip does not initialize animation controller in dispose process', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not initialize animation controller in dispose process', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( @@ -2298,7 +2326,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Tooltip does not crash when showing the tooltip but the OverlayPortal is unmounted, during dispose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not crash when showing the tooltip but the OverlayPortal is unmounted, during dispose', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: SelectionArea( diff --git a/packages/flutter/test/material/tooltip_theme_test.dart b/packages/flutter/test/material/tooltip_theme_test.dart index b2961b6e56746..4548f331211c2 100644 --- a/packages/flutter/test/material/tooltip_theme_test.dart +++ b/packages/flutter/test/material/tooltip_theme_test.dart @@ -7,8 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; const String tooltipText = 'TIP'; @@ -59,7 +58,7 @@ void main() { expect(theme.enableFeedback, null); }); - testWidgets('Default TooltipThemeData debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default TooltipThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const TooltipThemeData().debugFillProperties(builder); @@ -71,7 +70,7 @@ void main() { expect(description, <String>[]); }); - testWidgets('TooltipThemeData implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TooltipThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Duration wait = Duration(milliseconds: 100); const Duration show = Duration(milliseconds: 200); @@ -113,8 +112,10 @@ void main() { ]); }); - testWidgets('Tooltip verticalOffset, preferBelow; center prefer above fits - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center prefer above fits - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -129,7 +130,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -170,8 +171,12 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(200.0)); }); - testWidgets('Tooltip verticalOffset, preferBelow; center prefer above fits - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center prefer above fits - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -184,7 +189,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -225,8 +230,11 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(200.0)); }); - testWidgets('Tooltip verticalOffset, preferBelow; center prefer above does not fit - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center prefer above does not fit - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -241,7 +249,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -293,8 +301,12 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(589.0)); }); - testWidgets('Tooltip verticalOffset, preferBelow; center prefer above does not fit - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center prefer above does not fit - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -307,7 +319,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -359,8 +371,10 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(589.0)); }); - testWidgets('Tooltip verticalOffset, preferBelow; center preferBelow fits - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center preferBelow fits - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -375,7 +389,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -415,8 +429,12 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(590.0)); }); - testWidgets('Tooltip verticalOffset, preferBelow; center prefer below fits - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip verticalOffset, preferBelow; center prefer below fits - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -429,7 +447,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Stack( children: <Widget>[ @@ -469,14 +487,18 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(590.0)); }); - testWidgets('Tooltip margin - ThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip margin - ThemeData', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Theme( data: ThemeData( @@ -524,14 +546,17 @@ void main() { expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - _customPaddingValue); }); - testWidgets('Tooltip margin - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip margin - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return TooltipTheme( data: const TooltipThemeData( @@ -577,7 +602,7 @@ void main() { expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - _customPaddingValue); }); - testWidgets('Tooltip message textStyle - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip message textStyle - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -607,7 +632,7 @@ void main() { expect(textStyle.decoration, TextDecoration.underline); }); - testWidgets('Tooltip message textStyle - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip message textStyle - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(MaterialApp( home: TooltipTheme( @@ -636,7 +661,7 @@ void main() { expect(textStyle.decoration, TextDecoration.underline); }); - testWidgets('Tooltip message textAlign - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip message textAlign - TooltipTheme', (WidgetTester tester) async { Future<void> pumpTooltipWithTextAlign({TextAlign? textAlign}) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); await tester.pumpWidget( @@ -675,12 +700,14 @@ void main() { expect(textAlign, TextAlign.end); }); - testWidgets('Tooltip decoration - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip decoration - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); const Decoration customDecoration = ShapeDecoration( shape: StadiumBorder(), color: Color(0x80800000), ); + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -693,7 +720,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: key, @@ -717,12 +744,16 @@ void main() { expect(tip, paints..rrect(color: const Color(0x80800000))); }); - testWidgets('Tooltip decoration - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip decoration - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); const Decoration customDecoration = ShapeDecoration( shape: StadiumBorder(), color: Color(0x80800000), ); + + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -732,7 +763,7 @@ void main() { data: const TooltipThemeData(decoration: customDecoration), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: key, @@ -757,11 +788,14 @@ void main() { expect(tip, paints..rrect(color: const Color(0x80800000))); }); - testWidgets('Tooltip height and padding - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip height and padding - ThemeData.tooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); const double customTooltipHeight = 100.0; const double customPaddingVal = 20.0; + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -774,7 +808,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: key, @@ -804,10 +838,12 @@ void main() { expect(content.size.width, equals(tip.size.width - 2 * customPaddingVal)); }); - testWidgets('Tooltip height and padding - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip height and padding - TooltipTheme', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); const double customTooltipHeight = 100.0; const double customPaddingValue = 20.0; + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( @@ -819,7 +855,7 @@ void main() { ), child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) { return Tooltip( key: key, @@ -849,7 +885,7 @@ void main() { expect(content.size.width, equals(tip.size.width - 2 * customPaddingValue)); }); - testWidgets('Tooltip waitDuration - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip waitDuration - ThemeData.tooltipTheme', (WidgetTester tester) async { const Duration customWaitDuration = Duration(milliseconds: 500); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -896,7 +932,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip waitDuration - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip waitDuration - TooltipTheme', (WidgetTester tester) async { const Duration customWaitDuration = Duration(milliseconds: 500); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -939,7 +975,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip showDuration - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip showDuration - ThemeData.tooltipTheme', (WidgetTester tester) async { const Duration customShowDuration = Duration(milliseconds: 3000); await tester.pumpWidget( MaterialApp( @@ -976,7 +1012,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip showDuration - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip showDuration - TooltipTheme', (WidgetTester tester) async { const Duration customShowDuration = Duration(milliseconds: 3000); await tester.pumpWidget( const MaterialApp( @@ -1009,7 +1045,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip triggerMode - ThemeData.triggerMode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip triggerMode - ThemeData.triggerMode', (WidgetTester tester) async { const TooltipTriggerMode triggerMode = TooltipTriggerMode.tap; await tester.pumpWidget( MaterialApp( @@ -1034,7 +1070,7 @@ void main() { expect(find.text(tooltipText), findsOneWidget); // Tooltip should show immediately after tap }); - testWidgets('Tooltip triggerMode - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip triggerMode - TooltipTheme', (WidgetTester tester) async { const TooltipTriggerMode triggerMode = TooltipTriggerMode.tap; await tester.pumpWidget( const MaterialApp( @@ -1057,7 +1093,7 @@ void main() { expect(find.text(tooltipText), findsOneWidget); // Tooltip should show immediately after tap }); - testWidgets('Semantics included by default - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics included by default - ThemeData.tooltipTheme', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1098,7 +1134,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics included by default - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics included by default - TooltipTheme', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1141,7 +1177,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics excluded - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics excluded - ThemeData.tooltipTheme', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1185,7 +1221,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics excluded - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics excluded - TooltipTheme', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1227,7 +1263,7 @@ void main() { semantics.dispose(); }); - testWidgets('has semantic events by default - ThemeData.tooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantic events by default - ThemeData.tooltipTheme', (WidgetTester tester) async { final List<dynamic> semanticEvents = <dynamic>[]; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { semanticEvents.add(message); @@ -1270,7 +1306,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('has semantic events by default - TooltipTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has semantic events by default - TooltipTheme', (WidgetTester tester) async { final List<dynamic> semanticEvents = <dynamic>[]; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async { semanticEvents.add(message); @@ -1315,7 +1351,7 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); }); - testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default Tooltip debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Tooltip(message: 'message').debugFillProperties(builder); diff --git a/packages/flutter/test/material/tooltip_visibility_test.dart b/packages/flutter/test/material/tooltip_visibility_test.dart index ec5764271f2c1..e607bd9f7e11c 100644 --- a/packages/flutter/test/material/tooltip_visibility_test.dart +++ b/packages/flutter/test/material/tooltip_visibility_test.dart @@ -7,11 +7,12 @@ import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const String tooltipText = 'TIP'; void main() { - testWidgets('Tooltip does not build MouseRegion when mouse is detected and in TooltipVisibility with visibility = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not build MouseRegion when mouse is detected and in TooltipVisibility with visibility = false', (WidgetTester tester) async { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { return gesture.removePointer(); @@ -39,7 +40,7 @@ void main() { expect(find.descendant(of: find.byType(Tooltip), matching: find.byType(MouseRegion)), findsNothing); }); - testWidgets('Tooltip does not show when hovered when in TooltipVisibility with visible = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not show when hovered when in TooltipVisibility with visible = false', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { @@ -78,7 +79,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip shows when hovered when in TooltipVisibility with visible = true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip shows when hovered when in TooltipVisibility with visible = true', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { @@ -126,13 +127,13 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip does not build GestureDetector when in TooltipVisibility with visibility = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not build GestureDetector when in TooltipVisibility with visibility = false', (WidgetTester tester) async { await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, false); expect(find.byType(GestureDetector), findsNothing); }); - testWidgets('Tooltip triggers on tap when trigger mode is tap and in TooltipVisibility with visible = true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip triggers on tap when trigger mode is tap and in TooltipVisibility with visible = true', (WidgetTester tester) async { await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, true); final Finder tooltip = find.byType(Tooltip); @@ -142,7 +143,7 @@ void main() { expect(find.text(tooltipText), findsOneWidget); }); - testWidgets('Tooltip does not trigger manually when in TooltipVisibility with visible = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip does not trigger manually when in TooltipVisibility with visible = false', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); await tester.pumpWidget( MaterialApp( @@ -162,7 +163,7 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip triggers manually when in TooltipVisibility with visible = true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tooltip triggers manually when in TooltipVisibility with visible = true', (WidgetTester tester) async { final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/typography_test.dart b/packages/flutter/test/material/typography_test.dart index 87bc4b821fba2..a7e91eb6f5821 100644 --- a/packages/flutter/test/material/typography_test.dart +++ b/packages/flutter/test/material/typography_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('Typography is defined for all target platforms', () { @@ -89,7 +90,7 @@ void main() { } }); - testWidgets('Typography implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Typography implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Typography.material2014( black: Typography.blackCupertino, diff --git a/packages/flutter/test/material/user_accounts_drawer_header_test.dart b/packages/flutter/test/material/user_accounts_drawer_header_test.dart index 08efb09f05a84..004c801d0b457 100644 --- a/packages/flutter/test/material/user_accounts_drawer_header_test.dart +++ b/packages/flutter/test/material/user_accounts_drawer_header_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; const Key avatarA = Key('A'); @@ -82,7 +82,7 @@ void main() { matching: find.byType(Transform), ); - testWidgets('UserAccountsDrawerHeader inherits ColorScheme.primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader inherits ColorScheme.primary', (WidgetTester tester) async { const Color primaryColor = Color(0xff00ff00); const Color colorSchemePrimary = Color(0xff0000ff); @@ -95,7 +95,7 @@ void main() { expect(boxDecoration?.color == colorSchemePrimary, true); }); - testWidgets('UserAccountsDrawerHeader test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader test', (WidgetTester tester) async { await pumpTestWidget(tester); expect(find.text('A'), findsOneWidget); @@ -133,7 +133,7 @@ void main() { expect(avatarDTopRight.dx - avatarCTopRight.dx, equals(40.0 + 16.0)); // size + space between }); - testWidgets('UserAccountsDrawerHeader change default size test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader change default size test', (WidgetTester tester) async { const Size currentAccountPictureSize = Size.square(60.0); const Size otherAccountsPictureSize = Size.square(30.0); @@ -150,7 +150,7 @@ void main() { expect(otherAccountRenderBox.size, otherAccountsPictureSize); }); - testWidgets('UserAccountsDrawerHeader icon rotation test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader icon rotation test', (WidgetTester tester) async { await pumpTestWidget(tester); Transform transformWidget = tester.firstWidget(findTransform); @@ -186,7 +186,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/25801. - testWidgets('UserAccountsDrawerHeader icon does not rotate after setState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader icon does not rotate after setState', (WidgetTester tester) async { late StateSetter testSetState; await tester.pumpWidget(MaterialApp( home: Material( @@ -221,7 +221,7 @@ void main() { expect(transformWidget.transform.getRotation()[4], 1.0); }); - testWidgets('UserAccountsDrawerHeader icon rotation test speeeeeedy', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader icon rotation test speeeeeedy', (WidgetTester tester) async { await pumpTestWidget(tester); Transform transformWidget = tester.firstWidget(findTransform); @@ -262,7 +262,7 @@ void main() { expect(transformWidget.transform.getRotation()[4], 1.0); }); - testWidgets('UserAccountsDrawerHeader icon color changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader icon color changes', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: UserAccountsDrawerHeader( @@ -293,7 +293,7 @@ void main() { expect(iconWidget.color, arrowColor); }); - testWidgets('UserAccountsDrawerHeader null parameters LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader null parameters LTR', (WidgetTester tester) async { Widget buildFrame({ Widget? currentAccountPicture, List<Widget>? otherAccountsPictures, @@ -401,7 +401,7 @@ void main() { ); }); - testWidgets('UserAccountsDrawerHeader null parameters RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader null parameters RTL', (WidgetTester tester) async { Widget buildFrame({ Widget? currentAccountPicture, List<Widget>? otherAccountsPictures, @@ -512,7 +512,7 @@ void main() { ); }); - testWidgets('UserAccountsDrawerHeader provides semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader provides semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await pumpTestWidget(tester); @@ -568,7 +568,7 @@ void main() { semantics.dispose(); }); - testWidgets('alternative account selectors have sufficient tap targets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alternative account selectors have sufficient tap targets', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await pumpTestWidget(tester); @@ -589,7 +589,7 @@ void main() { handle.dispose(); }); - testWidgets('UserAccountsDrawerHeader provides semantics with missing properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UserAccountsDrawerHeader provides semantics with missing properties', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await pumpTestWidget( tester, diff --git a/packages/flutter/test/material/value_indicating_slider_test.dart b/packages/flutter/test/material/value_indicating_slider_test.dart index 4e1e46575e1aa..062115f2a7452 100644 --- a/packages/flutter/test/material/value_indicating_slider_test.dart +++ b/packages/flutter/test/material/value_indicating_slider_test.dart @@ -9,9 +9,11 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + void main() { - testWidgets('Slider value indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -52,7 +54,7 @@ void main() { ); }); - testWidgets('Slider value indicator wide text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator wide text', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -96,7 +98,7 @@ void main() { ); }); - testWidgets('Slider value indicator large text scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator large text scale', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -140,7 +142,7 @@ void main() { ); }); - testWidgets('Slider value indicator large text scale and wide text', + testWidgetsWithLeakTracking('Slider value indicator large text scale and wide text', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, @@ -193,7 +195,7 @@ void main() { // support is deprecated and the APIs are removed, these tests // can be deleted. - testWidgets('Slider value indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -231,7 +233,7 @@ void main() { ); }); - testWidgets('Slider value indicator wide text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator wide text', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -272,7 +274,7 @@ void main() { ); }); - testWidgets('Slider value indicator large text scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slider value indicator large text scale', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, value: 0, @@ -313,7 +315,7 @@ void main() { ); }); - testWidgets('Slider value indicator large text scale and wide text', + testWidgetsWithLeakTracking('Slider value indicator large text scale and wide text', (WidgetTester tester) async { await _buildValueIndicatorStaticSlider( tester, diff --git a/packages/flutter/test/material/will_pop_test.dart b/packages/flutter/test/material/will_pop_test.dart index b99e473d65b97..b5e584e6575cb 100644 --- a/packages/flutter/test/material/will_pop_test.dart +++ b/packages/flutter/test/material/will_pop_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; bool willPopValue = false; @@ -96,7 +97,7 @@ class _TestPage extends Page<dynamic> { } void main() { - testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -151,7 +152,7 @@ void main() { expect(find.text('Sample Page'), findsNothing); }); - testWidgets('willPop will only pop if the callback returns true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('willPop will only pop if the callback returns true', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( home: Scaffold( @@ -190,7 +191,7 @@ void main() { expect(find.text('Sample Form'), findsNothing); }); - testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Form.willPop can inhibit back button', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( home: Scaffold( @@ -244,7 +245,7 @@ void main() { expect(willPopCount, 1); }); - testWidgets('Form.willPop callbacks do not accumulate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Form.willPop callbacks do not accumulate', (WidgetTester tester) async { Future<bool> showYesNoAlert(BuildContext context) async { return (await showDialog<bool>( context: context, @@ -336,7 +337,7 @@ void main() { expect(find.text('Sample Form'), findsNothing); }); - testWidgets('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async { late StateSetter contentsSetState; // call this to rebuild the route's SampleForm contents bool contentsEmpty = false; // when true, don't include the SampleForm in the route @@ -396,7 +397,7 @@ void main() { expect(route.hasCallback, isFalse); }); - testWidgets('should handle new route if page moved from one navigator to another', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should handle new route if page moved from one navigator to another', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/89133 late StateSetter contentsSetState; bool moveToAnotherNavigator = false; diff --git a/packages/flutter/test/painting/_network_image_test_web.dart b/packages/flutter/test/painting/_network_image_test_web.dart index f82a6e16103dc..307d6f910841f 100644 --- a/packages/flutter/test/painting/_network_image_test_web.dart +++ b/packages/flutter/test/painting/_network_image_test_web.dart @@ -5,8 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/painting/_network_image_web.dart'; -import 'package:flutter/src/services/dom.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:web/web.dart' as web; import '../image_data.dart'; import '_test_http_request.dart'; @@ -20,7 +20,7 @@ void runTests() { (WidgetTester tester) async { final TestHttpRequest testHttpRequest = TestHttpRequest() ..status = 200 - ..mockEvent = MockEvent('load', createDomEvent('Event', 'test error')) + ..mockEvent = MockEvent('load', web.Event('test error')) ..response = (Uint8List.fromList(kTransparentImage)).buffer; httpRequestFactory = () { @@ -46,7 +46,7 @@ void runTests() { (WidgetTester tester) async { final TestHttpRequest testHttpRequest = TestHttpRequest() ..status = 404 - ..mockEvent = MockEvent('error', createDomEvent('Event', 'test error')); + ..mockEvent = MockEvent('error', web.Event('test error')); httpRequestFactory = () { @@ -64,14 +64,14 @@ void runTests() { ); await tester.pumpWidget(image); - expect((tester.takeException() as DomProgressEvent).type, 'test error'); + expect((tester.takeException() as web.ProgressEvent).type, 'test error'); }); testWidgets('loads an image from the network with empty response', (WidgetTester tester) async { final TestHttpRequest testHttpRequest = TestHttpRequest() ..status = 200 - ..mockEvent = MockEvent('load', createDomEvent('Event', 'test error')) + ..mockEvent = MockEvent('load', web.Event('test error')) ..response = (Uint8List.fromList(<int>[])).buffer; httpRequestFactory = () { diff --git a/packages/flutter/test/painting/_test_http_request.dart b/packages/flutter/test/painting/_test_http_request.dart index 8324d9b5e2a86..86e03ae80c1c1 100644 --- a/packages/flutter/test/painting/_test_http_request.dart +++ b/packages/flutter/test/painting/_test_http_request.dart @@ -4,16 +4,16 @@ import 'dart:js_interop'; -import 'package:flutter/src/services/dom.dart'; +import 'package:web/web.dart' as web; /// Defines a new property on an Object. @JS('Object.defineProperty') -external JSVoid objectDefineProperty(JSAny o, JSString symbol, JSAny desc); +external void objectDefineProperty(JSAny o, String symbol, JSAny desc); void createGetter(JSAny mock, String key, JSAny? Function() get) { objectDefineProperty( mock, - key.toJS, + key, <String, JSFunction>{ 'get': (() => get()).toJS, }.jsify()!, @@ -35,6 +35,8 @@ class DomXMLHttpRequestMock { }); } +typedef _DartDomEventListener = JSVoid Function(web.Event event); + class TestHttpRequest { TestHttpRequest() { _mock = DomXMLHttpRequestMock( @@ -43,8 +45,6 @@ class TestHttpRequest { setRequestHeader: setRequestHeader.toJS, addEventListener: addEventListener.toJS, ); - // TODO(srujzs): This is needed for when we reify JS types. Right now, JSAny - // is a typedef for Object?, but when we reify, it'll be its own type. final JSAny mock = _mock as JSAny; createGetter(mock, 'headers', () => headers.jsify()); createGetter(mock, @@ -60,26 +60,26 @@ class TestHttpRequest { Object? response; Map<String, String> get responseHeaders => headers; - JSVoid open(JSString method, JSString url, JSBoolean async) {} + JSVoid open(String method, String url, bool async) {} JSVoid send() {} - JSVoid setRequestHeader(JSString name, JSString value) { - headers[name.toDart] = value.toDart; + JSVoid setRequestHeader(String name, String value) { + headers[name] = value; } - JSVoid addEventListener(JSString type, DomEventListener listener) { - if (type.toDart == mockEvent?.type) { - final DartDomEventListener dartListener = - (listener as JSExportedDartFunction).toDart as DartDomEventListener; + JSVoid addEventListener(String type, web.EventListener listener) { + if (type == mockEvent?.type) { + final _DartDomEventListener dartListener = + (listener as JSExportedDartFunction).toDart as _DartDomEventListener; dartListener(mockEvent!.event); } } - DomXMLHttpRequest getMock() => _mock as DomXMLHttpRequest; + web.XMLHttpRequest getMock() => _mock as web.XMLHttpRequest; } class MockEvent { MockEvent(this.type, this.event); final String type; - final DomEvent event; + final web.Event event; } diff --git a/packages/flutter/test/painting/beveled_rectangle_border_test.dart b/packages/flutter/test/painting/beveled_rectangle_border_test.dart index c3d433040c548..4449af24ac3ee 100644 --- a/packages/flutter/test/painting/beveled_rectangle_border_test.dart +++ b/packages/flutter/test/painting/beveled_rectangle_border_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { test('BeveledRectangleBorder defaults', () { const BeveledRectangleBorder border = BeveledRectangleBorder(); diff --git a/packages/flutter/test/painting/border_rtl_test.dart b/packages/flutter/test/painting/border_rtl_test.dart index 47b4f1da4c8a2..bf5d47630f7fc 100644 --- a/packages/flutter/test/painting/border_rtl_test.dart +++ b/packages/flutter/test/painting/border_rtl_test.dart @@ -6,8 +6,6 @@ import 'package:flutter/foundation.dart' show DiagnosticLevel, FlutterError; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - class SillyBorder extends BoxBorder { const SillyBorder(); diff --git a/packages/flutter/test/painting/border_test.dart b/packages/flutter/test/painting/border_test.dart index db0fdc4522b5b..e4298f8c5499d 100644 --- a/packages/flutter/test/painting/border_test.dart +++ b/packages/flutter/test/painting/border_test.dart @@ -273,7 +273,7 @@ void main() { expect(error.diagnostics.length, 1); expect( error.diagnostics[0].toStringDeep(), - 'A Border can only draw strokeAlign different than\nBorderSide.strokeAlignInside on borders with uniform colors and\nstyles.\n', + 'A Border can only draw strokeAlign different than\nBorderSide.strokeAlignInside on borders with uniform colors.\n', ); }); @@ -341,8 +341,8 @@ void main() { // This falls into non-uniform border because of strokeAlign. await tester.pumpWidget(buildWidget(border: allowedBorderVariations)); - expect(tester.takeException(), isNull, - reason: 'Border with non-uniform strokeAlign should not fail.'); + expect(tester.takeException(), isAssertionError, + reason: 'Border with non-uniform strokeAlign should fail.'); await tester.pumpWidget(buildWidget( border: allowedBorderVariations, @@ -364,8 +364,8 @@ void main() { borderRadius: BorderRadius.circular(25), ), ); - expect(tester.takeException(), isAssertionError, - reason: 'Border with non-uniform styles should fail with borderRadius.'); + expect(tester.takeException(), isNull, + reason: 'Border with non-uniform styles should work with borderRadius.'); await tester.pumpWidget( buildWidget( @@ -381,6 +381,24 @@ void main() { expect(tester.takeException(), isAssertionError, reason: 'Border with non-uniform colors should fail with borderRadius.'); + await tester.pumpWidget( + buildWidget( + border: const Border(bottom: BorderSide(width: 0)), + borderRadius: BorderRadius.zero, + ), + ); + expect(tester.takeException(), isNull, + reason: 'Border with a side.width == 0 should work without borderRadius (hairline border).'); + + await tester.pumpWidget( + buildWidget( + border: const Border(bottom: BorderSide(width: 0)), + borderRadius: BorderRadius.circular(40), + ), + ); + expect(tester.takeException(), isAssertionError, + reason: 'Border with width == 0 and borderRadius should fail (hairline border).'); + // Tests for BorderDirectional. const BorderDirectional allowedBorderDirectionalVariations = BorderDirectional( start: BorderSide(width: 5), @@ -390,7 +408,7 @@ void main() { ); await tester.pumpWidget(buildWidget(border: allowedBorderDirectionalVariations)); - expect(tester.takeException(), isNull); + expect(tester.takeException(), isAssertionError); await tester.pumpWidget(buildWidget( border: allowedBorderDirectionalVariations, @@ -402,4 +420,71 @@ void main() { await tester.pumpWidget(buildWidget(border: allowedBorderDirectionalVariations, boxShape: BoxShape.circle)); expect(tester.takeException(), isNull); }); + + test('Compound borders with differing preferPaintInteriors', () { + expect(ShapeWithInterior().preferPaintInterior, isTrue); + expect(ShapeWithoutInterior().preferPaintInterior, isFalse); + expect((ShapeWithInterior() + ShapeWithInterior()).preferPaintInterior, isTrue); + expect((ShapeWithInterior() + ShapeWithoutInterior()).preferPaintInterior, isFalse); + expect((ShapeWithoutInterior() + ShapeWithInterior()).preferPaintInterior, isFalse); + expect((ShapeWithoutInterior() + ShapeWithoutInterior()).preferPaintInterior, isFalse); + }); +} + +class ShapeWithInterior extends ShapeBorder { + @override + bool get preferPaintInterior => true; + + @override + ShapeBorder scale(double t) { + return this; + } + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.zero; + + @override + Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + return Path(); + } + + @override + Path getOuterPath(Rect rect, { TextDirection? textDirection }) { + return Path(); + } + + @override + void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) { } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) { } +} + +class ShapeWithoutInterior extends ShapeBorder { + @override + bool get preferPaintInterior => false; + + @override + ShapeBorder scale(double t) { + return this; + } + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.zero; + + @override + Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + return Path(); + } + + @override + Path getOuterPath(Rect rect, { TextDirection? textDirection }) { + return Path(); + } + + @override + void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) { } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) { } } diff --git a/packages/flutter/test/painting/box_decoration_test.dart b/packages/flutter/test/painting/box_decoration_test.dart index 94ff46b87f6af..d233439589dcc 100644 --- a/packages/flutter/test/painting/box_decoration_test.dart +++ b/packages/flutter/test/painting/box_decoration_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { test('BoxDecoration.lerp identical a,b', () { expect(BoxDecoration.lerp(null, null, 0), null); diff --git a/packages/flutter/test/painting/circle_border_test.dart b/packages/flutter/test/painting/circle_border_test.dart index 6bd15000e9e7f..9c56db5c2c95a 100644 --- a/packages/flutter/test/painting/circle_border_test.dart +++ b/packages/flutter/test/painting/circle_border_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import 'common_matchers.dart'; void main() { diff --git a/packages/flutter/test/painting/colors_test.dart b/packages/flutter/test/painting/colors_test.dart index cee4ffaa30e88..4671d29db9254 100644 --- a/packages/flutter/test/painting/colors_test.dart +++ b/packages/flutter/test/painting/colors_test.dart @@ -464,7 +464,7 @@ void main() { }); test('ColorSwatch.lerp identical a,b', () { - expect(ColorSwatch.lerp(null, null, 0), null); + expect(ColorSwatch.lerp<Object?>(null, null, 0), null); const ColorSwatch<int> color = ColorSwatch<int>(0x00000000, <int, Color>{1: Color(0x00000000)}); expect(identical(ColorSwatch.lerp(color, color, 0.5), color), true); }); diff --git a/packages/flutter/test/painting/common_matchers.dart b/packages/flutter/test/painting/common_matchers.dart index 96c04b66ac61e..7c98519ef24f8 100644 --- a/packages/flutter/test/painting/common_matchers.dart +++ b/packages/flutter/test/painting/common_matchers.dart @@ -4,8 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - final Matcher isUnitCircle = isPathThat( includes: <Offset>[ const Offset(-0.6035617555492896, 0.2230970398703236), diff --git a/packages/flutter/test/painting/continuous_rectangle_border_test.dart b/packages/flutter/test/painting/continuous_rectangle_border_test.dart index 1f74c3c130b8d..47c48cd4e6405 100644 --- a/packages/flutter/test/painting/continuous_rectangle_border_test.dart +++ b/packages/flutter/test/painting/continuous_rectangle_border_test.dart @@ -10,8 +10,6 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { test('ContinuousRectangleBorder defaults', () { const ContinuousRectangleBorder border = ContinuousRectangleBorder(); diff --git a/packages/flutter/test/painting/decoration_image_lerp_test.dart b/packages/flutter/test/painting/decoration_image_lerp_test.dart new file mode 100644 index 0000000000000..44d2e3ede8293 --- /dev/null +++ b/packages/flutter/test/painting/decoration_image_lerp_test.dart @@ -0,0 +1,603 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines because it contains golden tests; see: +// https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter#reduced-test-set-tag +@Tags(<String>['reduced-test-set']) +library; + +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ImageDecoration.lerp', (WidgetTester tester) async { + final MemoryImage green = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, + 0xca, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4c, 0x54, 0x45, 0x00, 0xff, 0x00, 0x34, 0x5e, 0xc0, 0xa8, + 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, + 0x60, 0x82, + ])); + final MemoryImage red = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, + 0xca, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4c, 0x54, 0x45, 0xff, 0x00, 0x00, 0x19, 0xe2, 0x09, 0x37, + 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, + 0x60, 0x82, + ])); + + await tester.runAsync(() async { + await load(green); + await load(red); + }); + + await tester.pumpWidget( + ColoredBox( + color: Colors.white, + child: Align( + alignment: Alignment.topLeft, + child: RepaintBoundary( + child: Wrap( + textDirection: TextDirection.ltr, + children: <Widget>[ + TestImage( + DecorationImage(image: green, repeat: ImageRepeat.repeat) + ), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.1, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.2, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.8, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 0.9, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: red, repeat: ImageRepeat.repeat), + 1.0, + )), + TestImage( + DecorationImage(image: red, repeat: ImageRepeat.repeat), + ), + for (double t = 0.0; t < 1.0; t += 0.125) + TestImage(DecorationImage.lerp( + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + t, + ), + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + t, + ), + t, + )), + for (double t = 0.0; t < 1.0; t += 0.125) + TestImage(DecorationImage.lerp( + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + 1.0 - t, + ), + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + t, + ), + t, + )), + for (double t = 0.0; t < 1.0; t += 0.125) + TestImage(DecorationImage.lerp( + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + t, + ), + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + 1.0 - t, + ), + t, + )), + for (double t = 0.0; t < 1.0; t += 0.125) + TestImage(DecorationImage.lerp( + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + 1.0 - t, + ), + DecorationImage.lerp( + DecorationImage(image: green, repeat: ImageRepeat.repeat), + DecorationImage(image: green, repeat: ImageRepeat.repeat), + 1.0 - t, + ), + t, + )), + ], + ), + ), + ), + ), + ); + + await expectLater( + find.byType(Wrap), + matchesGoldenFile('decoration_image.lerp.0.png'), + ); + + if (!kIsWeb) { // TODO(ianh): https://github.com/flutter/flutter/issues/130610 + final ui.Image image = (await tester.binding.runAsync<ui.Image>(() => captureImage(find.byType(Wrap).evaluate().single)))!; + final Uint8List bytes = (await tester.binding.runAsync<ByteData?>(() => image.toByteData(format: ui.ImageByteFormat.rawStraightRgba)))!.buffer.asUint8List(); + expect(image.width, 792); + expect(image.height, 48); + expect(bytes, hasLength(image.width * image.height * 4)); + Color getPixel(int x, int y) { + final int offset = (x + y * image.width) * 4; + return Color.fromARGB(0xFF, bytes[offset], bytes[offset + 1], bytes[offset + 2]); + } + Color getBlockPixel(int index) { + int x = 12 + index * 24; + final int y = 12 + (x ~/ image.width) * 24; + x %= image.width; + return getPixel(x, y); + } + const Color lime = Color(0xFF00FF00); + expect(getBlockPixel(0), lime); // pure green + expect(getBlockPixel(1), lime); // 100% green 0% red + expect(getBlockPixel(2), const Color(0xFF19E600)); + expect(getBlockPixel(3), const Color(0xFF33CC00)); + expect(getBlockPixel(4), const Color(0xFF808000)); // 50-50 mix green/red + expect(getBlockPixel(5), const Color(0xFFCD3200)); + expect(getBlockPixel(6), const Color(0xFFE61900)); + expect(getBlockPixel(7), const Color(0xFFFF0000)); // 0% green 100% red + expect(getBlockPixel(8), const Color(0xFFFF0000)); // pure red + for (int index = 9; index < 40; index += 1) { + expect(getBlockPixel(index), lime); + } + } + }, skip: kIsWeb); // TODO(ianh): https://github.com/flutter/flutter/issues/130612, https://github.com/flutter/flutter/issues/130609 + + testWidgets('ImageDecoration.lerp', (WidgetTester tester) async { + final MemoryImage cmyk = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x02, 0x03, 0x00, 0x00, 0x00, 0xd4, 0x9f, 0x76, + 0xed, 0x00, 0x00, 0x00, 0x0c, 0x50, 0x4c, 0x54, 0x45, 0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x4c, 0x59, 0x13, 0x00, 0x00, 0x00, 0x0e, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xd7, 0x63, 0x60, 0x05, 0xc2, 0xf5, 0x0c, 0xeb, 0x01, 0x03, 0x00, 0x01, 0x69, 0x19, + 0xea, 0x34, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + ])); + final MemoryImage wrgb = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x02, 0x03, 0x00, 0x00, 0x00, 0xd4, 0x9f, 0x76, + 0xed, 0x00, 0x00, 0x00, 0x0c, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, + 0xff, 0x00, 0xff, 0x00, 0x00, 0x1e, 0x46, 0xbb, 0x1c, 0x00, 0x00, 0x00, 0x0e, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xd7, 0x63, 0xe0, 0x07, 0xc2, 0xa5, 0x0c, 0x4b, 0x01, 0x03, 0x50, 0x01, 0x69, 0x4a, + 0x78, 0x1d, 0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + ])); + + await tester.runAsync(() async { + await load(cmyk); + await load(wrgb); + }); + + await tester.pumpWidget( + ColoredBox( + color: Colors.white, + child: Align( + alignment: Alignment.topLeft, + child: RepaintBoundary( + child: Wrap( + textDirection: TextDirection.ltr, + children: <Widget>[ + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.1, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.2, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.8, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.9, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 1.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.cover), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeatY), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeatX), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, scale: 0.5, repeat: ImageRepeat.repeatX), + DecorationImage(image: cmyk, scale: 0.25, repeat: ImageRepeat.repeatY), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 1.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 1.0, + )), + ], + ), + ), + ), + ), + ); + + await expectLater( + find.byType(Wrap), + matchesGoldenFile('decoration_image.lerp.1.png'), + ); + + if (!kIsWeb) { // TODO(ianh): https://github.com/flutter/flutter/issues/130610 + final ui.Image image = (await tester.binding.runAsync<ui.Image>(() => captureImage(find.byType(Wrap).evaluate().single)))!; + final Uint8List bytes = (await tester.binding.runAsync<ByteData?>(() => image.toByteData(format: ui.ImageByteFormat.rawStraightRgba)))!.buffer.asUint8List(); + expect(image.width, 24 * 24); + expect(image.height, 1 * 24); + expect(bytes, hasLength(image.width * image.height * 4)); + Color getPixel(int x, int y) { + final int offset = (x + y * image.width) * 4; + return Color.fromARGB(0xFF, bytes[offset], bytes[offset + 1], bytes[offset + 2]); + } + Color getPixelFromBlock(int index, int dx, int dy) { + const int padding = 2; + int x = index * 24 + dx + padding; + final int y = (x ~/ image.width) * 24 + dy + padding; + x %= image.width; + return getPixel(x, y); + } + // wrgb image + expect(getPixelFromBlock(0, 5, 5), const Color(0xFFFFFFFF)); + expect(getPixelFromBlock(0, 15, 5), const Color(0xFFFF0000)); + expect(getPixelFromBlock(0, 5, 15), const Color(0xFF00FF00)); + expect(getPixelFromBlock(0, 15, 15), const Color(0xFF0000FF)); + // wrgb/cmyk 50/50 blended image + expect(getPixelFromBlock(3, 5, 5), const Color(0xFF80FFFF)); + expect(getPixelFromBlock(3, 15, 5), const Color(0xFFFF0080)); + expect(getPixelFromBlock(3, 5, 15), const Color(0xFF80FF00)); + expect(getPixelFromBlock(3, 15, 15), const Color(0xFF000080)); + // cmyk image + expect(getPixelFromBlock(6, 5, 5), const Color(0xFF00FFFF)); + expect(getPixelFromBlock(6, 15, 5), const Color(0xFFFF00FF)); + expect(getPixelFromBlock(6, 5, 15), const Color(0xFFFFFF00)); + expect(getPixelFromBlock(6, 15, 15), const Color(0xFF000000)); + // top left corner control + expect(getPixelFromBlock(14, 0, 0), const Color(0xFF00FFFF)); + expect(getPixelFromBlock(14, 1, 1), const Color(0xFF00FFFF)); + expect(getPixelFromBlock(14, 2, 0), const Color(0xFFFF00FF)); + expect(getPixelFromBlock(14, 19, 0), const Color(0xFFFF00FF)); + expect(getPixelFromBlock(14, 0, 2), const Color(0xFFFFFF00)); + expect(getPixelFromBlock(14, 0, 19), const Color(0xFFFFFF00)); + expect(getPixelFromBlock(14, 2, 2), const Color(0xFF000000)); + expect(getPixelFromBlock(14, 19, 19), const Color(0xFF000000)); + // bottom right corner control + expect(getPixelFromBlock(19, 0, 0), const Color(0xFF00FFFF)); + expect(getPixelFromBlock(19, 17, 17), const Color(0xFF00FFFF)); + expect(getPixelFromBlock(19, 19, 0), const Color(0xFFFF00FF)); + expect(getPixelFromBlock(19, 19, 17), const Color(0xFFFF00FF)); + expect(getPixelFromBlock(19, 0, 19), const Color(0xFFFFFF00)); + expect(getPixelFromBlock(19, 17, 19), const Color(0xFFFFFF00)); + expect(getPixelFromBlock(19, 18, 18), const Color(0xFF000000)); + expect(getPixelFromBlock(19, 19, 19), const Color(0xFF000000)); + } + }, skip: kIsWeb); // TODO(ianh): https://github.com/flutter/flutter/issues/130612, https://github.com/flutter/flutter/issues/130609 + + testWidgets('ImageDecoration.lerp with colored background', (WidgetTester tester) async { + final MemoryImage cmyk = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x02, 0x03, 0x00, 0x00, 0x00, 0xd4, 0x9f, 0x76, + 0xed, 0x00, 0x00, 0x00, 0x0c, 0x50, 0x4c, 0x54, 0x45, 0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x4c, 0x59, 0x13, 0x00, 0x00, 0x00, 0x0e, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xd7, 0x63, 0x60, 0x05, 0xc2, 0xf5, 0x0c, 0xeb, 0x01, 0x03, 0x00, 0x01, 0x69, 0x19, + 0xea, 0x34, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + ])); + final MemoryImage wrgb = MemoryImage(Uint8List.fromList(<int>[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x02, 0x03, 0x00, 0x00, 0x00, 0xd4, 0x9f, 0x76, + 0xed, 0x00, 0x00, 0x00, 0x0c, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, + 0xff, 0x00, 0xff, 0x00, 0x00, 0x1e, 0x46, 0xbb, 0x1c, 0x00, 0x00, 0x00, 0x0e, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xd7, 0x63, 0xe0, 0x07, 0xc2, 0xa5, 0x0c, 0x4b, 0x01, 0x03, 0x50, 0x01, 0x69, 0x4a, + 0x78, 0x1d, 0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + ])); + + await tester.runAsync(() async { + await load(cmyk); + await load(wrgb); + }); + + await tester.pumpWidget( + ColoredBox( + color: Colors.pink, + child: Align( + alignment: Alignment.topLeft, + child: Wrap( + textDirection: TextDirection.ltr, + children: <Widget>[ + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.1, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.2, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.8, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 0.9, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.contain), + DecorationImage(image: cmyk, fit: BoxFit.contain), + 1.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, fit: BoxFit.cover), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeatY), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeatX), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, repeat: ImageRepeat.repeat, opacity: 0.2), + DecorationImage(image: cmyk, repeat: ImageRepeat.repeat, opacity: 0.2), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: wrgb, scale: 0.5, repeat: ImageRepeat.repeatX), + DecorationImage(image: cmyk, scale: 0.25, repeat: ImageRepeat.repeatY), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 1.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.0, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.25, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.5, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 0.75, + )), + TestImage(DecorationImage.lerp( + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0)), + DecorationImage(image: cmyk, centerSlice: const Rect.fromLTWH(2.0, 2.0, 1.0, 1.0)), + 1.0, + )), + ], + ), + ), + ), + ); + + await expectLater( + find.byType(Wrap), + matchesGoldenFile('decoration_image.lerp.2.png'), + ); + }, skip: kIsWeb); // TODO(ianh): https://github.com/flutter/flutter/issues/130612, https://github.com/flutter/flutter/issues/130609 +} + +Future<void> load(MemoryImage image) { + final ImageStream stream = image.resolve(ImageConfiguration.empty); + final Completer<ImageInfo> completer = Completer<ImageInfo>(); + void listener(ImageInfo image, bool syncCall) { + completer.complete(image); + } + stream.addListener(ImageStreamListener(listener)); + return completer.future; +} + +class TestImage extends StatelessWidget { + TestImage(this.image); // ignore: use_key_in_widget_constructors, prefer_const_constructors_in_immutables + + final DecorationImage? image; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(2.0), + child: SizedBox( + width: 20, + height: 20, + child: DecoratedBox( + decoration: BoxDecoration( + image: image, + ), + ), + ), + ); + } +} diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index 649acd124753b..6cfa461d82d67 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -34,7 +34,7 @@ class SynchronousTestImageProvider extends ImageProvider<int> { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(TestImageInfo(key, image: image)), ); @@ -52,7 +52,7 @@ class SynchronousErrorTestImageProvider extends ImageProvider<int> { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { throw throwable; } } @@ -68,7 +68,7 @@ class AsyncTestImageProvider extends ImageProvider<int> { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( Future<ImageInfo>.value(TestImageInfo(key, image: image)), ); @@ -88,7 +88,7 @@ class DelayedImageProvider extends ImageProvider<DelayedImageProvider> { } @override - ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(DelayedImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter(_completer.future); } @@ -111,7 +111,7 @@ class MultiFrameImageProvider extends ImageProvider<MultiFrameImageProvider> { } @override - ImageStreamCompleter load(MultiFrameImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(MultiFrameImageProvider key, ImageDecoderCallback decode) { return completer; } @@ -333,6 +333,19 @@ void main() { expect(paint.invertColors, !kIsWeb); }); + test('DecorationImage.toString', () async { + expect( + DecorationImage( + image: SynchronousTestImageProvider( + await createTestImage(width: 100, height: 100), + ), + opacity: 0.99, + scale: 2.01, + ).toString(), + 'DecorationImage(SynchronousTestImageProvider(), Alignment.center, scale 2.0, opacity 1.0, FilterQuality.low)', + ); + }); + test('DecorationImage with null textDirection configuration should throw Error', () async { const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src); final ui.Image image = await createTestImage(width: 100, height: 100); diff --git a/packages/flutter/test/painting/fake_image_provider.dart b/packages/flutter/test/painting/fake_image_provider.dart index c352adaaab2df..a59f54670b58f 100644 --- a/packages/flutter/test/painting/fake_image_provider.dart +++ b/packages/flutter/test/painting/fake_image_provider.dart @@ -25,7 +25,7 @@ class FakeImageProvider extends ImageProvider<FakeImageProvider> { } @override - ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(FakeImageProvider key, ImageDecoderCallback decode) { assert(key == this); return MultiFrameImageStreamCompleter( codec: SynchronousFuture<ui.Codec>(_codec), diff --git a/packages/flutter/test/painting/image_provider_network_image_test.dart b/packages/flutter/test/painting/image_provider_network_image_test.dart index 086ffa5669c5b..45f9ffaf9e35e 100644 --- a/packages/flutter/test/painting/image_provider_network_image_test.dart +++ b/packages/flutter/test/painting/image_provider_network_image_test.dart @@ -73,6 +73,42 @@ void main() { expect(httpClient.request.response.drained, true); }, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag. + test('Expect thrown exception with statusCode - evicts from cache and drains, when using ResizeImage', () async { + const int errorStatusCode = HttpStatus.notFound; + const String requestUrl = 'foo-url'; + + httpClient.request.response.statusCode = errorStatusCode; + + final Completer<dynamic> caughtError = Completer<dynamic>(); + + final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst(requestUrl)), width: 5, height: 5); + expect(imageCache.pendingImageCount, 0); + expect(imageCache.statusForKey(imageProvider).untracked, true); + + final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); + + expect(imageCache.pendingImageCount, 1); + + result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}, + onError: (dynamic error, StackTrace? stackTrace) { + caughtError.complete(error); + })); + + final Object? err = await caughtError.future; + await Future<void>.delayed(Duration.zero); + + expect(imageCache.pendingImageCount, 0); + expect(imageCache.statusForKey(imageProvider).untracked, true); + + expect( + err, + isA<NetworkImageLoadException>() + .having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode) + .having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)), + ); + expect(httpClient.request.response.drained, true); + }, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag. + test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async { httpClient.thrownError = 'client1'; final List<dynamic> capturedErrors = <dynamic>[]; @@ -180,7 +216,7 @@ void main() { }, )); - final dynamic err = await caughtError.future; + final Object? err = await caughtError.future; expect(err, isA<SocketException>()); diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 05d3030a2b74c..737471dfeecbc 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -159,6 +159,12 @@ void main() { expect(imageCache.statusForKey(provider).untracked, false); expect(imageCache.pendingImageCount, 1); }, skip: kIsWeb); // [intended] The web cannot load files. + + test('ImageProvider toStrings', () async { + expect(const NetworkImage('test', scale: 1.21).toString(), 'NetworkImage("test", scale: 1.2)'); + expect(const ExactAssetImage('test', scale: 1.21).toString(), 'ExactAssetImage(name: "test", scale: 1.2, bundle: null)'); + expect(MemoryImage(Uint8List(0), scale: 1.21).toString(), equalsIgnoringHashCodes('MemoryImage(Uint8List#00000, scale: 1.2)')); + }); } class FakeCodec implements Codec { diff --git a/packages/flutter/test/painting/image_resolution_test.dart b/packages/flutter/test/painting/image_resolution_test.dart index 2a784de9e0764..e52f1b274b34e 100644 --- a/packages/flutter/test/painting/image_resolution_test.dart +++ b/packages/flutter/test/painting/image_resolution_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -22,6 +23,22 @@ class TestAssetBundle extends CachingAssetBundle { return const StandardMessageCodec().encodeMessage(_assetBundleMap)!; } + if (key == 'AssetManifest.bin.json') { + // Encode the manifest data that will be used by the app + final ByteData data = const StandardMessageCodec().encodeMessage(_assetBundleMap)!; + // Simulate the behavior of NetworkAssetBundle.load here, for web tests + return ByteData.sublistView( + utf8.encode( + json.encode( + base64.encode( + // Encode only the actual bytes of the buffer, and no more... + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes) + ) + ) + ) + ); + } + loadCallCount[key] = loadCallCount[key] ?? 0 + 1; if (key == 'one') { return ByteData(1) diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart index 847984663e269..4b137c3f349a6 100644 --- a/packages/flutter/test/painting/image_test_utils.dart +++ b/packages/flutter/test/painting/image_test_utils.dart @@ -28,11 +28,6 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { super.resolveStreamForKey(config, stream, key, handleError); } - @override - ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { - throw UnsupportedError('Use ImageProvider.loadImage instead.'); - } - @override ImageStreamCompleter loadBuffer(TestImageProvider key, DecoderBufferCallback decode) { throw UnsupportedError('Use ImageProvider.loadImage instead.'); diff --git a/packages/flutter/test/painting/linear_border_test.dart b/packages/flutter/test/painting/linear_border_test.dart index 7f14b691a007d..ed335221dae47 100644 --- a/packages/flutter/test/painting/linear_border_test.dart +++ b/packages/flutter/test/painting/linear_border_test.dart @@ -5,9 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - - const Rect canvasRect = Rect.fromLTWH(0, 0, 100, 100); const BorderSide borderSide = BorderSide(width: 4, color: Color(0x0f00ff00)); diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart index 1273faa4c5e81..bc80315747f76 100644 --- a/packages/flutter/test/painting/mocks_for_image_cache.dart +++ b/packages/flutter/test/painting/mocks_for_image_cache.dart @@ -54,7 +54,7 @@ class TestImageProvider extends ImageProvider<int> { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image.clone())), ); @@ -68,7 +68,7 @@ class FailingTestImageProvider extends TestImageProvider { const FailingTestImageProvider(super.key, super.imageValue, { required super.image }); @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!'))); } } @@ -95,11 +95,6 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> { throw Error(); } - @override - ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) { - throw Error(); - } - @override Future<ErrorImageProvider> obtainKey(ImageConfiguration configuration) { return SynchronousFuture<ErrorImageProvider>(this); @@ -121,11 +116,6 @@ class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvi Future<ObtainKeyErrorImageProvider> obtainKey(ImageConfiguration configuration) { throw Error(); } - - @override - ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) { - throw UnimplementedError(); - } } class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> { @@ -143,16 +133,11 @@ class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> { Future<LoadErrorImageProvider> obtainKey(ImageConfiguration configuration) { return SynchronousFuture<LoadErrorImageProvider>(this); } - - @override - ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) { - throw UnimplementedError(); - } } class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> { @override - ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(LoadErrorCompleterImageProvider key, ImageDecoderCallback decode) { final Completer<ImageInfo> completer = Completer<ImageInfo>.sync(); completer.completeError(Error()); return OneFrameImageStreamCompleter(completer.future); diff --git a/packages/flutter/test/painting/oval_border_test.dart b/packages/flutter/test/painting/oval_border_test.dart index 4b26dedfd6bb2..aabee9a7b32ad 100644 --- a/packages/flutter/test/painting/oval_border_test.dart +++ b/packages/flutter/test/painting/oval_border_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { test('OvalBorder defaults', () { const OvalBorder border = OvalBorder(); diff --git a/packages/flutter/test/painting/painting_utils.dart b/packages/flutter/test/painting/painting_utils.dart index b7edc0da1227a..7d669fa1e3672 100644 --- a/packages/flutter/test/painting/painting_utils.dart +++ b/packages/flutter/test/painting/painting_utils.dart @@ -14,9 +14,9 @@ class PaintingBindingSpy extends BindingBase with SchedulerBinding, ServicesBind int get instantiateImageCodecCalledCount => counter; @override - Future<ui.Codec> instantiateImageCodec(Uint8List list, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) { + Future<ui.Codec> instantiateImageCodecWithSize(ui.ImmutableBuffer buffer, { ui.TargetImageSizeCallback? getTargetSize }) { counter++; - return ui.instantiateImageCodec(list); + return ui.instantiateImageCodecWithSize(buffer, getTargetSize: getTargetSize); } @override diff --git a/packages/flutter/test/painting/rounded_rectangle_border_test.dart b/packages/flutter/test/painting/rounded_rectangle_border_test.dart index 94acd9c660c06..8a8ec49cfac95 100644 --- a/packages/flutter/test/painting/rounded_rectangle_border_test.dart +++ b/packages/flutter/test/painting/rounded_rectangle_border_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import 'common_matchers.dart'; void main() { diff --git a/packages/flutter/test/painting/shape_border_test.dart b/packages/flutter/test/painting/shape_border_test.dart index 958d33fa47caa..d1a854bdb606e 100644 --- a/packages/flutter/test/painting/shape_border_test.dart +++ b/packages/flutter/test/painting/shape_border_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { test('Border.lerp identical a,b', () { expect(Border.lerp(null, null, 0), null); diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index 402aa70c3b6e1..269fb9e416b16 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart'; void main() { @@ -157,7 +156,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { } @override - ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(TestImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(ImageInfo(image: image)), ); diff --git a/packages/flutter/test/painting/stadium_border_test.dart b/packages/flutter/test/painting/stadium_border_test.dart index 257aa4a1516ff..2347e264e46c3 100644 --- a/packages/flutter/test/painting/stadium_border_test.dart +++ b/packages/flutter/test/painting/stadium_border_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import 'common_matchers.dart'; void main() { diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart index 57b6d63023597..8dec06994531b 100644 --- a/packages/flutter/test/painting/text_painter_test.dart +++ b/packages/flutter/test/painting/text_painter_test.dart @@ -400,27 +400,27 @@ void main() { painter.dispose(); }); - test('TextPainter textScaleFactor test', () { + test('TextPainter textScaler test', () { final TextPainter painter = TextPainter( text: const TextSpan( text: 'X', style: TextStyle(inherit: false, fontSize: 10.0), ), textDirection: TextDirection.ltr, - textScaleFactor: 2.0, + textScaler: const TextScaler.linear(2.0), ); painter.layout(); expect(painter.size, const Size(20.0, 20.0)); painter.dispose(); }); - test('TextPainter textScaleFactor null style test', () { + test('TextPainter textScaler null style test', () { final TextPainter painter = TextPainter( text: const TextSpan( text: 'X', ), textDirection: TextDirection.ltr, - textScaleFactor: 2.0, + textScaler: const TextScaler.linear(2.0), ); painter.layout(); expect(painter.size, const Size(28.0, 28.0)); @@ -1509,9 +1509,6 @@ void main() { }); test('TextPainter line breaking does not round to integers', () { - if (! const bool.hasEnvironment('SKPARAGRAPH_REMOVE_ROUNDING_HACK')) { - return; - } const double fontSize = 1.25; const String text = '12345'; assert((fontSize * text.length).truncate() != fontSize * text.length); diff --git a/packages/flutter/test/painting/text_scaler_test.dart b/packages/flutter/test/painting/text_scaler_test.dart new file mode 100644 index 0000000000000..6083f15ce2aef --- /dev/null +++ b/packages/flutter/test/painting/text_scaler_test.dart @@ -0,0 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Linear TextScaler', () { + test('equality', () { + const TextScaler a = TextScaler.linear(3.0); + final TextScaler b = TextScaler.noScaling.clamp(minScaleFactor: 3.0); + // Creates a non-const TextScaler instance. + final TextScaler c = TextScaler.linear(3.0); // ignore: prefer_const_constructors + final TextScaler d = TextScaler.noScaling + .clamp(minScaleFactor: 1, maxScaleFactor: 5) + .clamp(minScaleFactor: 3, maxScaleFactor: 6); + + final List<TextScaler> list = <TextScaler>[a, b, c, d]; + for (final TextScaler lhs in list) { + expect(list, everyElement(lhs)); + } + }); + + test('clamping', () { + expect(TextScaler.noScaling.clamp(minScaleFactor: 3.0), const TextScaler.linear(3.0)); + expect(const TextScaler.linear(5.0).clamp(maxScaleFactor: 3.0), const TextScaler.linear(3.0)); + expect(const TextScaler.linear(5.0).clamp(maxScaleFactor: 3.0), const TextScaler.linear(3.0)); + expect(const TextScaler.linear(5.0).clamp(minScaleFactor: 3.0, maxScaleFactor: 3.0), const TextScaler.linear(3.0)); + // Asserts when min > max. + expect( + () => TextScaler.noScaling.clamp(minScaleFactor: 5.0, maxScaleFactor: 4.0), + throwsA(isA<AssertionError>().having((AssertionError error) => error.toString(), 'message', contains('maxScaleFactor >= minScaleFactor'))), + ); + }); + }); +} diff --git a/packages/flutter/test/painting/text_style_test.dart b/packages/flutter/test/painting/text_style_test.dart index d2bd4c4b1ef65..6480a7f822642 100644 --- a/packages/flutter/test/painting/text_style_test.dart +++ b/packages/flutter/test/painting/text_style_test.dart @@ -450,6 +450,13 @@ void main() { }); test('backgroundColor', () { + // TODO(matanlurey): Remove when https://github.com/flutter/engine/pull/44705 rolls into the framework. + // Currently, dithering is disabled by default, but it's about to be flipped (enabled by default), + // and deprecated. This avoids #44705 causing a breakage in this test. + // + // ignore: deprecated_member_use + Paint.enableDithering = true; + const TextStyle s1 = TextStyle(); expect(s1.backgroundColor, isNull); expect(s1.toString(), 'TextStyle(<all styles inherited>)'); @@ -459,7 +466,16 @@ void main() { expect(s2.toString(), 'TextStyle(inherit: true, backgroundColor: Color(0xff00ff00))'); final ui.TextStyle ts2 = s2.getTextStyle(); - expect(ts2.toString(), contains('background: Paint(Color(0xff00ff00))')); + + // TODO(matanlurey): Remove when https://github.com/flutter/flutter/issues/133698 is resolved. + // There are 5+ implementations of Paint, so we should either align the toString() or stop + // testing it, IMO. + if (kIsWeb) { + // The web implementation never includes "dither: ..." as a property. + expect(ts2.toString(), contains('background: Paint(Color(0xff00ff00))')); + } else { + expect(ts2.toString(), contains('background: Paint(Color(0xff00ff00); dither: true)')); + } }); test('TextStyle background and backgroundColor combos', () { @@ -503,9 +519,9 @@ void main() { expect(TextStyle.lerp(redPaintTextStyle, bluePaintTextStyle, .75)!.background!.color, blue); }); - test('TextStyle strut textScaleFactor', () { + test('TextStyle strut textScaler', () { const TextStyle style0 = TextStyle(fontSize: 10); - final ui.ParagraphStyle paragraphStyle0 = style0.getParagraphStyle(textScaleFactor: 2.5); + final ui.ParagraphStyle paragraphStyle0 = style0.getParagraphStyle(textScaler: const TextScaler.linear(2.5)); const TextStyle style1 = TextStyle(fontSize: 25); final ui.ParagraphStyle paragraphStyle1 = style1.getParagraphStyle(); diff --git a/packages/flutter/test/painting/widget_span_test.dart b/packages/flutter/test/painting/widget_span_test.dart deleted file mode 100644 index 632920754e4cd..0000000000000 --- a/packages/flutter/test/painting/widget_span_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('WidgetSpan codeUnitAt', () { - const InlineSpan span = WidgetSpan(child: SizedBox()); - expect(span.codeUnitAt(-1), isNull); - expect(span.codeUnitAt(0), PlaceholderSpan.placeholderCodeUnit); - expect(span.codeUnitAt(1), isNull); - expect(span.codeUnitAt(2), isNull); - - const InlineSpan nestedSpan = TextSpan( - text: 'AAA', - children: <InlineSpan>[span, span], - ); - expect(nestedSpan.codeUnitAt(-1), isNull); - expect(nestedSpan.codeUnitAt(0), 65); - expect(nestedSpan.codeUnitAt(1), 65); - expect(nestedSpan.codeUnitAt(2), 65); - expect(nestedSpan.codeUnitAt(3), PlaceholderSpan.placeholderCodeUnit); - expect(nestedSpan.codeUnitAt(4), PlaceholderSpan.placeholderCodeUnit); - expect(nestedSpan.codeUnitAt(5), isNull); - }); -} diff --git a/packages/flutter/test/rendering/baseline_test.dart b/packages/flutter/test/rendering/baseline_test.dart index 239a050ba68e4..3084acd4d466a 100644 --- a/packages/flutter/test/rendering/baseline_test.dart +++ b/packages/flutter/test/rendering/baseline_test.dart @@ -52,4 +52,56 @@ void main() { expect(childParentData.offset.dy, equals(10.0)); expect(parent.size, equals(const Size(100.0, 110.0))); }); + + test('RenderFlex and RenderIgnoreBaseline (control test -- with baseline)', () { + final RenderBox a, b; + final RenderBox root = RenderFlex( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + textDirection: TextDirection.ltr, + children: <RenderBox>[ + a = RenderParagraph( + const TextSpan(text: 'a', style: TextStyle(fontSize: 128.0, fontFamily: 'FlutterTest')), // places baseline at y=96 + textDirection: TextDirection.ltr, + ), + b = RenderParagraph( + const TextSpan(text: 'b', style: TextStyle(fontSize: 32.0, fontFamily: 'FlutterTest')), // 24 above baseline, 8 below baseline + textDirection: TextDirection.ltr, + ), + ], + ); + layout(root); + + final Offset aPos = a.localToGlobal(Offset.zero); + final Offset bPos = b.localToGlobal(Offset.zero); + expect(aPos.dy, 0.0); + expect(bPos.dy, 96.0 - 24.0); + }); + + test('RenderFlex and RenderIgnoreBaseline (with ignored baseline)', () { + final RenderBox a, b; + final RenderBox root = RenderFlex( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + textDirection: TextDirection.ltr, + children: <RenderBox>[ + RenderIgnoreBaseline( + child: a = RenderParagraph( + const TextSpan(text: 'a', style: TextStyle(fontSize: 128.0, fontFamily: 'FlutterTest')), + textDirection: TextDirection.ltr, + ), + ), + b = RenderParagraph( + const TextSpan(text: 'b', style: TextStyle(fontSize: 32.0, fontFamily: 'FlutterTest')), + textDirection: TextDirection.ltr, + ), + ], + ); + layout(root); + + final Offset aPos = a.localToGlobal(Offset.zero); + final Offset bPos = b.localToGlobal(Offset.zero); + expect(aPos.dy, 0.0); + expect(bPos.dy, 0.0); + }); } diff --git a/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart b/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart index 5374454db90be..81d79a5a82e34 100644 --- a/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart +++ b/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart @@ -13,20 +13,20 @@ void main() { tearDown(() { final List<PipelineOwner> children = <PipelineOwner>[]; - RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) { + RendererBinding.instance.rootPipelineOwner.visitChildren((PipelineOwner child) { children.add(child); }); - children.forEach(RendererBinding.instance.pipelineOwner.dropChild); + children.forEach(RendererBinding.instance.rootPipelineOwner.dropChild); }); test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () { final PipelineOwner child = PipelineOwner(); - RendererBinding.instance.pipelineOwner.adoptChild(child); + RendererBinding.instance.rootPipelineOwner.adoptChild(child); final RenderObject renderObject = TestRenderObject(); child.rootNode = renderObject; renderObject.scheduleInitialLayout(); - RendererBinding.instance.pipelineOwner.flushLayout(); + RendererBinding.instance.rootPipelineOwner.flushLayout(); MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0; renderObject.markNeedsLayout(); @@ -37,20 +37,20 @@ void main() { final PipelineOwner child = PipelineOwner( onSemanticsUpdate: (_) { }, ); - RendererBinding.instance.pipelineOwner.adoptChild(child); + RendererBinding.instance.rootPipelineOwner.adoptChild(child); expect(child.semanticsOwner, isNull); - expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); + expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull); final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); expect(child.semanticsOwner, isNotNull); - expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull); + expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNotNull); handle.dispose(); expect(child.semanticsOwner, isNull); - expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); + expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull); }); } diff --git a/packages/flutter/test/rendering/binding_test.dart b/packages/flutter/test/rendering/binding_test.dart index 80495aa264c02..67cc00b998dac 100644 --- a/packages/flutter/test/rendering/binding_test.dart +++ b/packages/flutter/test/rendering/binding_test.dart @@ -10,29 +10,48 @@ import 'package:flutter_test/flutter_test.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - test('handleMetricsChanged does not scheduleForcedFrame unless there is a child to the renderView', () async { + test('handleMetricsChanged does not scheduleForcedFrame unless there a registered renderView with a child', () async { expect(SchedulerBinding.instance.hasScheduledFrame, false); RendererBinding.instance.handleMetricsChanged(); expect(SchedulerBinding.instance.hasScheduledFrame, false); + RendererBinding.instance.addRenderView(RendererBinding.instance.renderView); + RendererBinding.instance.handleMetricsChanged(); + expect(SchedulerBinding.instance.hasScheduledFrame, false); + RendererBinding.instance.renderView.child = RenderLimitedBox(); RendererBinding.instance.handleMetricsChanged(); expect(SchedulerBinding.instance.hasScheduledFrame, true); + + RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView); }); test('debugDumpSemantics prints explanation when semantics are unavailable', () { + RendererBinding.instance.addRenderView(RendererBinding.instance.renderView); final List<String?> log = <String?>[]; debugPrint = (String? message, {int? wrapWidth}) { log.add(message); }; debugDumpSemanticsTree(); expect(log, hasLength(1)); + expect(log.single, startsWith('Semantics not generated')); + expect(log.single, endsWith( + 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' + 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' + 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' + )); + RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView); + }); + + test('root pipeline owner cannot manage root node', () { + final RenderObject rootNode = RenderProxyBox(); expect( - log.single, - 'Semantics not generated.\n' - 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' - 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' - 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' + () => RendererBinding.instance.rootPipelineOwner.rootNode = rootNode, + throwsA(isFlutterError.having( + (FlutterError e) => e.message, + 'message', + contains('Cannot set a rootNode on the default root pipeline owner.'), + )), ); }); } diff --git a/packages/flutter/test/rendering/debug_overflow_indicator_test.dart b/packages/flutter/test/rendering/debug_overflow_indicator_test.dart index 8186f1714031e..7e49c164fa9c3 100644 --- a/packages/flutter/test/rendering/debug_overflow_indicator_test.dart +++ b/packages/flutter/test/rendering/debug_overflow_indicator_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - void main() { testWidgets('overflow indicator is not shown when not overflowing', (WidgetTester tester) async { await tester.pumpWidget( diff --git a/packages/flutter/test/rendering/debug_test.dart b/packages/flutter/test/rendering/debug_test.dart index 034f0144076a5..e3e73b9ff3a97 100644 --- a/packages/flutter/test/rendering/debug_test.dart +++ b/packages/flutter/test/rendering/debug_test.dart @@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_math/vector_math_64.dart'; -import 'mock_canvas.dart'; import 'rendering_tester.dart'; void main() { diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index 0a31aa54e15ab..c980447eaf093 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -9,8 +9,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/text_input.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; -import 'recording_canvas.dart'; import 'rendering_tester.dart'; void _applyParentData(List<RenderBox> inlineRenderBoxes, InlineSpan span) { @@ -230,7 +228,6 @@ void main() { ' │ maxLines: 1\n' ' │ minLines: null\n' ' │ selectionColor: null\n' - ' │ textScaleFactor: 1.0\n' ' │ locale: ja_JP\n' ' │ selection: null\n' ' │ offset: _FixedViewportOffset#00000(offset: 0.0)\n' @@ -331,7 +328,7 @@ void main() { ), )); - editable.textScaleFactor = 2; + editable.textScaler = const TextScaler.linear(2.0); pumpFrame(phase: EnginePhase.compositingBits); // Now the caret height is much bigger due to the bigger font scale. @@ -454,7 +451,7 @@ void main() { ), )); - editable.textScaleFactor = 2; + editable.textScaler = const TextScaler.linear(2.0); pumpFrame(phase: EnginePhase.compositingBits); // Now the caret height is much bigger due to the bigger font scale. @@ -1631,7 +1628,7 @@ void main() { selection: const TextSelection.collapsed(offset: 3), maxLines: 2, minLines: 2, - textScaleFactor: 2.0, + textScaler: const TextScaler.linear(2.0), children: renderBoxes, ); _applyParentData(renderBoxes, editable.text!); @@ -1722,6 +1719,89 @@ void main() { editable.hitTest(result, position: const Offset(5.0, 15.0)); expect(result.path, hasLength(0)); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 + + test('hits correct WidgetSpan when scrolled', () { + final String text = '${"\n" * 10}test'; + final TextSelectionDelegate delegate = _FakeEditableTextState() + ..textEditingValue = TextEditingValue( + text: text, + selection: const TextSelection.collapsed(offset: 13), + ); + final List<RenderBox> renderBoxes = <RenderBox>[ + RenderParagraph(const TextSpan(text: 'a'), textDirection: TextDirection.ltr), + RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr), + RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr), + ]; + final RenderEditable editable = RenderEditable( + maxLines: null, + text: TextSpan( + style: const TextStyle(height: 1.0, fontSize: 10.0), + children: <InlineSpan>[ + TextSpan(text: text), + const WidgetSpan(child: Text('a')), + const TextSpan(children: <InlineSpan>[ + WidgetSpan(child: Text('b')), + WidgetSpan(child: Text('c')), + ], + ), + ], + ), + startHandleLayerLink: LayerLink(), + endHandleLayerLink: LayerLink(), + textDirection: TextDirection.ltr, + offset: ViewportOffset.fixed(100.0), // equal to the height of the 10 empty lines + textSelectionDelegate: delegate, + selection: const TextSelection.collapsed( + offset: 0, + ), + children: renderBoxes, + ); + _applyParentData(renderBoxes, editable.text!); + layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0))); + // Prepare for painting after layout. + pumpFrame(phase: EnginePhase.compositingBits); + BoxHitTestResult result = BoxHitTestResult(); + editable.hitTest(result, position: Offset.zero); + // We expect two hit test entries in the path because the RenderEditable + // will add itself as well. + expect(result.path, hasLength(2)); + HitTestTarget target = result.path.first.target; + expect(target, isA<TextSpan>()); + expect((target as TextSpan).text, text); + // Only testing the RenderEditable entry here once, not anymore below. + expect(result.path.last.target, isA<RenderEditable>()); + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(15.0, 0.0)); + expect(result.path, hasLength(2)); + target = result.path.first.target; + expect(target, isA<TextSpan>()); + expect((target as TextSpan).text, text); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(41.0, 0.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA<TextSpan>()); + expect((target as TextSpan).text, 'a'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(55.0, 0.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA<TextSpan>()); + expect((target as TextSpan).text, 'b'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(69.0, 5.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA<TextSpan>()); + expect((target as TextSpan).text, 'c'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(5.0, 15.0)); + expect(result.path, hasLength(2)); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 }); test('does not skip TextPainter.layout because of invalid cache', () { diff --git a/packages/flutter/test/rendering/error_test.dart b/packages/flutter/test/rendering/error_test.dart index adc98034a623f..8804f3319c6a5 100644 --- a/packages/flutter/test/rendering/error_test.dart +++ b/packages/flutter/test/rendering/error_test.dart @@ -6,8 +6,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; - /// Unit tests error.dart's usage via ErrorWidget. void main() { const String errorMessage = 'Some error message'; diff --git a/packages/flutter/test/rendering/flex_overflow_test.dart b/packages/flutter/test/rendering/flex_overflow_test.dart index 7774400da177e..0119c83919545 100644 --- a/packages/flutter/test/rendering/flex_overflow_test.dart +++ b/packages/flutter/test/rendering/flex_overflow_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; - void main() { testWidgets('Flex overflow indicator', (WidgetTester tester) async { await tester.pumpWidget( diff --git a/packages/flutter/test/rendering/mouse_tracker_test_utils.dart b/packages/flutter/test/rendering/mouse_tracker_test_utils.dart index 1f9c5bfe052dc..eb81870be9c7c 100644 --- a/packages/flutter/test/rendering/mouse_tracker_test_utils.dart +++ b/packages/flutter/test/rendering/mouse_tracker_test_utils.dart @@ -32,8 +32,21 @@ class TestMouseTrackerFlutterBinding extends BindingBase postFrameCallbacks = <void Function(Duration)>[]; } + late final RenderView _renderView = RenderView( + view: platformDispatcher.implicitView!, + ); + + late final PipelineOwner _pipelineOwner = PipelineOwner( + onSemanticsUpdate: (ui.SemanticsUpdate _) { assert(false); }, + ); + void setHitTest(BoxHitTest hitTest) { - renderView.child = _TestHitTester(hitTest); + if (_pipelineOwner.rootNode == null) { + _pipelineOwner.rootNode = _renderView; + rootPipelineOwner.adoptChild(_pipelineOwner); + addRenderView(_renderView); + } + _renderView.child = _TestHitTester(hitTest); } SchedulerPhase? _overridePhase; diff --git a/packages/flutter/test/rendering/multi_view_binding_test.dart b/packages/flutter/test/rendering/multi_view_binding_test.dart new file mode 100644 index 0000000000000..9a57757975895 --- /dev/null +++ b/packages/flutter/test/rendering/multi_view_binding_test.dart @@ -0,0 +1,208 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final RendererBinding binding = RenderingFlutterBinding.ensureInitialized(); + + test('Adding/removing renderviews updates renderViews getter', () { + final FlutterView flutterView = FakeFlutterView(); + final RenderView view = RenderView(view: flutterView); + + expect(binding.renderViews, isEmpty); + binding.addRenderView(view); + expect(binding.renderViews, contains(view)); + expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio); + expect(view.configuration.size, flutterView.physicalSize / flutterView.devicePixelRatio); + + binding.removeRenderView(view); + expect(binding.renderViews, isEmpty); + }); + + test('illegal add/remove renderviews', () { + final FlutterView flutterView = FakeFlutterView(); + final RenderView view1 = RenderView(view: flutterView); + final RenderView view2 = RenderView(view: flutterView); + final RenderView view3 = RenderView(view: FakeFlutterView(viewId: 200)); + + expect(binding.renderViews, isEmpty); + binding.addRenderView(view1); + expect(binding.renderViews, contains(view1)); + + expect(() => binding.addRenderView(view1), throwsAssertionError); + expect(() => binding.addRenderView(view2), throwsAssertionError); + expect(() => binding.removeRenderView(view2), throwsAssertionError); + expect(() => binding.removeRenderView(view3), throwsAssertionError); + + expect(binding.renderViews, contains(view1)); + binding.removeRenderView(view1); + expect(binding.renderViews, isEmpty); + expect(() => binding.removeRenderView(view1), throwsAssertionError); + expect(() => binding.removeRenderView(view2), throwsAssertionError); + }); + + test('changing metrics updates configuration', () { + final FakeFlutterView flutterView = FakeFlutterView(); + final RenderView view = RenderView(view: flutterView); + binding.addRenderView(view); + expect(view.configuration.devicePixelRatio, 2.5); + expect(view.configuration.size, const Size(160.0, 240.0)); + + flutterView.devicePixelRatio = 3.0; + flutterView.physicalSize = const Size(300, 300); + binding.handleMetricsChanged(); + expect(view.configuration.devicePixelRatio, 3.0); + expect(view.configuration.size, const Size(100.0, 100.0)); + + binding.removeRenderView(view); + }); + + test('semantics actions are performed on the right view', () { + final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1); + final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2); + final RenderView renderView1 = RenderView(view: flutterView1); + final RenderView renderView2 = RenderView(view: flutterView2); + final PipelineOwnerSpy owner1 = PipelineOwnerSpy() + ..rootNode = renderView1; + final PipelineOwnerSpy owner2 = PipelineOwnerSpy() + ..rootNode = renderView2; + + binding.addRenderView(renderView1); + binding.addRenderView(renderView2); + + binding.performSemanticsAction( + const SemanticsActionEvent(type: SemanticsAction.copy, viewId: 1, nodeId: 11), + ); + expect(owner1.semanticsOwner.performedActions.single, (11, SemanticsAction.copy, null)); + expect(owner2.semanticsOwner.performedActions, isEmpty); + owner1.semanticsOwner.performedActions.clear(); + + binding.performSemanticsAction( + const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 2, nodeId: 22), + ); + expect(owner1.semanticsOwner.performedActions, isEmpty); + expect(owner2.semanticsOwner.performedActions.single, (22, SemanticsAction.tap, null)); + owner2.semanticsOwner.performedActions.clear(); + + binding.performSemanticsAction( + const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 3, nodeId: 22), + ); + expect(owner1.semanticsOwner.performedActions, isEmpty); + expect(owner2.semanticsOwner.performedActions, isEmpty); + + binding.removeRenderView(renderView1); + binding.removeRenderView(renderView2); + }); + + test('all registered renderviews are asked to composite frame', () { + final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1); + final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2); + final RenderView renderView1 = RenderView(view: flutterView1); + final RenderView renderView2 = RenderView(view: flutterView2); + final PipelineOwner owner1 = PipelineOwner()..rootNode = renderView1; + final PipelineOwner owner2 = PipelineOwner()..rootNode = renderView2; + binding.rootPipelineOwner.adoptChild(owner1); + binding.rootPipelineOwner.adoptChild(owner2); + binding.addRenderView(renderView1); + binding.addRenderView(renderView2); + renderView1.prepareInitialFrame(); + renderView2.prepareInitialFrame(); + + expect(flutterView1.renderedScenes, isEmpty); + expect(flutterView2.renderedScenes, isEmpty); + + binding.handleBeginFrame(Duration.zero); + binding.handleDrawFrame(); + + expect(flutterView1.renderedScenes, hasLength(1)); + expect(flutterView2.renderedScenes, hasLength(1)); + + binding.removeRenderView(renderView1); + + binding.handleBeginFrame(Duration.zero); + binding.handleDrawFrame(); + + expect(flutterView1.renderedScenes, hasLength(1)); + expect(flutterView2.renderedScenes, hasLength(2)); + + binding.removeRenderView(renderView2); + + binding.handleBeginFrame(Duration.zero); + binding.handleDrawFrame(); + + expect(flutterView1.renderedScenes, hasLength(1)); + expect(flutterView2.renderedScenes, hasLength(2)); + }); + + test('hit-testing reaches the right view', () { + final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1); + final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2); + final RenderView renderView1 = RenderView(view: flutterView1); + final RenderView renderView2 = RenderView(view: flutterView2); + binding.addRenderView(renderView1); + binding.addRenderView(renderView2); + + HitTestResult result = HitTestResult(); + binding.hitTestInView(result, Offset.zero, 1); + expect(result.path, hasLength(2)); + expect(result.path.first.target, renderView1); + expect(result.path.last.target, binding); + + result = HitTestResult(); + binding.hitTestInView(result, Offset.zero, 2); + expect(result.path, hasLength(2)); + expect(result.path.first.target, renderView2); + expect(result.path.last.target, binding); + + result = HitTestResult(); + binding.hitTestInView(result, Offset.zero, 3); + expect(result.path.single.target, binding); + + binding.removeRenderView(renderView1); + binding.removeRenderView(renderView2); + }); +} + +class FakeFlutterView extends Fake implements FlutterView { + FakeFlutterView({ + this.viewId = 100, + this.devicePixelRatio = 2.5, + this.physicalSize = const Size(400,600), + this.padding = FakeViewPadding.zero, + }); + + @override + final int viewId; + @override + double devicePixelRatio; + @override + Size physicalSize; + @override + ViewPadding padding; + + List<Scene> renderedScenes = <Scene>[]; + + @override + void render(Scene scene) { + renderedScenes.add(scene); + } +} + +class PipelineOwnerSpy extends PipelineOwner { + @override + final SemanticsOwnerSpy semanticsOwner = SemanticsOwnerSpy(); +} + +class SemanticsOwnerSpy extends Fake implements SemanticsOwner { + final List<(int, SemanticsAction, Object?)> performedActions = <(int, SemanticsAction, Object?)>[]; + + @override + void performAction(int id, SemanticsAction action, [ Object? args ]) { + performedActions.add((id, action, args)); + } +} diff --git a/packages/flutter/test/rendering/painting_context_test.dart b/packages/flutter/test/rendering/painting_context_test.dart new file mode 100644 index 0000000000000..0b97e7db3440f --- /dev/null +++ b/packages/flutter/test/rendering/painting_context_test.dart @@ -0,0 +1,32 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'rendering_tester.dart'; + +void main() { + TestRenderingFlutterBinding.ensureInitialized(); + + test('PaintingContext.setIsComplexHint', () { + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.zero); + expect(layer.hasChildren, isFalse); + context.setIsComplexHint(); + expect(layer.hasChildren, isTrue); + expect(layer.firstChild, isA<PictureLayer>()); + expect((layer.firstChild! as PictureLayer).isComplexHint, isTrue); + }); + + test('PaintingContext.setWillChangeHint', () { + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.zero); + expect(layer.hasChildren, isFalse); + context.setWillChangeHint(); + expect(layer.hasChildren, isTrue); + expect(layer.firstChild, isA<PictureLayer>()); + expect((layer.firstChild! as PictureLayer).willChangeHint, isTrue); + }); +} diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index 8ae7c59ec126e..06aa6a151a3d9 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -406,7 +406,7 @@ void main() { expect(paragraph.debugNeedsPaint, isFalse); }); - test('nested TextSpans in paragraph handle textScaleFactor correctly.', () { + test('nested TextSpans in paragraph handle linear textScaler correctly.', () { const TextSpan testSpan = TextSpan( text: 'a', style: TextStyle( @@ -430,21 +430,21 @@ void main() { final RenderParagraph paragraph = RenderParagraph( testSpan, textDirection: TextDirection.ltr, - textScaleFactor: 1.3, + textScaler: const TextScaler.linear(1.3), ); paragraph.layout(const BoxConstraints()); expect(paragraph.size.width, 78.0); expect(paragraph.size.height, 26.0); + final int length = testSpan.toPlainText().length; // Test the sizes of nested spans. - final String text = testSpan.toStringDeep(); final List<ui.TextBox> boxes = <ui.TextBox>[ - for (int i = 0; i < text.length; ++i) + for (int i = 0; i < length; ++i) ...paragraph.getBoxesForSelection( TextSelection(baseOffset: i, extentOffset: i + 1), ), ]; - expect(boxes.length, equals(4)); + expect(boxes, hasLength(4)); expect(boxes[0].toRect().width, 13.0); expect(boxes[0].toRect().height, 13.0); @@ -978,7 +978,8 @@ void main() { granularity: TextGranularity.word, ), ); - expect(paragraph.selections.length, 0); // how []are you + expect(paragraph.selections.length, 1); // how []are you + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 4)); // Equivalent to sending shift + alt + arrow-left. registrar.selectables[0].dispatchSelectionEvent( diff --git a/packages/flutter/test/rendering/pipeline_owner_tree_test.dart b/packages/flutter/test/rendering/pipeline_owner_tree_test.dart index 5c2368dee5a7a..2f997ea7c670e 100644 --- a/packages/flutter/test/rendering/pipeline_owner_tree_test.dart +++ b/packages/flutter/test/rendering/pipeline_owner_tree_test.dart @@ -678,20 +678,43 @@ void main() { expect(root.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNotNull); - expect(childOfChild.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached. final SemanticsHandle childSemantics = child.ensureSemantics(); root.dropChild(child); expect(root.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNotNull); - expect(childOfChild.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached. childSemantics.dispose(); expect(root.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNull); - expect(childOfChild.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = false; + + expect(root.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNotNull); + + root.adoptChild(childOfChild); + expect(root.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNull); // Disposed on re-attachment. + + manifold.semanticsEnabled = true; + expect(root.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNotNull); + + root.dropChild(childOfChild); + + expect(root.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNotNull); + + childOfChild.dispose(); + + expect(root.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNull); // Disposed on dispose. }); test('can adopt/drop children during own layout', () { @@ -789,6 +812,38 @@ void main() { }); expect(children.single, childOfChild3); }); + + test('printing pipeline owner tree smoke test', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child1 = PipelineOwner() + ..rootNode = FakeRenderView(); + final PipelineOwner childOfChild1 = PipelineOwner() + ..rootNode = FakeRenderView(); + final PipelineOwner child2 = PipelineOwner() + ..rootNode = FakeRenderView(); + final PipelineOwner childOfChild2 = PipelineOwner() + ..rootNode = FakeRenderView(); + + root.adoptChild(child1); + child1.adoptChild(childOfChild1); + root.adoptChild(child2); + child2.adoptChild(childOfChild2); + + expect(root.toStringDeep(), equalsIgnoringHashCodes( + 'PipelineOwner#00000\n' + ' ├─PipelineOwner#00000\n' + ' │ │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n' + ' │ │\n' + ' │ └─PipelineOwner#00000\n' + ' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n' + ' │\n' + ' └─PipelineOwner#00000\n' + ' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n' + ' │\n' + ' └─PipelineOwner#00000\n' + ' rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n' + )); + }); } class TestPipelineManifold extends ChangeNotifier implements PipelineManifold { @@ -860,3 +915,5 @@ List<PipelineOwner> _treeWalk(PipelineOwner root) { root.visitChildren(visitor); return results; } + +class FakeRenderView extends RenderBox { } diff --git a/packages/flutter/test/rendering/platform_view_test.dart b/packages/flutter/test/rendering/platform_view_test.dart index baee5bc54586d..c6ab6cc9b54f0 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -47,10 +47,10 @@ void main() { child: platformViewRenderBox, ); int semanticsUpdateCount = 0; - final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics( - listener: () { - ++semanticsUpdateCount; - }, + final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.rootPipelineOwner.ensureSemantics( + listener: () { + ++semanticsUpdateCount; + }, ); layout(tree, phase: EnginePhase.flushSemantics); // Initial semantics update diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index 38fe9a0a0b47a..4dfa6c2b9ba4f 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; import 'rendering_tester.dart'; void main() { @@ -882,7 +881,7 @@ void main() { }); // Simulate painting a RenderBox as if 'debugPaintSizeEnabled == true' - Function(PaintingContext, Offset) debugPaint(RenderBox renderBox) { + DebugPaintCallback debugPaint(RenderBox renderBox) { layout(renderBox); pumpFrame(phase: EnginePhase.compositingBits); return (PaintingContext context, Offset offset) { @@ -892,7 +891,7 @@ void main() { } test('RenderClipPath.debugPaintSize draws a path and a debug text when clipBehavior is not Clip.none', () { - Function(PaintingContext, Offset) debugPaintClipRect(Clip clip) { + DebugPaintCallback debugPaintClipRect(Clip clip) { final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200)); final RenderClipPath renderClipPath = RenderClipPath(clipBehavior: clip, child: child); return debugPaint(renderClipPath); @@ -909,7 +908,7 @@ void main() { }); test('RenderClipRect.debugPaintSize draws a rect and a debug text when clipBehavior is not Clip.none', () { - Function(PaintingContext, Offset) debugPaintClipRect(Clip clip) { + DebugPaintCallback debugPaintClipRect(Clip clip) { final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200)); final RenderClipRect renderClipRect = RenderClipRect(clipBehavior: clip, child: child); return debugPaint(renderClipRect); @@ -925,7 +924,7 @@ void main() { }); test('RenderClipRRect.debugPaintSize draws a rounded rect and a debug text when clipBehavior is not Clip.none', () { - Function(PaintingContext, Offset) debugPaintClipRRect(Clip clip) { + DebugPaintCallback debugPaintClipRRect(Clip clip) { final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200)); final RenderClipRRect renderClipRRect = RenderClipRRect(clipBehavior: clip, child: child); return debugPaint(renderClipRRect); @@ -941,7 +940,7 @@ void main() { }); test('RenderClipOval.debugPaintSize draws a path and a debug text when clipBehavior is not Clip.none', () { - Function(PaintingContext, Offset) debugPaintClipOval(Clip clip) { + DebugPaintCallback debugPaintClipOval(Clip clip) { final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200)); final RenderClipOval renderClipOval = RenderClipOval(clipBehavior: clip, child: child); return debugPaint(renderClipOval); @@ -1086,3 +1085,5 @@ void expectAssertionError() { FlutterError.reportError(errorDetails); } } + +typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset); diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index 7e8d0e6c1dde7..6ebfc38f4de22 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:ui' show SemanticsUpdate; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -43,6 +44,44 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser void initInstances() { super.initInstances(); _instance = this; + // TODO(goderbauer): Create (fake) window if embedder doesn't provide an implicit view. + assert(platformDispatcher.implicitView != null); + _renderView = initRenderView(platformDispatcher.implicitView!); + } + + @override + RenderView get renderView => _renderView; + late RenderView _renderView; + + @override + PipelineOwner get pipelineOwner => rootPipelineOwner; + + /// Creates a [RenderView] object to be the root of the + /// [RenderObject] rendering tree, and initializes it so that it + /// will be rendered when the next frame is requested. + /// + /// Called automatically when the binding is created. + RenderView initRenderView(FlutterView view) { + final RenderView renderView = RenderView(view: view); + rootPipelineOwner.rootNode = renderView; + addRenderView(renderView); + renderView.prepareInitialFrame(); + return renderView; + } + + @override + PipelineOwner createRootPipelineOwner() { + return PipelineOwner( + onSemanticsOwnerCreated: () { + renderView.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { + renderView.updateSemantics(update); + }, + onSemanticsOwnerDisposed: () { + renderView.clearSemantics(); + }, + ); } /// Creates and initializes the binding. This function is @@ -139,23 +178,25 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError; FlutterError.onError = _errors.add; try { - pipelineOwner.flushLayout(); + rootPipelineOwner.flushLayout(); if (phase == EnginePhase.layout) { return; } - pipelineOwner.flushCompositingBits(); + rootPipelineOwner.flushCompositingBits(); if (phase == EnginePhase.compositingBits) { return; } - pipelineOwner.flushPaint(); + rootPipelineOwner.flushPaint(); if (phase == EnginePhase.paint) { return; } - renderView.compositeFrame(); + for (final RenderView renderView in renderViews) { + renderView.compositeFrame(); + } if (phase == EnginePhase.composite) { return; } - pipelineOwner.flushSemantics(); + rootPipelineOwner.flushSemantics(); if (phase == EnginePhase.flushSemantics) { return; } diff --git a/packages/flutter/test/rendering/sliver_utils.dart b/packages/flutter/test/rendering/sliver_utils.dart new file mode 100644 index 0000000000000..8fbe123d0959e --- /dev/null +++ b/packages/flutter/test/rendering/sliver_utils.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Test sliver which always attempts to paint itself whether it is visible or not. +// Use for checking if slivers which take sliver children paints optimally. +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class RenderMockSliverToBoxAdapter extends RenderSliverToBoxAdapter { + RenderMockSliverToBoxAdapter({ + super.child, + required this.incrementCounter, + }); + final void Function() incrementCounter; + + @override + void paint(PaintingContext context, Offset offset) { + incrementCounter(); + } +} + +class MockSliverToBoxAdapter extends SingleChildRenderObjectWidget { + /// Creates a sliver that contains a single box widget. + const MockSliverToBoxAdapter({ + super.key, + super.child, + required this.incrementCounter, + }); + + final void Function() incrementCounter; + + @override + RenderMockSliverToBoxAdapter createRenderObject(BuildContext context) => + RenderMockSliverToBoxAdapter(incrementCounter: incrementCounter); +} diff --git a/packages/flutter/test/rendering/table_test.dart b/packages/flutter/test/rendering/table_test.dart index 12ff18b0352a0..521e695ada9eb 100644 --- a/packages/flutter/test/rendering/table_test.dart +++ b/packages/flutter/test/rendering/table_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; import 'rendering_tester.dart'; RenderBox sizedBox(double width, double height) { @@ -277,4 +276,37 @@ void main() { ); }); + test('MaxColumnWidth.flex returns the correct result', () { + MaxColumnWidth columnWidth = const MaxColumnWidth( + FixedColumnWidth(100), // returns null from .flex + FlexColumnWidth(), // returns 1 from .flex + ); + final double? flexValue = columnWidth.flex(<RenderBox>[]); + expect(flexValue, 1.0); + + // Swap a and b, check for same result. + columnWidth = const MaxColumnWidth( + FlexColumnWidth(), // returns 1 from .flex + FixedColumnWidth(100), // returns null from .flex + ); + // Same result. + expect(columnWidth.flex(<RenderBox>[]), flexValue); + }); + + test('MinColumnWidth.flex returns the correct result', () { + MinColumnWidth columnWidth = const MinColumnWidth( + FixedColumnWidth(100), // returns null from .flex + FlexColumnWidth(), // returns 1 from .flex + ); + final double? flexValue = columnWidth.flex(<RenderBox>[]); + expect(flexValue, 1.0); + + // Swap a and b, check for same result. + columnWidth = const MinColumnWidth( + FlexColumnWidth(), // returns 1 from .flex + FixedColumnWidth(100), // returns null from .flex + ); + // Same result. + expect(columnWidth.flex(<RenderBox>[]), flexValue); + }); } diff --git a/packages/flutter/test/rendering/view_test.dart b/packages/flutter/test/rendering/view_test.dart index a3367a691fbef..9caed8c544ac5 100644 --- a/packages/flutter/test/rendering/view_test.dart +++ b/packages/flutter/test/rendering/view_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; import 'rendering_tester.dart'; void main() { @@ -122,6 +121,16 @@ void main() { isNot(paintsGreenRect), ); }); + + test('Config can be set and changed after instantiation without calling prepareInitialFrame first', () { + final RenderView view = RenderView( + view: RendererBinding.instance.platformDispatcher.views.single, + ); + view.configuration = const ViewConfiguration(size: Size(100, 200), devicePixelRatio: 3.0); + view.configuration = const ViewConfiguration(size: Size(200, 300), devicePixelRatio: 2.0); + PipelineOwner().rootNode = view; + view.prepareInitialFrame(); + }); } const Color orange = Color(0xFFFF9000); diff --git a/packages/flutter/test/rendering/viewport_test.dart b/packages/flutter/test/rendering/viewport_test.dart index e2e00012721c6..b4cf6ae489c94 100644 --- a/packages/flutter/test/rendering/viewport_test.dart +++ b/packages/flutter/test/rendering/viewport_test.dart @@ -1586,6 +1586,14 @@ void main() { final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset; expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 34.0 * 2); }); + + testWidgets('will not assert on mismatched axis', (WidgetTester tester) async { + await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: true)); + final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first; + + final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false)); + viewport.getOffsetToReveal(target, 0.0, axis: Axis.horizontal); + }); }); testWidgets('RenderViewportBase.showOnScreen reports the correct targetRect', (WidgetTester tester) async { diff --git a/packages/flutter/test/scheduler/benchmarks_test.dart b/packages/flutter/test/scheduler/benchmarks_test.dart index 9578ba74c4938..ec9c03b54a2f8 100644 --- a/packages/flutter/test/scheduler/benchmarks_test.dart +++ b/packages/flutter/test/scheduler/benchmarks_test.dart @@ -42,7 +42,7 @@ void main() { await benchmarkWidgets( (WidgetTester tester) async { const Key root = Key('root'); - binding.attachRootWidget(Container(key: root)); + binding.attachRootWidget(binding.wrapWithDefaultView(Container(key: root))); await tester.pump(); expect(binding.framesBegun, greaterThan(0)); diff --git a/packages/flutter/test/scheduler/binding_test.dart b/packages/flutter/test/scheduler/binding_test.dart index 233e12c94af03..5079db9266fed 100644 --- a/packages/flutter/test/scheduler/binding_test.dart +++ b/packages/flutter/test/scheduler/binding_test.dart @@ -28,4 +28,21 @@ void main() { ); timeDilation = 1.0; }); + + test('Adding a persistent frame callback during a persistent frame callback', () { + bool calledBack = false; + SchedulerBinding.instance.addPersistentFrameCallback((Duration timeStamp) { + if (!calledBack) { + SchedulerBinding.instance.addPersistentFrameCallback((Duration timeStamp) { + calledBack = true; + }); + } + }); + SchedulerBinding.instance.handleBeginFrame(null); + SchedulerBinding.instance.handleDrawFrame(); + expect(calledBack, false); + SchedulerBinding.instance.handleBeginFrame(null); + SchedulerBinding.instance.handleDrawFrame(); + expect(calledBack, true); + }); } diff --git a/packages/flutter/test/scheduler/ticker_test.dart b/packages/flutter/test/scheduler/ticker_test.dart index bbe9b7d9df3c3..d4eab13d6fb94 100644 --- a/packages/flutter/test/scheduler/ticker_test.dart +++ b/packages/flutter/test/scheduler/ticker_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { Future<void> setAppLifeCycleState(AppLifecycleState state) async { @@ -15,7 +16,7 @@ void main() { .handlePlatformMessage('flutter/lifecycle', message, (_) {}); } - testWidgets('Ticker mute control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker mute control test', (WidgetTester tester) async { int tickCount = 0; void handleTick(Duration duration) { tickCount += 1; @@ -97,7 +98,7 @@ void main() { expect(ticker.isActive, isFalse); }); - testWidgets('Ticker control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker control test', (WidgetTester tester) async { late Ticker ticker; void testFunction() { @@ -110,7 +111,7 @@ void main() { expect(ticker.toString(debugIncludeStack: true), contains('testFunction')); }); - testWidgets('Ticker can be sped up with time dilation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker can be sped up with time dilation', (WidgetTester tester) async { timeDilation = 0.5; // Move twice as fast. late Duration lastDuration; void handleTick(Duration duration) { @@ -128,7 +129,7 @@ void main() { timeDilation = 1.0; // restore time dilation, or it will affect other tests }); - testWidgets('Ticker can be slowed down with time dilation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker can be slowed down with time dilation', (WidgetTester tester) async { timeDilation = 2.0; // Move half as fast. late Duration lastDuration; void handleTick(Duration duration) { @@ -146,7 +147,7 @@ void main() { timeDilation = 1.0; // restore time dilation, or it will affect other tests }); - testWidgets('Ticker stops ticking when application is paused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker stops ticking when application is paused', (WidgetTester tester) async { int tickCount = 0; void handleTick(Duration duration) { tickCount += 1; @@ -169,7 +170,7 @@ void main() { setAppLifeCycleState(AppLifecycleState.resumed); }); - testWidgets('Ticker can be created before application unpauses', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticker can be created before application unpauses', (WidgetTester tester) async { setAppLifeCycleState(AppLifecycleState.paused); int tickCount = 0; diff --git a/packages/flutter/test/semantics/semantics_binding_test.dart b/packages/flutter/test/semantics/semantics_binding_test.dart index 29f256ef53f77..380713692c283 100644 --- a/packages/flutter/test/semantics/semantics_binding_test.dart +++ b/packages/flutter/test/semantics/semantics_binding_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Listeners are called when semantics are turned on with ensureSemantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Listeners are called when semantics are turned on with ensureSemantics', (WidgetTester tester) async { expect(SemanticsBinding.instance.semanticsEnabled, isFalse); final List<bool> status = <bool>[]; @@ -43,7 +44,7 @@ void main() { expect(SemanticsBinding.instance.semanticsEnabled, isFalse); }, semanticsEnabled: false); - testWidgets('Listeners are called when semantics are turned on by platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Listeners are called when semantics are turned on by platform', (WidgetTester tester) async { expect(SemanticsBinding.instance.semanticsEnabled, isFalse); final List<bool> status = <bool>[]; @@ -69,7 +70,7 @@ void main() { expect(SemanticsBinding.instance.semanticsEnabled, isFalse); }, semanticsEnabled: false); - testWidgets('SemanticsBinding.ensureSemantics triggers creation of semantics owner.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsBinding.ensureSemantics triggers creation of semantics owner.', (WidgetTester tester) async { expect(SemanticsBinding.instance.semanticsEnabled, isFalse); expect(tester.binding.pipelineOwner.semanticsOwner, isNull); diff --git a/packages/flutter/test/semantics/semantics_elevation_test.dart b/packages/flutter/test/semantics/semantics_elevation_test.dart index 24c6918af2bdb..149101db2a837 100644 --- a/packages/flutter/test/semantics/semantics_elevation_test.dart +++ b/packages/flutter/test/semantics/semantics_elevation_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('SemanticsNodes overlapping in z', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNodes overlapping in z', (WidgetTester tester) async { // Cards are semantic boundaries that always own their own SemanticNode, // PhysicalModels merge their semantics information into parent. // @@ -97,7 +98,7 @@ void main() { semantics.dispose(); }); - testWidgets('SemanticsNodes overlapping in z with switched children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNodes overlapping in z with switched children', (WidgetTester tester) async { // Same as 'SemanticsNodes overlapping in z', but the order of children // is reversed @@ -173,7 +174,7 @@ void main() { semantics.dispose(); }); - testWidgets('single node thickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('single node thickness', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const MaterialApp( @@ -193,7 +194,7 @@ void main() { semantics.dispose(); }); - testWidgets('force-merge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('force-merge', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -247,7 +248,7 @@ void main() { semantics.dispose(); }); - testWidgets('force-merge with inversed children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('force-merge with inversed children', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/semantics/semantics_owner_test.dart b/packages/flutter/test/semantics/semantics_owner_test.dart index 18c93f5eb62ed..c8f59f19df493 100644 --- a/packages/flutter/test/semantics/semantics_owner_test.dart +++ b/packages/flutter/test/semantics/semantics_owner_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Performing SemanticsAction.showOnScreen does not crash if node no longer exist', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Performing SemanticsAction.showOnScreen does not crash if node no longer exist', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/100358. final SemanticsTester semantics = SemanticsTester(tester); diff --git a/packages/flutter/test/semantics/semantics_update_test.dart b/packages/flutter/test/semantics/semantics_update_test.dart index e5a41228b13ed..42708c877974c 100644 --- a/packages/flutter/test/semantics/semantics_update_test.dart +++ b/packages/flutter/test/semantics/semantics_update_test.dart @@ -8,11 +8,12 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { SemanticsUpdateTestBinding(); - testWidgets('Semantics update does not send update for merged nodes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics update does not send update for merged nodes.', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); // Pumps a placeholder to trigger the warm up frame. await tester.pumpWidget( @@ -85,7 +86,7 @@ void main() { handle.dispose(); }); - testWidgets('Semantics update receives attributed text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics update receives attributed text', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); // Pumps a placeholder to trigger the warm up frame. await tester.pumpWidget( diff --git a/packages/flutter/test/semantics/traversal_order_test.dart b/packages/flutter/test/semantics/traversal_order_test.dart index 2b469bb1139a4..5bc8981463ad2 100644 --- a/packages/flutter/test/semantics/traversal_order_test.dart +++ b/packages/flutter/test/semantics/traversal_order_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('Traversal order handles touching elements', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal order handles touching elements', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/services/asset_bundle_test.dart b/packages/flutter/test/services/asset_bundle_test.dart index efa2f7d7cc006..7641adaf1a8a4 100644 --- a/packages/flutter/test/services/asset_bundle_test.dart +++ b/packages/flutter/test/services/asset_bundle_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestAssetBundle extends CachingAssetBundle { Map<String, int> loadCallCount = <String, int>{}; @@ -16,17 +17,32 @@ class TestAssetBundle extends CachingAssetBundle { Future<ByteData> load(String key) async { loadCallCount[key] = (loadCallCount[key] ?? 0) + 1; if (key == 'AssetManifest.json') { - return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer); + return ByteData.sublistView(utf8.encode('{"one": ["one"]}')); } if (key == 'AssetManifest.bin') { - return const StandardMessageCodec().encodeMessage(<String, Object>{ - 'one': <Object>[] - })!; + return const StandardMessageCodec() + .encodeMessage(<String, Object>{'one': <Object>[]})!; + } + + if (key == 'AssetManifest.bin.json') { + // Encode the manifest data that will be used by the app + final ByteData data = const StandardMessageCodec().encodeMessage(<String, Object> {'one': <Object>[]})!; + // Simulate the behavior of NetworkAssetBundle.load here, for web tests + return ByteData.sublistView( + utf8.encode( + json.encode( + base64.encode( + // Encode only the actual bytes of the buffer, and no more... + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes) + ) + ) + ) + ); } if (key == 'counter') { - return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(loadCallCount[key]!.toString())).buffer); + return ByteData.sublistView(utf8.encode(loadCallCount[key]!.toString())); } if (key == 'one') { @@ -135,6 +151,26 @@ void main() { expect(data, isA<SynchronousFuture<int>>()); expect(await data, 1); }); + + testWidgetsWithLeakTracking('loadStructuredData handles exceptions correctly', (WidgetTester tester) async { + final TestAssetBundle bundle = TestAssetBundle(); + try { + await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.error('what do they say?')); + fail('expected exception did not happen'); + } catch (e) { + expect(e.toString(), contains('what do they say?')); + } + }); + + testWidgetsWithLeakTracking('loadStructuredBinaryData handles exceptions correctly', (WidgetTester tester) async { + final TestAssetBundle bundle = TestAssetBundle(); + try { + await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.error('buy more crystals')); + fail('expected exception did not happen'); + } catch (e) { + expect(e.toString(), contains('buy more crystals')); + } + }); }); test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async { diff --git a/packages/flutter/test/services/asset_manifest_test.dart b/packages/flutter/test/services/asset_manifest_test.dart index c06ffd0126b64..108515f4330d0 100644 --- a/packages/flutter/test/services/asset_manifest_test.dart +++ b/packages/flutter/test/services/asset_manifest_test.dart @@ -2,34 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; class TestAssetBundle extends AssetBundle { + static const Map<String, List<Object>> _binManifestData = <String, List<Object>>{ + 'assets/foo.png': <Object>[ + <String, Object>{ + 'asset': 'assets/foo.png', + }, + <String, Object>{ + 'asset': 'assets/2x/foo.png', + 'dpr': 2.0 + }, + ], + 'assets/bar.png': <Object>[ + <String, Object>{ + 'asset': 'assets/bar.png', + }, + ], + }; + @override Future<ByteData> load(String key) async { if (key == 'AssetManifest.bin') { - final Map<String, List<Object>> binManifestData = <String, List<Object>>{ - 'assets/foo.png': <Object>[ - <String, Object>{ - 'asset': 'assets/foo.png', - }, - <String, Object>{ - 'asset': 'assets/2x/foo.png', - 'dpr': 2.0 - }, - ], - 'assets/bar.png': <Object>[ - <String, Object>{ - 'asset': 'assets/bar.png', - }, - ], - }; - - final ByteData data = const StandardMessageCodec().encodeMessage(binManifestData)!; + final ByteData data = const StandardMessageCodec().encodeMessage(_binManifestData)!; return data; } + if (key == 'AssetManifest.bin.json') { + // Encode the manifest data that will be used by the app + final ByteData data = const StandardMessageCodec().encodeMessage(_binManifestData)!; + // Simulate the behavior of NetworkAssetBundle.load here, for web tests + return ByteData.sublistView( + utf8.encode( + json.encode( + base64.encode( + // Encode only the actual bytes of the buffer, and no more... + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes) + ) + ) + ) + ); + } + throw ArgumentError('Unexpected key'); } diff --git a/packages/flutter/test/services/binding_test.dart b/packages/flutter/test/services/binding_test.dart index 1aff214478eba..3f3439e62abc2 100644 --- a/packages/flutter/test/services/binding_test.dart +++ b/packages/flutter/test/services/binding_test.dart @@ -89,7 +89,7 @@ void main() { int flutterAssetsCallCount = 0; binding.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', (ByteData? message) async { flutterAssetsCallCount += 1; - return Uint8List.fromList('test_asset_data'.codeUnits).buffer.asByteData(); + return ByteData.sublistView(utf8.encode('test_asset_data')); }); await rootBundle.loadString('test_asset'); diff --git a/packages/flutter/test/services/channel_buffers_test.dart b/packages/flutter/test/services/channel_buffers_test.dart index 09eeec03eaa2a..2c48f10cd3b2f 100644 --- a/packages/flutter/test/services/channel_buffers_test.dart +++ b/packages/flutter/test/services/channel_buffers_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; @@ -14,15 +13,11 @@ class TestChannelBuffersFlutterBinding extends BindingBase with SchedulerBinding void main() { ByteData makeByteData(String str) { - final List<int> list = utf8.encode(str); - final ByteBuffer buffer = list is Uint8List ? list.buffer : Uint8List.fromList(list).buffer; - return ByteData.view(buffer); + return ByteData.sublistView(utf8.encode(str)); } String getString(ByteData data) { - final ByteBuffer buffer = data.buffer; - final List<int> list = buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - return utf8.decode(list); + return utf8.decode(Uint8List.sublistView(data)); } test('does drain channel buffers', () async { diff --git a/packages/flutter/test/services/default_binary_messenger_test.dart b/packages/flutter/test/services/default_binary_messenger_test.dart index 690493d46acdf..72702f5208b58 100644 --- a/packages/flutter/test/services/default_binary_messenger_test.dart +++ b/packages/flutter/test/services/default_binary_messenger_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,10 +12,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); ByteData makeByteData(String str) { - final List<int> list = utf8.encode(str); - final ByteBuffer buffer = - list is Uint8List ? list.buffer : Uint8List.fromList(list).buffer; - return ByteData.view(buffer); + return ByteData.sublistView(utf8.encode(str)); } test('default binary messenger calls callback once', () async { diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 7d85e93733f32..e67589bff156a 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -471,39 +471,59 @@ class FakeIosPlatformViewsController { } } -class FakeHtmlPlatformViewsController { - FakeHtmlPlatformViewsController() { +class FakeMacosPlatformViewsController { + FakeMacosPlatformViewsController() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall); } - Iterable<FakeHtmlPlatformView> get views => _views.values; - final Map<int, FakeHtmlPlatformView> _views = <int, FakeHtmlPlatformView>{}; + Iterable<FakeAppKitView> get views => _views.values; + final Map<int, FakeAppKitView> _views = <int, FakeAppKitView>{}; final Set<String> _registeredViewTypes = <String>{}; - late Completer<void> resizeCompleter; + // When this completer is non null, the 'create' method channel call will be + // delayed until it completes. + Completer<void>? creationDelay; - Completer<void>? createCompleter; + // Maps a view id to the number of gestures it accepted so far. + final Map<int, int> gesturesAccepted = <int, int>{}; + + // Maps a view id to the number of gestures it rejected so far. + final Map<int, int> gesturesRejected = <int, int>{}; void registerViewType(String viewType) { _registeredViewTypes.add(viewType); } + void invokeViewFocused(int viewId) { + final MethodCodec codec = SystemChannels.platform_views.codec; + final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId)); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage(SystemChannels.platform_views.name, data, (ByteData? data) {}); + } + Future<dynamic> _onMethodCall(MethodCall call) { switch (call.method) { case 'create': return _create(call); case 'dispose': return _dispose(call); + case 'acceptGesture': + return _acceptGesture(call); + case 'rejectGesture': + return _rejectGesture(call); } return Future<dynamic>.sync(() => null); } Future<dynamic> _create(MethodCall call) async { + if (creationDelay != null) { + await creationDelay!.future; + } final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>; final int id = args['id'] as int; final String viewType = args['viewType'] as String; - final Object? params = args['params']; + final Uint8List? creationParams = args['params'] as Uint8List?; if (_views.containsKey(id)) { throw PlatformException( @@ -519,11 +539,23 @@ class FakeHtmlPlatformViewsController { ); } - if (createCompleter != null) { - await createCompleter!.future; - } + _views[id] = FakeAppKitView(id, viewType, creationParams); + gesturesAccepted[id] = 0; + gesturesRejected[id] = 0; + return Future<int?>.sync(() => null); + } + + Future<dynamic> _acceptGesture(MethodCall call) async { + final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>; + final int id = args['id'] as int; + gesturesAccepted[id] = gesturesAccepted[id]! + 1; + return Future<int?>.sync(() => null); + } - _views[id] = FakeHtmlPlatformView(id, viewType, params); + Future<dynamic> _rejectGesture(MethodCall call) async { + final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>; + final int id = args['id'] as int; + gesturesRejected[id] = gesturesRejected[id]! + 1; return Future<int?>.sync(() => null); } @@ -658,29 +690,29 @@ class FakeUiKitView { } @immutable -class FakeHtmlPlatformView { - const FakeHtmlPlatformView(this.id, this.type, [this.creationParams]); +class FakeAppKitView { + const FakeAppKitView(this.id, this.type, [this.creationParams]); final int id; final String type; - final Object? creationParams; + final Uint8List? creationParams; @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } - return other is FakeHtmlPlatformView + return other is FakeAppKitView && other.id == id && other.type == type && other.creationParams == creationParams; } @override - int get hashCode => Object.hash(id, type, creationParams); + int get hashCode => Object.hash(id, type); @override String toString() { - return 'FakeHtmlPlatformView(id: $id, type: $type, params: $creationParams)'; + return 'FakeAppKitView(id: $id, type: $type, creationParams: $creationParams)'; } } diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart index b9522292a7563..c2c48412263fc 100644 --- a/packages/flutter/test/services/hardware_keyboard_test.dart +++ b/packages/flutter/test/services/hardware_keyboard_test.dart @@ -8,9 +8,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async { await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock})); @@ -68,7 +69,7 @@ void main() { equals(<KeyboardLockMode>{})); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData()); - testWidgets('KeyboardManager synthesizes modifier keys in rawKeyData mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('KeyboardManager synthesizes modifier keys in rawKeyData mode', (WidgetTester tester) async { final List<KeyEvent> events = <KeyEvent>[]; HardwareKeyboard.instance.addHandler((KeyEvent event) { events.add(event); @@ -96,8 +97,9 @@ void main() { expect(events[1].synthesized, false); }); - testWidgets('Dispatch events to all handlers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dispatch events to all handlers', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<int> logs = <int>[]; await tester.pumpWidget( @@ -201,8 +203,9 @@ void main() { // _CastError on _hardwareKeyboard.lookUpLayout(key). The original scenario // that this is triggered on Android is unknown. Here we make up a scenario // where a ShiftLeft key down is dispatched but the modifier bit is not set. - testWidgets('Correctly convert down events that are synthesized released', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Correctly convert down events that are synthesized released', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<KeyEvent> events = <KeyEvent>[]; await tester.pumpWidget( @@ -244,8 +247,9 @@ void main() { KeyDataTransitMode.rawKeyData, })); - testWidgets('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<int> logs = <int>[]; await tester.pumpWidget( @@ -276,19 +280,22 @@ void main() { logs.clear(); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData()); - testWidgets('Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async { + final FocusNode keyboardListenerFocusNode = FocusNode(); + addTearDown(keyboardListenerFocusNode.dispose); + final FocusNode rawKeyboardListenerFocusNode = FocusNode(); + addTearDown(rawKeyboardListenerFocusNode.dispose); final List<String> logs = <String>[]; await tester.pumpWidget( RawKeyboardListener( - focusNode: FocusNode(), + focusNode: rawKeyboardListenerFocusNode, onKey: (RawKeyEvent event) { logs.add('${event.runtimeType}'); }, child: KeyboardListener( autofocus: true, - focusNode: focusNode, + focusNode: keyboardListenerFocusNode, child: Container(), onKeyEvent: (KeyEvent event) { logs.add('${event.runtimeType}'); @@ -331,7 +338,7 @@ void main() { // In that case, the key data should not be converted to any [KeyEvent]s, // but is only used so that *a* key data comes before the raw key message // and makes [KeyEventManager] infer [KeyDataTransitMode.keyDataThenRawKeyData]. - testWidgets('Empty keyData yields no event but triggers inference', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty keyData yields no event but triggers inference', (WidgetTester tester) async { final List<KeyEvent> events = <KeyEvent>[]; final List<RawKeyEvent> rawEvents = <RawKeyEvent>[]; tester.binding.keyboard.addHandler((KeyEvent event) { @@ -383,7 +390,7 @@ void main() { expect(rawEvents.length, 2); }); - testWidgets('Exceptions from keyMessageHandler are caught and reported', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Exceptions from keyMessageHandler are caught and reported', (WidgetTester tester) async { final KeyMessageHandler? oldKeyMessageHandler = tester.binding.keyEventManager.keyMessageHandler; addTearDown(() { tester.binding.keyEventManager.keyMessageHandler = oldKeyMessageHandler; @@ -426,7 +433,7 @@ void main() { expect(record, isNull); }); - testWidgets('Exceptions from HardwareKeyboard handlers are caught and reported', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Exceptions from HardwareKeyboard handlers are caught and reported', (WidgetTester tester) async { bool throwingCallback(KeyEvent event) { throw 1; } @@ -466,7 +473,7 @@ void main() { expect(record, isNull); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async { final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents; final DebugPrintCallback oldDebugPrint = debugPrint; final StringBuffer messages = StringBuffer(); diff --git a/packages/flutter/test/services/lifecycle_test.dart b/packages/flutter/test/services/lifecycle_test.dart index c275691b3cd0c..6737ce15fa1cf 100644 --- a/packages/flutter/test/services/lifecycle_test.dart +++ b/packages/flutter/test/services/lifecycle_test.dart @@ -6,9 +6,10 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('initialLifecycleState is used to init state paused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialLifecycleState is used to init state paused', (WidgetTester tester) async { expect(ServicesBinding.instance.lifecycleState, isNull); final TestWidgetsFlutterBinding binding = tester.binding; binding.resetLifecycleState(); @@ -20,7 +21,7 @@ void main() { // even though no lifecycle event was fired from the platform. expect(binding.lifecycleState.toString(), equals('AppLifecycleState.paused')); }); - testWidgets('Handles all of the allowed states of AppLifecycleState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Handles all of the allowed states of AppLifecycleState', (WidgetTester tester) async { final TestWidgetsFlutterBinding binding = tester.binding; for (final AppLifecycleState state in AppLifecycleState.values) { binding.resetLifecycleState(); diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart index ed52400bdeb51..f5c4b38b4acf5 100644 --- a/packages/flutter/test/services/raw_keyboard_test.dart +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _ModifierCheck { const _ModifierCheck(this.key, this.side); @@ -17,7 +18,7 @@ class _ModifierCheck { void main() { group('RawKeyboard', () { - testWidgets('The correct character is produced', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The correct character is produced', (WidgetTester tester) async { for (final String platform in <String>['linux', 'android', 'macos', 'fuchsia', 'windows']) { String character = ''; void handleKey(RawKeyEvent event) { @@ -32,7 +33,7 @@ void main() { } }); - testWidgets('No character is produced for non-printables', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No character is produced for non-printables', (WidgetTester tester) async { for (final String platform in <String>['linux', 'android', 'macos', 'fuchsia', 'windows', 'web']) { void handleKey(RawKeyEvent event) { expect(event.character, isNull, reason: 'on $platform'); @@ -43,7 +44,7 @@ void main() { } }); - testWidgets('keysPressed is maintained', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed is maintained', (WidgetTester tester) async { for (final String platform in <String>['linux', 'android', 'macos', 'fuchsia', 'windows', 'ios']) { RawKeyboard.instance.clearKeysPressed(); expect(RawKeyboard.instance.keysPressed, isEmpty, reason: 'on $platform'); @@ -149,7 +150,7 @@ void main() { } }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 - testWidgets('keysPressed is correct when modifier is released before key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed is correct when modifier is released before key', (WidgetTester tester) async { for (final String platform in <String>['linux', 'android', 'macos', 'fuchsia', 'windows', 'ios']) { RawKeyboard.instance.clearKeysPressed(); expect(RawKeyboard.instance.keysPressed, isEmpty, reason: 'on $platform'); @@ -200,7 +201,7 @@ void main() { } }, skip: isBrowser); // https://github.com/flutter/flutter/issues/76741 - testWidgets('keysPressed modifiers are synchronized with key events on macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -224,7 +225,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a macOS-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on iOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -248,7 +249,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is an iOS-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on Windows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on Windows', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -272,7 +273,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a Windows-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on android', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -296,7 +297,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is an Android-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on fuchsia', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on fuchsia', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -320,7 +321,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a Fuchsia-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on Linux GLFW', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on Linux GLFW', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -382,7 +383,7 @@ void main() { // // GTK has some weird behavior where the tested key event sequence will // result in a AltRight down event without Alt bitmask. - testWidgets('keysPressed modifiers are synchronized with key events on Linux GTK (down events)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on Linux GTK (down events)', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); await simulateGTKKeyEvent(true, 0x6c/*AltRight*/, 0xffea/*AltRight*/, 0x2000000); @@ -402,7 +403,7 @@ void main() { // Regression test for https://github.com/flutter/flutter/issues/114591 . // // On Linux, CapsLock can be remapped to a non-modifier key. - testWidgets('CapsLock should not be release when remapped on Linux', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CapsLock should not be release when remapped on Linux', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); await simulateGTKKeyEvent(true, 0x42/*CapsLock*/, 0xff08/*Backspace*/, 0x2000000); @@ -419,7 +420,7 @@ void main() { // Regression test for https://github.com/flutter/flutter/issues/114591 . // // On Web, CapsLock can be remapped to a non-modifier key. - testWidgets('CapsLock should not be release when remapped on Web', (WidgetTester _) async { + testWidgetsWithLeakTracking('CapsLock should not be release when remapped on Web', (WidgetTester _) async { final List<RawKeyEvent> events = <RawKeyEvent>[]; RawKeyboard.instance.addListener(events.add); addTearDown(() { @@ -449,7 +450,7 @@ void main() { ); }, skip: !isBrowser); // [intended] This is a Browser-specific test. - testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. Change the modifiers so // that they show the shift key as already down when this event is @@ -537,7 +538,7 @@ void main() { ); }); - testWidgets('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -574,7 +575,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is an Android-specific test. - testWidgets('sided modifiers without a side set return all sides on macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return all sides on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -611,7 +612,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a macOS-specific test. - testWidgets('sided modifiers without a side set return all sides on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return all sides on iOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -648,7 +649,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is an iOS-specific test. - testWidgets('repeat events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('repeat events', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); late RawKeyEvent receivedEvent; RawKeyboard.instance.keyEventHandler = (RawKeyEvent event) { @@ -691,7 +692,7 @@ void main() { RawKeyboard.instance.keyEventHandler = null; }, skip: isBrowser); // [intended] This is a Windows-specific test. - testWidgets('sided modifiers without a side set return all sides on Windows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return all sides on Windows', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -726,7 +727,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a Windows-specific test. - testWidgets('sided modifiers without a side set return all sides on Linux GLFW', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return all sides on Linux GLFW', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -764,7 +765,7 @@ void main() { ); }, skip: isBrowser); // [intended] This is a GLFW-specific test. - testWidgets('sided modifiers without a side set return left sides on web', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sided modifiers without a side set return left sides on web', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -797,7 +798,7 @@ void main() { ); }); - testWidgets('RawKeyboard asserts if no keys are in keysPressed after receiving a key down event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawKeyboard asserts if no keys are in keysPressed after receiving a key down event', (WidgetTester tester) async { final Map<String, dynamic> keyEventMessage; if (kIsWeb) { keyEventMessage = const <String, dynamic>{ @@ -833,7 +834,7 @@ void main() { ); }); - testWidgets('Allows inconsistent modifier for iOS', (WidgetTester _) async { + testWidgetsWithLeakTracking('Allows inconsistent modifier for iOS', (WidgetTester _) async { // Use `testWidgets` for clean-ups. final List<RawKeyEvent> events = <RawKeyEvent>[]; RawKeyboard.instance.addListener(events.add); @@ -861,7 +862,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, contains(LogicalKeyboardKey.capsLock)); }, skip: isBrowser); // [intended] This is an iOS-specific group. - testWidgets('Allows inconsistent modifier for Android', (WidgetTester _) async { + testWidgetsWithLeakTracking('Allows inconsistent modifier for Android', (WidgetTester _) async { // Use `testWidgets` for clean-ups. final List<RawKeyEvent> events = <RawKeyEvent>[]; RawKeyboard.instance.addListener(events.add); @@ -892,7 +893,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, contains(LogicalKeyboardKey.capsLock)); }, skip: isBrowser); // [intended] This is an Android-specific group. - testWidgets('Allows inconsistent modifier for Web - Alt graph', (WidgetTester _) async { + testWidgetsWithLeakTracking('Allows inconsistent modifier for Web - Alt graph', (WidgetTester _) async { // Regression test for https://github.com/flutter/flutter/issues/113836 final List<RawKeyEvent> events = <RawKeyEvent>[]; RawKeyboard.instance.addListener(events.add); @@ -921,7 +922,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, contains(LogicalKeyboardKey.altGraph)); }, skip: !isBrowser); // [intended] This is a Browser-specific test. - testWidgets('Allows inconsistent modifier for Web - Alt right', (WidgetTester _) async { + testWidgetsWithLeakTracking('Allows inconsistent modifier for Web - Alt right', (WidgetTester _) async { // Regression test for https://github.com/flutter/flutter/issues/113836 final List<RawKeyEvent> events = <RawKeyEvent>[]; RawKeyboard.instance.addListener(events.add); @@ -950,8 +951,9 @@ void main() { expect(RawKeyboard.instance.keysPressed, contains(LogicalKeyboardKey.altRight)); }, skip: !isBrowser); // [intended] This is a Browser-specific test. - testWidgets('Dispatch events to all handlers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dispatch events to all handlers', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<int> logs = <int>[]; await tester.pumpWidget( @@ -1014,7 +1016,7 @@ void main() { logs.clear(); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Exceptions from RawKeyboard listeners are caught and reported', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Exceptions from RawKeyboard listeners are caught and reported', (WidgetTester tester) async { void throwingListener(RawKeyEvent event) { throw 1; } @@ -1288,7 +1290,7 @@ void main() { expect(data.repeatCount, equals(42)); }); - testWidgets('Key events are responded to correctly.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Key events are responded to correctly.', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. final Map<String, dynamic> data = KeyEventSimulator.getKeyData( @@ -1308,6 +1310,7 @@ void main() { // Set up a widget that will receive focused text events. final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( Focus( focusNode: focusNode, @@ -2098,7 +2101,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft)); }); - testWidgets('Win32 VK_PROCESSKEY events are skipped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Win32 VK_PROCESSKEY events are skipped', (WidgetTester tester) async { const String platform = 'windows'; bool lastHandled = true; final List<RawKeyEvent> events = <RawKeyEvent>[]; @@ -2111,6 +2114,7 @@ void main() { return KeyEventResult.ignored; }, ); + addTearDown(node.dispose); await tester.pumpWidget(RawKeyboardListener( focusNode: node, child: Container(), diff --git a/packages/flutter/test/services/restoration_test.dart b/packages/flutter/test/services/restoration_test.dart index 8bfc04d121abd..ce52f5f342bcb 100644 --- a/packages/flutter/test/services/restoration_test.dart +++ b/packages/flutter/test/services/restoration_test.dart @@ -8,12 +8,20 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'restoration.dart'; void main() { + testWidgetsWithLeakTracking('$RestorationManager dispatches memory events', (WidgetTester tester) async { + await expectLater( + await memoryEvents(() => RestorationManager().dispose(), RestorationManager), + areCreateAndDispose, + ); + }); + group('RestorationManager', () { - testWidgets('root bucket retrieval', (WidgetTester tester) async { + testWidgetsWithLeakTracking('root bucket retrieval', (WidgetTester tester) async { final List<MethodCall> callsToEngine = <MethodCall>[]; final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { @@ -22,6 +30,7 @@ void main() { }); final RestorationManager manager = RestorationManager(); + addTearDown(manager.dispose); final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket; RestorationBucket? rootBucket; rootBucketFuture.then((RestorationBucket? bucket) { @@ -59,7 +68,7 @@ void main() { expect(synchronousBucket, same(rootBucket)); }); - testWidgets('root bucket received from engine before retrieval', (WidgetTester tester) async { + testWidgetsWithLeakTracking('root bucket received from engine before retrieval', (WidgetTester tester) async { SystemChannels.restoration.setMethodCallHandler(null); final List<MethodCall> callsToEngine = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) async { @@ -67,6 +76,7 @@ void main() { return null; }); final RestorationManager manager = RestorationManager(); + addTearDown(manager.dispose); await _pushDataFromEngine(_createEncodedRestorationData1()); @@ -78,7 +88,7 @@ void main() { expect(callsToEngine, isEmpty); }); - testWidgets('root bucket received while engine retrieval is pending', (WidgetTester tester) async { + testWidgetsWithLeakTracking('root bucket received while engine retrieval is pending', (WidgetTester tester) async { SystemChannels.restoration.setMethodCallHandler(null); final List<MethodCall> callsToEngine = <MethodCall>[]; final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); @@ -87,6 +97,7 @@ void main() { return result.future; }); final RestorationManager manager = RestorationManager(); + addTearDown(manager.dispose); RestorationBucket? rootBucket; manager.rootBucket.then((RestorationBucket? bucket) => rootBucket = bucket); @@ -108,11 +119,12 @@ void main() { expect(rootBucket2!.contains('foo'), isFalse); }); - testWidgets('root bucket is properly replaced when new data is available', (WidgetTester tester) async { + testWidgetsWithLeakTracking('root bucket is properly replaced when new data is available', (WidgetTester tester) async { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) async { return _createEncodedRestorationData1(); }); final RestorationManager manager = RestorationManager(); + addTearDown(manager.dispose); RestorationBucket? rootBucket; manager.rootBucket.then((RestorationBucket? bucket) { rootBucket = bucket; @@ -148,7 +160,7 @@ void main() { expect(newChild.read<String>('bar'), 'Hello'); }); - testWidgets('returns null as root bucket when restoration is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns null as root bucket when restoration is disabled', (WidgetTester tester) async { final List<MethodCall> callsToEngine = <MethodCall>[]; final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { @@ -159,6 +171,7 @@ void main() { final RestorationManager manager = RestorationManager()..addListener(() { listenerCount++; }); + addTearDown(manager.dispose); RestorationBucket? rootBucket; bool rootBucketResolved = false; manager.rootBucket.then((RestorationBucket? bucket) { @@ -191,7 +204,7 @@ void main() { expect(rootBucket, isNull); }); - testWidgets('flushData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('flushData', (WidgetTester tester) async { final List<MethodCall> callsToEngine = <MethodCall>[]; final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { @@ -200,6 +213,7 @@ void main() { }); final RestorationManager manager = RestorationManager(); + addTearDown(manager.dispose); final Future<RestorationBucket?> rootBucketFuture = manager.rootBucket; RestorationBucket? rootBucket; rootBucketFuture.then((RestorationBucket? bucket) { @@ -227,13 +241,14 @@ void main() { expect(callsToEngine, hasLength(1)); }); - testWidgets('isReplacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isReplacing', (WidgetTester tester) async { final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.restoration, (MethodCall call) { return result.future; }); final TestRestorationManager manager = TestRestorationManager(); + addTearDown(manager.dispose); expect(manager.isReplacing, isFalse); RestorationBucket? rootBucket; diff --git a/packages/flutter/test/services/system_chrome_test.dart b/packages/flutter/test/services/system_chrome_test.dart index 555d9e219797d..c4aa7fe3373ed 100644 --- a/packages/flutter/test/services/system_chrome_test.dart +++ b/packages/flutter/test/services/system_chrome_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('SystemChrome overlay style test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SystemChrome overlay style test', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { diff --git a/packages/flutter/test/widgets/absorb_pointer_test.dart b/packages/flutter/test/widgets/absorb_pointer_test.dart index 6c2a88f58ca4a..afe8198a29ab2 100644 --- a/packages/flutter/test/widgets/absorb_pointer_test.dart +++ b/packages/flutter/test/widgets/absorb_pointer_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('AbsorbPointers do not block siblings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AbsorbPointers do not block siblings', (WidgetTester tester) async { bool tapped = false; await tester.pumpWidget( Column( @@ -29,7 +30,7 @@ void main() { }); group('AbsorbPointer semantics', () { - testWidgets('does not change semantics when not absorbing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not change semantics when not absorbing', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -56,7 +57,7 @@ void main() { ); }); - testWidgets('drops semantics when its ignoreSemantics is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drops semantics when its ignoreSemantics is true', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final UniqueKey key = UniqueKey(); await tester.pumpWidget( @@ -75,7 +76,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignores user interactions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores user interactions', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart index d428c994d04aa..b0ee37f40828c 100644 --- a/packages/flutter/test/widgets/actions_test.dart +++ b/packages/flutter/test/widgets/actions_test.dart @@ -8,10 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group(ActionDispatcher, () { - testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async { await tester.pumpWidget(Container()); bool invoked = false; const ActionDispatcher dispatcher = ActionDispatcher(); @@ -48,7 +49,7 @@ void main() { setUp(clear); - testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; @@ -75,7 +76,7 @@ void main() { expect(invoked, isTrue); }); - testWidgets('Actions widget can invoke actions with default dispatcher and maybeInvoke', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions widget can invoke actions with default dispatcher and maybeInvoke', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; @@ -102,7 +103,7 @@ void main() { expect(invoked, isTrue); }); - testWidgets('maybeInvoke returns null when no action is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('maybeInvoke returns null when no action is found', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; @@ -129,7 +130,7 @@ void main() { expect(invoked, isFalse); }); - testWidgets('invoke throws when no action is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('invoke throws when no action is found', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; @@ -156,7 +157,7 @@ void main() { expect(invoked, isFalse); }); - testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; const TestIntent intent = TestIntent(); @@ -187,7 +188,7 @@ void main() { expect(invokedIntent, equals(intent)); }); - testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; const TestIntent intent = TestIntent(); @@ -224,7 +225,7 @@ void main() { expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); }); - testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; const TestIntent intent = TestIntent(); @@ -260,7 +261,7 @@ void main() { expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); }); - testWidgets('Actions widget can be found with of', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions widget can be found with of', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect); @@ -277,7 +278,7 @@ void main() { expect(dispatcher, equals(testDispatcher)); }); - testWidgets('Action can be found with find', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Action can be found with find', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect); bool invoked = false; @@ -324,7 +325,7 @@ void main() { expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull); }); - testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final GlobalKey containerKey = GlobalKey(); bool invoked = false; @@ -339,6 +340,8 @@ void main() { bool hovering = false; bool focusing = false; + addTearDown(focusNode.dispose); + Future<void> buildTest(bool enabled) async { await tester.pumpWidget( Center( @@ -394,7 +397,7 @@ void main() { expect(focusing, isFalse); }); - testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( MouseRegion( cursor: SystemMouseCursors.forbidden, @@ -427,7 +430,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); }); - testWidgets('Actions.invoke returns the value of Action.invoke', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions.invoke returns the value of Action.invoke', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final Object sentinel = Object(); bool invoked = false; @@ -458,7 +461,7 @@ void main() { expect(invoked, isTrue); }); - testWidgets('ContextAction can return null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ContextAction can return null', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); const TestIntent intent = TestIntent(); final TestContextAction testAction = TestContextAction(); @@ -485,7 +488,7 @@ void main() { expect(testAction.capturedContexts.single, containerKey.currentContext); }); - testWidgets('Disabled actions stop propagation to an ancestor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disabled actions stop propagation to an ancestor', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; const TestIntent intent = TestIntent(); @@ -534,7 +537,7 @@ void main() { }); group('Listening', () { - testWidgets('can listen to enabled state of Actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can listen to enabled state of Actions', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked1 = false; bool invoked2 = false; @@ -756,7 +759,11 @@ void main() { ); }); - testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { + tearDown(() async { + focusNode.dispose(); + }); + + testWidgetsWithLeakTracking('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final GlobalKey containerKey = GlobalKey(); @@ -790,7 +797,7 @@ void main() { expect(focusing, isFalse); }); - testWidgets('FocusableActionDetector shows focus highlight appropriately when focused and disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusableActionDetector shows focus highlight appropriately when focused and disabled', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final GlobalKey containerKey = GlobalKey(); @@ -821,7 +828,7 @@ void main() { expect(focusing, isTrue); }); - testWidgets('FocusableActionDetector can be used without callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusableActionDetector can be used without callbacks', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final GlobalKey containerKey = GlobalKey(); @@ -855,11 +862,13 @@ void main() { expect(focusing, isFalse); }); - testWidgets( + testWidgetsWithLeakTracking( 'FocusableActionDetector can prevent its descendants from being focusable', (WidgetTester tester) async { final FocusNode buttonNode = FocusNode(debugLabel: 'Test'); + addTearDown(buttonNode.dispose); + await tester.pumpWidget( MaterialApp( home: FocusableActionDetector( @@ -899,15 +908,23 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'FocusableActionDetector can prevent its descendants from being traversable', (WidgetTester tester) async { final FocusNode buttonNode1 = FocusNode(debugLabel: 'Button Node 1'); final FocusNode buttonNode2 = FocusNode(debugLabel: 'Button Node 2'); + final FocusNode skipTraversalNode = FocusNode(skipTraversal: true); + + addTearDown(() { + buttonNode1.dispose(); + buttonNode2.dispose(); + skipTraversalNode.dispose(); + }); await tester.pumpWidget( MaterialApp( home: FocusableActionDetector( + focusNode: skipTraversalNode, child: Column( children: <Widget>[ ElevatedButton( @@ -938,6 +955,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: FocusableActionDetector( + focusNode: skipTraversalNode, descendantsAreTraversable: false, child: Column( children: <Widget>[ @@ -968,7 +986,7 @@ void main() { }, ); - testWidgets('FocusableActionDetector can exclude Focus semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusableActionDetector can exclude Focus semantics', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: FocusableActionDetector( @@ -1074,7 +1092,7 @@ void main() { }); group('Action subclasses', () { - testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async { late Intent passedIntent; final TestAction action = TestAction(onInvoke: (Intent intent) { passedIntent = intent; @@ -1085,7 +1103,7 @@ void main() { expect(passedIntent, equals(intent)); }); - testWidgets('VoidCallbackAction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('VoidCallbackAction', (WidgetTester tester) async { bool called = false; void testCallback() { called = true; @@ -1095,7 +1113,7 @@ void main() { action.invoke(intent); expect(called, isTrue); }); - testWidgets('Base Action class default toKeyEventResult delegates to consumesKey', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Base Action class default toKeyEventResult delegates to consumesKey', (WidgetTester tester) async { expect( DefaultToKeyEventResultAction(consumesKey: false).toKeyEventResult(const DefaultToKeyEventResultIntent(), null), KeyEventResult.skipRemainingHandlers, @@ -1108,7 +1126,7 @@ void main() { }); group('Diagnostics', () { - testWidgets('default Intent debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default Intent debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); // ignore: invalid_use_of_protected_member @@ -1124,7 +1142,7 @@ void main() { expect(description, isEmpty); }); - testWidgets('default Actions debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default Actions debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Actions( @@ -1150,7 +1168,7 @@ void main() { ); }); - testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Actions( @@ -1189,7 +1207,7 @@ void main() { invokingContext = null; }); - testWidgets('Basic usage', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Basic usage', (WidgetTester tester) async { late BuildContext invokingContext2; late BuildContext invokingContext3; await tester.pumpWidget( @@ -1254,7 +1272,7 @@ void main() { expect(invocations, <String>['action1.invoke']); }); - testWidgets('Does not break after use', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not break after use', (WidgetTester tester) async { late BuildContext invokingContext2; late BuildContext invokingContext3; await tester.pumpWidget( @@ -1321,7 +1339,7 @@ void main() { ]); }); - testWidgets('Does not override if not overridable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not override if not overridable', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { @@ -1364,7 +1382,7 @@ void main() { ]); }); - testWidgets('The final override controls isEnabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The final override controls isEnabled', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { @@ -1451,7 +1469,7 @@ void main() { expect(invocations, <String>[]); }); - testWidgets('The override can choose to defer isActionEnabled to the overridable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The override can choose to defer isActionEnabled to the overridable', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { @@ -1541,7 +1559,7 @@ void main() { ]); }); - testWidgets('Throws on infinite recursions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws on infinite recursions', (WidgetTester tester) async { late StateSetter setState; BuildContext? action2LookupContext; await tester.pumpWidget( @@ -1600,7 +1618,7 @@ void main() { expect(exception?.toString(), contains('debugAssertIsEnabledMutuallyRecursive')); }); - testWidgets('Throws on invoking invalid override', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws on invoking invalid override', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context) { @@ -1638,7 +1656,7 @@ void main() { ); }); - testWidgets('Make an overridable action overridable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Make an overridable action overridable', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { @@ -1694,7 +1712,7 @@ void main() { ]); }); - testWidgets('Overriding Actions can change the intent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overriding Actions can change the intent', (WidgetTester tester) async { final List<String> newLogChannel = <String>[]; await tester.pumpWidget( Builder( @@ -1744,7 +1762,7 @@ void main() { ]); }); - testWidgets('Override non-context overridable Actions with a ContextAction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override non-context overridable Actions with a ContextAction', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { @@ -1797,7 +1815,7 @@ void main() { expect(LogInvocationContextAction.invokeContext, invokingContext); }); - testWidgets('Override a ContextAction with a regular Action', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Override a ContextAction with a regular Action', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context1) { diff --git a/packages/flutter/test/widgets/align_test.dart b/packages/flutter/test/widgets/align_test.dart index aa44e27bce1aa..1a6e89f73f176 100644 --- a/packages/flutter/test/widgets/align_test.dart +++ b/packages/flutter/test/widgets/align_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Align smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Align smoke test', (WidgetTester tester) async { await tester.pumpWidget( Align( alignment: const Alignment(0.50, 0.50), @@ -38,7 +39,7 @@ void main() { ); }); - testWidgets('Align control test (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Align control test (LTR)', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, child: Align( @@ -62,7 +63,7 @@ void main() { expect(tester.getBottomRight(find.byType(SizedBox)).dx, 100.0); }); - testWidgets('Align control test (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Align control test (RTL)', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.rtl, child: Align( @@ -86,7 +87,7 @@ void main() { expect(tester.getBottomRight(find.byType(SizedBox)).dx, 100.0); }); - testWidgets('Shrink wraps in finite space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shrink wraps in finite space', (WidgetTester tester) async { final GlobalKey alignKey = GlobalKey(); await tester.pumpWidget( SingleChildScrollView( @@ -105,7 +106,7 @@ void main() { expect(size.height, equals(10.0)); }); - testWidgets('Align widthFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Align widthFactor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -128,7 +129,7 @@ void main() { expect(box.size.width, equals(50.0)); }); - testWidgets('Align heightFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Align heightFactor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/animated_align_test.dart b/packages/flutter/test/widgets/animated_align_test.dart index 032f2678654fb..0d0331505abdc 100644 --- a/packages/flutter/test/widgets/animated_align_test.dart +++ b/packages/flutter/test/widgets/animated_align_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedAlign.debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign.debugFillProperties', (WidgetTester tester) async { const AnimatedAlign box = AnimatedAlign( alignment: Alignment.topCenter, curve: Curves.ease, @@ -15,7 +16,7 @@ void main() { expect(box, hasOneLineDescription); }); - testWidgets('AnimatedAlign alignment visual-to-directional animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign alignment visual-to-directional animation', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( @@ -57,7 +58,7 @@ void main() { expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 400.0)); }); - testWidgets('AnimatedAlign widthFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign widthFactor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -82,7 +83,7 @@ void main() { expect(box.size.width, equals(50.0)); }); - testWidgets('AnimatedAlign heightFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign heightFactor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -106,7 +107,7 @@ void main() { expect(box.size.height, equals( 50.0)); }); - testWidgets('AnimatedAlign null height factor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign null height factor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -130,7 +131,7 @@ void main() { expect(box.size, equals(const Size(100.0, 100))); }); - testWidgets('AnimatedAlign null widthFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign null widthFactor', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/animated_container_test.dart b/packages/flutter/test/widgets/animated_container_test.dart index 6d40f450cd5c9..d25156a595762 100644 --- a/packages/flutter/test/widgets/animated_container_test.dart +++ b/packages/flutter/test/widgets/animated_container_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedContainer.debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer.debugFillProperties', (WidgetTester tester) async { final AnimatedContainer container = AnimatedContainer( constraints: const BoxConstraints.tightFor(width: 17.0, height: 23.0), decoration: const BoxDecoration(color: Color(0xFF00FF00)), @@ -24,7 +25,7 @@ void main() { expect(container, hasOneLineDescription); }); - testWidgets('AnimatedContainer control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer control test', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); const BoxDecoration decorationA = BoxDecoration( @@ -102,7 +103,7 @@ void main() { ); }); - testWidgets('AnimatedContainer overanimate test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer overanimate test', (WidgetTester tester) async { await tester.pumpWidget( AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -139,7 +140,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('AnimatedContainer padding visual-to-directional animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer padding visual-to-directional animation', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( @@ -181,7 +182,7 @@ void main() { expect(tester.getTopRight(find.byKey(target)), const Offset(700.0, 0.0)); }); - testWidgets('AnimatedContainer alignment visual-to-directional animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer alignment visual-to-directional animation', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( @@ -223,7 +224,7 @@ void main() { expect(tester.getTopRight(find.byKey(target)), const Offset(800.0, 400.0)); }); - testWidgets('Animation rerun', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animation rerun', (WidgetTester tester) async { await tester.pumpWidget( Center( child: AnimatedContainer( @@ -291,7 +292,7 @@ void main() { expect(text.size.height, equals(100.0)); }); - testWidgets('AnimatedContainer sets transformAlignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer sets transformAlignment', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( @@ -339,7 +340,7 @@ void main() { expect(tester.getTopLeft(find.byKey(target)), const Offset(400.0, 300.0)); }); - testWidgets('AnimatedContainer sets clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer sets clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( AnimatedContainer( decoration: const BoxDecoration( diff --git a/packages/flutter/test/widgets/animated_cross_fade_test.dart b/packages/flutter/test/widgets/animated_cross_fade_test.dart index 7a4ac09b49bd8..ee3f88b132b32 100644 --- a/packages/flutter/test/widgets/animated_cross_fade_test.dart +++ b/packages/flutter/test/widgets/animated_cross_fade_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedCrossFade test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade test', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -61,7 +62,7 @@ void main() { expect(box.size.height, equals(150.0)); }); - testWidgets('AnimatedCrossFade test showSecond', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade test showSecond', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -88,7 +89,7 @@ void main() { expect(box.size.height, equals(200.0)); }); - testWidgets('AnimatedCrossFade alignment (VISUAL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade alignment (VISUAL)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); @@ -146,7 +147,7 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); }); - testWidgets('AnimatedCrossFade alignment (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade alignment (LTR)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); @@ -204,7 +205,7 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); }); - testWidgets('AnimatedCrossFade alignment (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade alignment (RTL)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); @@ -274,7 +275,7 @@ void main() { ); } - testWidgets('AnimatedCrossFade preserves widget state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade preserves widget state', (WidgetTester tester) async { await tester.pumpWidget(crossFadeWithWatcher()); _TickerWatchingWidgetState findState() => tester.state(find.byType(_TickerWatchingWidget)); @@ -287,7 +288,7 @@ void main() { } }); - testWidgets('AnimatedCrossFade switches off TickerMode and semantics on faded out widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade switches off TickerMode and semantics on faded out widget', (WidgetTester tester) async { ExcludeSemantics findSemantics() { return tester.widget(find.descendant( of: find.byKey(const ValueKey<CrossFadeState>(CrossFadeState.showFirst)), @@ -317,7 +318,7 @@ void main() { expect(findSemantics().excluding, true); }); - testWidgets('AnimatedCrossFade.layoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade.layoutBuilder', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -361,7 +362,7 @@ void main() { expect(find.text('AAA'), findsNothing); }); - testWidgets('AnimatedCrossFade test focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade test focus', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -385,7 +386,7 @@ void main() { expect(hiddenNode.hasPrimaryFocus, isFalse); }); - testWidgets('AnimatedCrossFade bottom child can have focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedCrossFade bottom child can have focus', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -410,7 +411,7 @@ void main() { expect(hiddenNode.hasPrimaryFocus, isTrue); }); - testWidgets('AnimatedCrossFade second child do not receive touch events', + testWidgetsWithLeakTracking('AnimatedCrossFade second child do not receive touch events', (WidgetTester tester) async { int numberOfTouchEventNoticed = 0; diff --git a/packages/flutter/test/widgets/animated_grid_test.dart b/packages/flutter/test/widgets/animated_grid_test.dart index dcec143a9e705..c03c7143febd1 100644 --- a/packages/flutter/test/widgets/animated_grid_test.dart +++ b/packages/flutter/test/widgets/animated_grid_test.dart @@ -5,10 +5,11 @@ import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('SliverAnimatedGrid.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAnimatedGrid.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -50,7 +51,7 @@ void main() { expect(finderCalled, true); }); - testWidgets('AnimatedGrid', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedGrid', (WidgetTester tester) async { Widget builder(BuildContext context, int index, Animation<double> animation) { return SizedBox( height: 100.0, @@ -132,7 +133,7 @@ void main() { }); group('SliverAnimatedGrid', () { - testWidgets('initialItemCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialItemCount', (WidgetTester tester) async { final Map<int, Animation<double>> animations = <int, Animation<double>>{}; await tester.pumpWidget( @@ -170,7 +171,7 @@ void main() { expect(animations[1]!.value, 1.0); }); - testWidgets('insert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('insert', (WidgetTester tester) async { final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); await tester.pumpWidget( @@ -250,7 +251,7 @@ void main() { expect(itemRight(2), 300.0); }); - testWidgets('insertAll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('insertAll', (WidgetTester tester) async { final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); await tester.pumpWidget( @@ -306,7 +307,7 @@ void main() { expect(itemRight(1), 200.0); }); - testWidgets('remove', (WidgetTester tester) async { + testWidgetsWithLeakTracking('remove', (WidgetTester tester) async { final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); final List<int> items = <int>[0, 1, 2]; @@ -384,7 +385,7 @@ void main() { expect(itemRight(2), 200.0); }); - testWidgets('removeAll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('removeAll', (WidgetTester tester) async { final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); final List<int> items = <int>[0, 1, 2]; @@ -436,7 +437,7 @@ void main() { expect(find.text('item 2'), findsNothing); }); - testWidgets('works in combination with other slivers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works in combination with other slivers', (WidgetTester tester) async { final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); await tester.pumpWidget( @@ -505,7 +506,7 @@ void main() { expect(tester.getTopLeft(find.text('item 0')).dx, 0); }); - testWidgets('passes correctly derived index of findChildIndexCallback to the inner SliverChildBuilderDelegate', + testWidgetsWithLeakTracking('passes correctly derived index of findChildIndexCallback to the inner SliverChildBuilderDelegate', (WidgetTester tester) async { final List<int> items = <int>[0, 1, 2, 3]; final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>(); @@ -573,7 +574,7 @@ void main() { }); }); - testWidgets( + testWidgetsWithLeakTracking( 'AnimatedGrid.of() and maybeOf called with a context that does not contain AnimatedGrid', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -618,7 +619,7 @@ void main() { }, ); - testWidgets('AnimatedGrid.clipBehavior is forwarded to its inner CustomScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedGrid.clipBehavior is forwarded to its inner CustomScrollView', (WidgetTester tester) async { const Clip clipBehavior = Clip.none; await tester.pumpWidget( @@ -647,7 +648,7 @@ void main() { expect(tester.widget<CustomScrollView>(find.byType(CustomScrollView)).clipBehavior, clipBehavior); }); - testWidgets('AnimatedGrid applies MediaQuery padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedGrid applies MediaQuery padding', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); EdgeInsets? innerMediaQueryPadding; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/animated_image_filtered_repaint_test.dart b/packages/flutter/test/widgets/animated_image_filtered_repaint_test.dart index 0ccd25107435e..75de31aea77d4 100644 --- a/packages/flutter/test/widgets/animated_image_filtered_repaint_test.dart +++ b/packages/flutter/test/widgets/animated_image_filtered_repaint_test.dart @@ -7,9 +7,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ImageFiltered avoids repainting child as it animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageFiltered avoids repainting child as it animates', (WidgetTester tester) async { RenderTestObject.paintCount = 0; await tester.pumpWidget( ColoredBox( diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart index 6e585c9fcd2dc..d8d637bb43083 100644 --- a/packages/flutter/test/widgets/animated_list_test.dart +++ b/packages/flutter/test/widgets/animated_list_test.dart @@ -5,10 +5,11 @@ import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('SliverAnimatedList.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAnimatedList.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -47,7 +48,7 @@ void main() { expect(finderCalled, true); }); - testWidgets('AnimatedList', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedList', (WidgetTester tester) async { Widget builder(BuildContext context, int index, Animation<double> animation) { return SizedBox( height: 100.0, @@ -126,7 +127,7 @@ void main() { }); group('SliverAnimatedList', () { - testWidgets('initialItemCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialItemCount', (WidgetTester tester) async { final Map<int, Animation<double>> animations = <int, Animation<double>>{}; await tester.pumpWidget( @@ -159,7 +160,7 @@ void main() { expect(animations[1]!.value, 1.0); }); - testWidgets('insert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('insert', (WidgetTester tester) async { final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); await tester.pumpWidget( @@ -245,7 +246,7 @@ void main() { }); // Test for insertAllItems with SliverAnimatedList - testWidgets('insertAll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('insertAll', (WidgetTester tester) async { final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); await tester.pumpWidget( @@ -302,7 +303,7 @@ void main() { }); // Test for removeAllItems with SliverAnimatedList - testWidgets('remove', (WidgetTester tester) async { + testWidgetsWithLeakTracking('remove', (WidgetTester tester) async { final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); final List<int> items = <int>[0, 1, 2]; @@ -379,7 +380,7 @@ void main() { }); // Test for removeAllItems with SliverAnimatedList - testWidgets('removeAll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('removeAll', (WidgetTester tester) async { final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); final List<int> items = <int>[0, 1, 2]; @@ -429,7 +430,7 @@ void main() { expect(find.text('item 2'), findsNothing); }); - testWidgets('works in combination with other slivers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works in combination with other slivers', (WidgetTester tester) async { final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); await tester.pumpWidget( @@ -494,7 +495,7 @@ void main() { expect(tester.getTopLeft(find.text('item 0')).dy, 200); }); - testWidgets('passes correctly derived index of findChildIndexCallback to the inner SliverChildBuilderDelegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('passes correctly derived index of findChildIndexCallback to the inner SliverChildBuilderDelegate', (WidgetTester tester) async { final List<int> items = <int>[0, 1, 2, 3]; final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); @@ -556,7 +557,7 @@ void main() { }); }); - testWidgets( + testWidgetsWithLeakTracking( 'AnimatedList.of() and maybeOf called with a context that does not contain AnimatedList', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -601,7 +602,7 @@ void main() { }, ); - testWidgets('AnimatedList.clipBehavior is forwarded to its inner CustomScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedList.clipBehavior is forwarded to its inner CustomScrollView', (WidgetTester tester) async { const Clip clipBehavior = Clip.none; await tester.pumpWidget( @@ -625,9 +626,12 @@ void main() { expect(tester.widget<CustomScrollView>(find.byType(CustomScrollView)).clipBehavior, clipBehavior); }); - testWidgets('AnimatedList.shrinkwrap is forwarded to its inner CustomScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedList.shrinkwrap is forwarded to its inner CustomScrollView', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/115040 final ScrollController controller = ScrollController(); + + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -650,7 +654,7 @@ void main() { expect(tester.widget<CustomScrollView>(find.byType(CustomScrollView)).shrinkWrap, true); }); - testWidgets('AnimatedList applies MediaQuery padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedList applies MediaQuery padding', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); EdgeInsets? innerMediaQueryPadding; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/animated_opacity_repaint_test.dart b/packages/flutter/test/widgets/animated_opacity_repaint_test.dart index d3ce05edd2938..bc9f44653829b 100644 --- a/packages/flutter/test/widgets/animated_opacity_repaint_test.dart +++ b/packages/flutter/test/widgets/animated_opacity_repaint_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('RenderAnimatedOpacityMixin does not drop layer when animating to 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderAnimatedOpacityMixin does not drop layer when animating to 1', (WidgetTester tester) async { RenderTestObject.paintCount = 0; final AnimationController controller = AnimationController(vsync: const TestVSync(), duration: const Duration(seconds: 1)); final Tween<double> opacityTween = Tween<double>(begin: 0, end: 1); @@ -40,7 +41,7 @@ void main() { expect(RenderTestObject.paintCount, 1); }); - testWidgets('RenderAnimatedOpacityMixin avoids repainting child as it animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderAnimatedOpacityMixin avoids repainting child as it animates', (WidgetTester tester) async { RenderTestObject.paintCount = 0; final AnimationController controller = AnimationController(vsync: const TestVSync(), duration: const Duration(seconds: 1)); final Tween<double> opacityTween = Tween<double>(begin: 0, end: 0.99); // Layer is dropped at 1 @@ -73,10 +74,13 @@ void main() { expect(RenderTestObject.paintCount, 1); }); - testWidgets('RenderAnimatedOpacityMixin allows opacity layer to be disposed when animating to 0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderAnimatedOpacityMixin allows opacity layer to be disposed when animating to 0 opacity', (WidgetTester tester) async { RenderTestObject.paintCount = 0; final AnimationController controller = AnimationController(vsync: const TestVSync(), duration: const Duration(seconds: 1)); final Tween<double> opacityTween = Tween<double>(begin: 0.99, end: 0); + + addTearDown(controller.dispose); + await tester.pumpWidget( ColoredBox( color: Colors.red, diff --git a/packages/flutter/test/widgets/animated_padding_test.dart b/packages/flutter/test/widgets/animated_padding_test.dart index e8cd37f1073e2..8542efdfd6113 100644 --- a/packages/flutter/test/widgets/animated_padding_test.dart +++ b/packages/flutter/test/widgets/animated_padding_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedPadding.debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPadding.debugFillProperties', (WidgetTester tester) async { final AnimatedPadding padding = AnimatedPadding( padding: const EdgeInsets.all(7.0), curve: Curves.ease, @@ -16,7 +17,7 @@ void main() { expect(padding, hasOneLineDescription); }); - testWidgets('AnimatedPadding padding visual-to-directional animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPadding padding visual-to-directional animation', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( @@ -58,7 +59,7 @@ void main() { expect(tester.getTopRight(find.byKey(target)), const Offset(700.0, 0.0)); }); - testWidgets('AnimatedPadding animated padding clamped to positive values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPadding animated padding clamped to positive values', (WidgetTester tester) async { final Key target = UniqueKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/animated_positioned_test.dart b/packages/flutter/test/widgets/animated_positioned_test.dart index f1a794069a4ce..8d3b5efe73faf 100644 --- a/packages/flutter/test/widgets/animated_positioned_test.dart +++ b/packages/flutter/test/widgets/animated_positioned_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedPositioned.fromRect control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositioned.fromRect control test', (WidgetTester tester) async { final AnimatedPositioned positioned = AnimatedPositioned.fromRect( rect: const Rect.fromLTWH(7.0, 5.0, 12.0, 16.0), duration: const Duration(milliseconds: 200), @@ -20,7 +21,7 @@ void main() { expect(positioned, hasOneLineDescription); }); - testWidgets('AnimatedPositioned - basics (VISUAL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositioned - basics (VISUAL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -102,7 +103,7 @@ void main() { ); }); - testWidgets('AnimatedPositionedDirectional - basics (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - basics (LTR)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -188,7 +189,7 @@ void main() { ); }); - testWidgets('AnimatedPositionedDirectional - basics (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - basics (RTL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -274,7 +275,7 @@ void main() { ); }); - testWidgets('AnimatedPositioned - interrupted animation (VISUAL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositioned - interrupted animation (VISUAL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -357,7 +358,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(200.0, 200.0))); }); - testWidgets('AnimatedPositioned - switching variables (VISUAL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositioned - switching variables (VISUAL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -416,7 +417,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 150.0))); }); - testWidgets('AnimatedPositionedDirectional - interrupted animation (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - interrupted animation (LTR)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -505,7 +506,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(200.0, 200.0))); }); - testWidgets('AnimatedPositionedDirectional - switching variables (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - switching variables (LTR)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -568,7 +569,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(350.0, 150.0))); }); - testWidgets('AnimatedPositionedDirectional - interrupted animation (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - interrupted animation (RTL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; @@ -657,7 +658,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(600.0, 200.0))); }); - testWidgets('AnimatedPositionedDirectional - switching variables (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional - switching variables (RTL)', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); RenderBox box; diff --git a/packages/flutter/test/widgets/animated_size_test.dart b/packages/flutter/test/widgets/animated_size_test.dart index 1e70d3446b8c8..75a9272d05f8f 100644 --- a/packages/flutter/test/widgets/animated_size_test.dart +++ b/packages/flutter/test/widgets/animated_size_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestPaintingContext implements PaintingContext { final List<Invocation> invocations = <Invocation>[]; @@ -17,7 +18,7 @@ class TestPaintingContext implements PaintingContext { void main() { group('AnimatedSize', () { - testWidgets('animates forwards then backwards with stable-sized children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animates forwards then backwards with stable-sized children', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: AnimatedSize( @@ -87,7 +88,7 @@ void main() { expect(box.size.height, equals(100.0)); }); - testWidgets('clamps animated size to constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clamps animated size to constraints', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox ( @@ -132,7 +133,7 @@ void main() { expect(box.size.height, equals(100.0)); }); - testWidgets('tracks unstable child, then resumes animation when child stabilizes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tracks unstable child, then resumes animation when child stabilizes', (WidgetTester tester) async { Future<void> pumpMillis(int millis) async { await tester.pump(Duration(milliseconds: millis)); } @@ -215,7 +216,7 @@ void main() { verify(size: 100.0, state: RenderAnimatedSizeState.stable); }); - testWidgets('resyncs its animation controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('resyncs its animation controller', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: AnimatedSize( @@ -246,7 +247,7 @@ void main() { expect(box.size.width, equals(150.0)); }); - testWidgets('does not run animation unnecessarily', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not run animation unnecessarily', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: AnimatedSize( @@ -269,7 +270,7 @@ void main() { } }); - testWidgets('can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: AnimatedSize( @@ -303,7 +304,7 @@ void main() { } }); - testWidgets('works wrapped in IntrinsicHeight and Wrap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works wrapped in IntrinsicHeight and Wrap', (WidgetTester tester) async { Future<void> pumpWidget(Size size, [Duration? duration]) async { return tester.pumpWidget( Center( @@ -350,7 +351,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(222, 222)); }); - testWidgets('re-attach with interrupted animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('re-attach with interrupted animation', (WidgetTester tester) async { const Key key1 = ValueKey<String>('key1'); const Key key2 = ValueKey<String>('key2'); late StateSetter setState; @@ -433,5 +434,42 @@ void main() { const Size.square(150), ); }); + + testWidgets('disposes animation and controller', (WidgetTester tester) async { + await tester.pumpWidget( + const Center( + child: AnimatedSize( + duration: Duration(milliseconds: 200), + child: SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ), + ); + + final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize)); + + await tester.pumpWidget( + const Center(), + ); + + expect(box.debugAnimation, isNotNull); + expect(box.debugAnimation!.isDisposed, isTrue); + expect(box.debugController, isNotNull); + expect( + () => box.debugController!.dispose(), + throwsA(isA<AssertionError>().having( + (AssertionError error) => error.message, + 'message', + equalsIgnoringHashCodes( + 'AnimationController.dispose() called more than once.\n' + 'A given AnimationController cannot be disposed more than once.\n' + 'The following AnimationController object was disposed multiple times:\n' + ' AnimationController#00000(⏮ 0.000; paused; DISPOSED)', + ), + )), + ); + }); }); } diff --git a/packages/flutter/test/widgets/animated_switcher_test.dart b/packages/flutter/test/widgets/animated_switcher_test.dart index 0d914cb73ed9c..70de6e294d385 100644 --- a/packages/flutter/test/widgets/animated_switcher_test.dart +++ b/packages/flutter/test/widgets/animated_switcher_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('AnimatedSwitcher fades in a new child.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher fades in a new child.', (WidgetTester tester) async { final UniqueKey containerOne = UniqueKey(); final UniqueKey containerTwo = UniqueKey(); final UniqueKey containerThree = UniqueKey(); @@ -50,7 +51,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('AnimatedSwitcher can handle back-to-back changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher can handle back-to-back changes.', (WidgetTester tester) async { final UniqueKey container1 = UniqueKey(); final UniqueKey container2 = UniqueKey(); final UniqueKey container3 = UniqueKey(); @@ -85,7 +86,7 @@ void main() { expect(find.byKey(container3), findsOneWidget); }); - testWidgets("AnimatedSwitcher doesn't transition in a new child of the same type.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("AnimatedSwitcher doesn't transition in a new child of the same type.", (WidgetTester tester) async { await tester.pumpWidget( AnimatedSwitcher( duration: const Duration(milliseconds: 100), @@ -111,7 +112,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('AnimatedSwitcher handles null children.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher handles null children.', (WidgetTester tester) async { await tester.pumpWidget( const AnimatedSwitcher( duration: Duration(milliseconds: 100), @@ -166,7 +167,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets("AnimatedSwitcher doesn't start any animations after dispose.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("AnimatedSwitcher doesn't start any animations after dispose.", (WidgetTester tester) async { await tester.pumpWidget(AnimatedSwitcher( duration: const Duration(milliseconds: 100), child: Container(color: const Color(0xff000000)), @@ -178,7 +179,7 @@ void main() { expect(await tester.pumpAndSettle(), equals(1)); }); - testWidgets('AnimatedSwitcher uses custom layout.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher uses custom layout.', (WidgetTester tester) async { Widget newLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) { return Column( children: <Widget>[ @@ -199,7 +200,7 @@ void main() { expect(find.byType(Column), findsOneWidget); }); - testWidgets('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async { late List<Widget> foundChildren; Widget newLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) { foundChildren = <Widget>[ @@ -254,7 +255,7 @@ void main() { } }); - testWidgets("AnimatedSwitcher doesn't reset state of the children in transitions.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("AnimatedSwitcher doesn't reset state of the children in transitions.", (WidgetTester tester) async { final UniqueKey statefulOne = UniqueKey(); final UniqueKey statefulTwo = UniqueKey(); final UniqueKey statefulThree = UniqueKey(); @@ -305,7 +306,7 @@ void main() { expect(StatefulTestState.generation, equals(3)); }); - testWidgets('AnimatedSwitcher updates widgets without animating if they are isomorphic.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher updates widgets without animating if they are isomorphic.', (WidgetTester tester) async { Future<void> pumpChild(Widget child) async { return tester.pumpWidget( Directionality( @@ -332,7 +333,7 @@ void main() { expect(find.text('2'), findsOneWidget); }); - testWidgets('AnimatedSwitcher updates previous child transitions if the transitionBuilder changes.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher updates previous child transitions if the transitionBuilder changes.', (WidgetTester tester) async { final UniqueKey containerOne = UniqueKey(); final UniqueKey containerTwo = UniqueKey(); final UniqueKey containerThree = UniqueKey(); @@ -416,7 +417,7 @@ void main() { } }); - testWidgets('AnimatedSwitcher does not duplicate animations if the same child is entered twice.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSwitcher does not duplicate animations if the same child is entered twice.', (WidgetTester tester) async { Future<void> pumpChild(Widget child) async { return tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/annotated_region_test.dart b/packages/flutter/test/widgets/annotated_region_test.dart index ec159c90a11aa..66ab89b630354 100644 --- a/packages/flutter/test/widgets/annotated_region_test.dart +++ b/packages/flutter/test/widgets/annotated_region_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('provides a value to the layer tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('provides a value to the layer tree', (WidgetTester tester) async { await tester.pumpWidget( const AnnotatedRegion<int>( value: 1, @@ -18,7 +19,8 @@ void main() { final AnnotatedRegionLayer<int> layer = layers.whereType<AnnotatedRegionLayer<int>>().first; expect(layer.value, 1); }); - testWidgets('provides a value to the layer tree in a particular region', (WidgetTester tester) async { + + testWidgetsWithLeakTracking('provides a value to the layer tree in a particular region', (WidgetTester tester) async { await tester.pumpWidget( Transform.translate( offset: const Offset(25.0, 25.0), diff --git a/packages/flutter/test/widgets/app_lifecycle_listener_test.dart b/packages/flutter/test/widgets/app_lifecycle_listener_test.dart index a93b97628b37e..d704785804b3b 100644 --- a/packages/flutter/test/widgets/app_lifecycle_listener_test.dart +++ b/packages/flutter/test/widgets/app_lifecycle_listener_test.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { late AppLifecycleListener listener; @@ -47,13 +48,13 @@ void main() { 'There were ${TestAppLifecycleListener.registerCount} listeners that were not disposed of in tests.'); }); - testWidgets('Default Diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default Diagnostics', (WidgetTester tester) async { listener = TestAppLifecycleListener(binding: tester.binding); expect(listener.toString(), equalsIgnoringHashCodes('TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>)')); }); - testWidgets('Diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Diagnostics', (WidgetTester tester) async { Future<AppExitResponse> handleExitRequested() async { return AppExitResponse.cancel; } @@ -69,7 +70,7 @@ void main() { 'TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>, onStateChange, onExitRequested)')); }); - testWidgets('listens to AppLifecycleState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('listens to AppLifecycleState', (WidgetTester tester) async { final List<AppLifecycleState> states = <AppLifecycleState>[tester.binding.lifecycleState!]; void stateChange(AppLifecycleState state) { states.add(state); @@ -95,7 +96,7 @@ void main() { ])); }); - testWidgets('Triggers correct state transition callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Triggers correct state transition callbacks', (WidgetTester tester) async { final List<String> transitions = <String>[]; listener = TestAppLifecycleListener( binding: WidgetsBinding.instance, @@ -148,7 +149,7 @@ void main() { await setAppLifeCycleState(AppLifecycleState.detached); }); - testWidgets('Receives exit requests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Receives exit requests', (WidgetTester tester) async { bool exitRequested = false; Future<AppExitResponse> handleExitRequested() async { exitRequested = true; diff --git a/packages/flutter/test/widgets/app_navigator_key_test.dart b/packages/flutter/test/widgets/app_navigator_key_test.dart index a989516e14347..57c959329038b 100644 --- a/packages/flutter/test/widgets/app_navigator_key_test.dart +++ b/packages/flutter/test/widgets/app_navigator_key_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Route<void> generateRoute(RouteSettings settings) => PageRouteBuilder<void>( settings: settings, @@ -13,7 +14,7 @@ Route<void> generateRoute(RouteSettings settings) => PageRouteBuilder<void>( ); void main() { - testWidgets('WidgetsApp.navigatorKey', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp.navigatorKey', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); await tester.pumpWidget(WidgetsApp( navigatorKey: key, diff --git a/packages/flutter/test/widgets/app_overrides_test.dart b/packages/flutter/test/widgets/app_overrides_test.dart index 66a7a1c6c0c57..601db0b84c394 100644 --- a/packages/flutter/test/widgets/app_overrides_test.dart +++ b/packages/flutter/test/widgets/app_overrides_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestRoute<T> extends PageRoute<T> { TestRoute({ required this.child, super.settings }); @@ -40,7 +41,7 @@ Future<void> pumpApp(WidgetTester tester) async { } void main() { - testWidgets('WidgetsApp control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp control test', (WidgetTester tester) async { await pumpApp(tester); expect(find.byType(WidgetsApp), findsOneWidget); expect(find.byType(Navigator), findsOneWidget); @@ -48,7 +49,7 @@ void main() { expect(find.byType(CheckedModeBanner), findsOneWidget); }); - testWidgets('showPerformanceOverlayOverride true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showPerformanceOverlayOverride true', (WidgetTester tester) async { expect(WidgetsApp.showPerformanceOverlayOverride, false); WidgetsApp.showPerformanceOverlayOverride = true; await pumpApp(tester); @@ -59,7 +60,7 @@ void main() { WidgetsApp.showPerformanceOverlayOverride = false; }); - testWidgets('showPerformanceOverlayOverride false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showPerformanceOverlayOverride false', (WidgetTester tester) async { WidgetsApp.showPerformanceOverlayOverride = true; expect(WidgetsApp.showPerformanceOverlayOverride, true); WidgetsApp.showPerformanceOverlayOverride = false; @@ -70,7 +71,7 @@ void main() { expect(find.byType(CheckedModeBanner), findsOneWidget); }); - testWidgets('debugAllowBannerOverride false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugAllowBannerOverride false', (WidgetTester tester) async { expect(WidgetsApp.showPerformanceOverlayOverride, false); expect(WidgetsApp.debugAllowBannerOverride, true); WidgetsApp.debugAllowBannerOverride = false; @@ -82,7 +83,7 @@ void main() { WidgetsApp.debugAllowBannerOverride = true; // restore to default value }); - testWidgets('debugAllowBannerOverride true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugAllowBannerOverride true', (WidgetTester tester) async { WidgetsApp.debugAllowBannerOverride = false; expect(WidgetsApp.showPerformanceOverlayOverride, false); expect(WidgetsApp.debugAllowBannerOverride, false); diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart index c74273310602c..38c67439767be 100644 --- a/packages/flutter/test/widgets/app_test.dart +++ b/packages/flutter/test/widgets/app_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestIntent extends Intent { const TestIntent(); @@ -23,7 +24,7 @@ class TestAction extends Action<Intent> { } void main() { - testWidgets('WidgetsApp with builder only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp with builder only', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( WidgetsApp( @@ -37,7 +38,7 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - testWidgets('WidgetsApp default key bindings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp default key bindings', (WidgetTester tester) async { bool? checked = false; final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -64,7 +65,7 @@ void main() { expect(checked, isTrue); }); - testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp can override default key bindings', (WidgetTester tester) async { final TestAction action = TestAction(); bool? checked = false; final GlobalKey key = GlobalKey(); @@ -101,7 +102,7 @@ void main() { expect(action.calls, equals(1)); }); - testWidgets('WidgetsApp default activation key mappings work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp default activation key mappings work', (WidgetTester tester) async { bool? checked = false; await tester.pumpWidget( @@ -168,7 +169,7 @@ void main() { } } - testWidgets('push unknown route when onUnknownRoute is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('push unknown route when onUnknownRoute is null', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); expectFlutterError( key: key, @@ -196,7 +197,7 @@ void main() { ); }); - testWidgets('push unknown route when onUnknownRoute returns null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('push unknown route when onUnknownRoute returns null', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); expectFlutterError( key: key, @@ -218,7 +219,7 @@ void main() { }); }); - testWidgets('WidgetsApp can customize initial routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp can customize initial routes', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( WidgetsApp( @@ -271,12 +272,13 @@ void main() { expect(find.text('regular page'), findsNothing); }); - testWidgets('WidgetsApp.router works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp.router works', (WidgetTester tester) async { final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -288,6 +290,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); await tester.pumpWidget(WidgetsApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), @@ -301,9 +304,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('WidgetsApp.router route information parser is optional', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + notDisposedAllowList: <String, int?> {'_RestorableRouteInformation': 1}, + )); + + testWidgetsWithLeakTracking('WidgetsApp.router route information parser is optional', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -315,6 +323,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget(WidgetsApp.router( routerDelegate: delegate, @@ -327,9 +336,14 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('WidgetsApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + notDisposedAllowList: <String, int?> {'_RestorableRouteInformation': 1}, + )); + + testWidgetsWithLeakTracking('WidgetsApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -341,12 +355,14 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); await expectLater(() async { await tester.pumpWidget(WidgetsApp.router( routeInformationProvider: provider, @@ -356,7 +372,7 @@ void main() { }, throwsAssertionError); }); - testWidgets('WidgetsApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); @@ -368,6 +384,7 @@ void main() { return route.didPop(result); }, ); + addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>(routerDelegate: delegate); await expectLater(() async { @@ -379,25 +396,29 @@ void main() { }, throwsAssertionError); }); - testWidgets('WidgetsApp.router router config works', (WidgetTester tester) async { - final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( - routeInformationProvider: PlatformRouteInformationProvider( - initialRouteInformation: RouteInformation( - uri: Uri.parse('initial'), - ), + testWidgetsWithLeakTracking('WidgetsApp.router router config works', (WidgetTester tester) async { + final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( + builder: (BuildContext context, RouteInformation information) { + return Text(information.uri.toString()); + }, + onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { + delegate.routeInformation = RouteInformation( + uri: Uri.parse('popped'), + ); + return route.didPop(result); + }, + ); + addTearDown(delegate.dispose); + final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( + initialRouteInformation: RouteInformation( + uri: Uri.parse('initial'), ), + ); + addTearDown(provider.dispose); + final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( + routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), - routerDelegate: SimpleNavigatorRouterDelegate( - builder: (BuildContext context, RouteInformation information) { - return Text(information.uri.toString()); - }, - onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { - delegate.routeInformation = RouteInformation( - uri: Uri.parse('popped'), - ); - return route.didPop(result); - }, - ), + routerDelegate: delegate, backButtonDispatcher: RootBackButtonDispatcher() ); await tester.pumpWidget(WidgetsApp.router( @@ -411,24 +432,35 @@ void main() { await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); - }); - - testWidgets('WidgetsApp.router has correct default', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + notDisposedAllowList: <String, int?> {'_RestorableRouteInformation': 1}, + )); + + testWidgetsWithLeakTracking('WidgetsApp.router has correct default', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<Object?> route, Object? result, SimpleNavigatorRouterDelegate delegate) => true, ); + addTearDown(delegate.dispose); await tester.pumpWidget(WidgetsApp.router( routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, color: const Color(0xFF123456), )); expect(find.text('/'), findsOneWidget); - }); - - testWidgets('WidgetsApp has correct default ScrollBehavior', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + notDisposedAllowList: <String, int?> {'_RestorableRouteInformation': 1}, + )); + + testWidgetsWithLeakTracking('WidgetsApp has correct default ScrollBehavior', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( WidgetsApp( @@ -569,7 +601,7 @@ void main() { ); }); - testWidgets("WidgetsApp reports an exception if the selected locale isn't supported", (WidgetTester tester) async { + testWidgetsWithLeakTracking("WidgetsApp reports an exception if the selected locale isn't supported", (WidgetTester tester) async { late final List<Locale>? localesArg; late final Iterable<Locale> supportedLocalesArg; await tester.pumpWidget( @@ -593,7 +625,7 @@ void main() { expect(tester.takeException(), "Warning: This application's locale, C_UTF-8, is not supported by all of its localization delegates."); }); - testWidgets("WidgetsApp doesn't have dependency on MediaQuery", (WidgetTester tester) async { + testWidgetsWithLeakTracking("WidgetsApp doesn't have dependency on MediaQuery", (WidgetTester tester) async { int routeBuildCount = 0; final Widget widget = WidgetsApp( @@ -619,8 +651,10 @@ void main() { expect(routeBuildCount, equals(1)); }); - testWidgets('WidgetsApp provides meta based shortcuts for iOS and macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp provides meta based shortcuts for iOS and macOS', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final SelectAllSpy selectAllSpy = SelectAllSpy(); final CopySpy copySpy = CopySpy(); final PasteSpy pasteSpy = PasteSpy(); @@ -683,6 +717,90 @@ void main() { expect(copySpy.invoked, isTrue); expect(pasteSpy.invoked, isTrue); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); + + group('Android Predictive Back', () { + Future<void> setAppLifeCycleState(AppLifecycleState state) async { + final ByteData? message = const StringCodec().encodeMessage(state.toString()); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage('flutter/lifecycle', message, (ByteData? data) {}); + } + + final List<bool> frameworkHandlesBacks = <bool>[]; + setUp(() async { + frameworkHandlesBacks.clear(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA<bool>()); + frameworkHandlesBacks.add(methodCall.arguments as bool); + } + return; + }); + }); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + await setAppLifeCycleState(AppLifecycleState.resumed); + }); + + testWidgetsWithLeakTracking('WidgetsApp calls setFrameworkHandlesBack only when app is ready', (WidgetTester tester) async { + // Start in the `resumed` state, where setFrameworkHandlesBack should be + // called like normal. + await setAppLifeCycleState(AppLifecycleState.resumed); + + late BuildContext currentContext; + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFF123456), + builder: (BuildContext context, Widget? child) { + currentContext = context; + return const Placeholder(); + }, + ), + ); + + expect(frameworkHandlesBacks, isEmpty); + + const NavigationNotification(canHandlePop: true).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks, isNotEmpty); + expect(frameworkHandlesBacks.last, isTrue); + + const NavigationNotification(canHandlePop: false).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks.last, isFalse); + + // Set the app state to inactive, where setFrameworkHandlesBack shouldn't + // be called. + await setAppLifeCycleState(AppLifecycleState.inactive); + + final int finalCallsLength = frameworkHandlesBacks.length; + const NavigationNotification(canHandlePop: true).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks, hasLength(finalCallsLength)); + + const NavigationNotification(canHandlePop: false).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks, hasLength(finalCallsLength)); + + // Set the app state to detached, which also shouldn't call + // setFrameworkHandlesBack. Must go to paused, then detached. + await setAppLifeCycleState(AppLifecycleState.paused); + await setAppLifeCycleState(AppLifecycleState.detached); + + const NavigationNotification(canHandlePop: true).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks, hasLength(finalCallsLength)); + + const NavigationNotification(canHandlePop: false).dispatch(currentContext); + await tester.pumpAndSettle(); + expect(frameworkHandlesBacks, hasLength(finalCallsLength)); + }, + skip: kIsWeb, // [intended] predictive back is only native Android. + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }) + ); + }); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation); diff --git a/packages/flutter/test/widgets/app_title_test.dart b/packages/flutter/test/widgets/app_title_test.dart index 7cddafb6d56ea..1528c3c6c12a1 100644 --- a/packages/flutter/test/widgets/app_title_test.dart +++ b/packages/flutter/test/widgets/app_title_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color kTitleColor = Color(0xFF333333); const String kTitleString = 'Hello World'; @@ -30,13 +31,13 @@ Future<void> pumpApp(WidgetTester tester, { GenerateAppTitle? onGenerateTitle, C } void main() { - testWidgets('Specified title and color are used to build a Title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Specified title and color are used to build a Title', (WidgetTester tester) async { await pumpApp(tester); expect(tester.widget<Title>(find.byType(Title)).title, kTitleString); expect(tester.widget<Title>(find.byType(Title)).color, kTitleColor); }); - testWidgets('Specified color is made opaque for Title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Specified color is made opaque for Title', (WidgetTester tester) async { // The Title widget can only handle fully opaque colors, the WidgetApp should // ensure it only uses a fully opaque version of its color for the title. const Color transparentBlue = Color(0xDD0000ff); @@ -45,7 +46,7 @@ void main() { expect(tester.widget<Title>(find.byType(Title)).color, opaqueBlue); }); - testWidgets('onGenerateTitle handles changing locales', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onGenerateTitle handles changing locales', (WidgetTester tester) async { String generateTitle(BuildContext context) { return Localizations.localeOf(context).toString(); } diff --git a/packages/flutter/test/widgets/aspect_ratio_test.dart b/packages/flutter/test/widgets/aspect_ratio_test.dart index b103f381cf909..f480fa37cee97 100644 --- a/packages/flutter/test/widgets/aspect_ratio_test.dart +++ b/packages/flutter/test/widgets/aspect_ratio_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Future<Size> _getSize(WidgetTester tester, BoxConstraints constraints, double aspectRatio) async { final Key childKey = UniqueKey(); @@ -25,12 +26,12 @@ Future<Size> _getSize(WidgetTester tester, BoxConstraints constraints, double as } void main() { - testWidgets('Aspect ratio control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Aspect ratio control test', (WidgetTester tester) async { expect(await _getSize(tester, BoxConstraints.loose(const Size(500.0, 500.0)), 2.0), equals(const Size(500.0, 250.0))); expect(await _getSize(tester, BoxConstraints.loose(const Size(500.0, 500.0)), 0.5), equals(const Size(250.0, 500.0))); }); - testWidgets('Aspect ratio infinite width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Aspect ratio infinite width', (WidgetTester tester) async { final Key childKey = UniqueKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/async_test.dart b/packages/flutter/test/widgets/async_test.dart index 9c685412061aa..c3eb815cc00d3 100644 --- a/packages/flutter/test/widgets/async_test.dart +++ b/packages/flutter/test/widgets/async_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { Widget snapshotText(BuildContext context, AsyncSnapshot<String> snapshot) { @@ -66,21 +67,21 @@ void main() { }); }); group('Async smoke tests', () { - testWidgets('FutureBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FutureBuilder', (WidgetTester tester) async { await tester.pumpWidget(FutureBuilder<String>( future: Future<String>.value('hello'), builder: snapshotText, )); await eventFiring(tester); }); - testWidgets('StreamBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StreamBuilder', (WidgetTester tester) async { await tester.pumpWidget(StreamBuilder<String>( stream: Stream<String>.fromIterable(<String>['hello', 'world']), builder: snapshotText, )); await eventFiring(tester); }); - testWidgets('StreamFold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StreamFold', (WidgetTester tester) async { await tester.pumpWidget(StringCollector( stream: Stream<String>.fromIterable(<String>['hello', 'world']), )); @@ -88,7 +89,7 @@ void main() { }); }); group('FutureBuilder', () { - testWidgets('gives expected snapshot with SynchronousFuture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gives expected snapshot with SynchronousFuture', (WidgetTester tester) async { final SynchronousFuture<String> future = SynchronousFuture<String>('flutter'); await tester.pumpWidget(FutureBuilder<String>( future: future, @@ -102,7 +103,7 @@ void main() { )); }); - testWidgets('gracefully handles transition from null future', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition from null future', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(FutureBuilder<String>( key: key, builder: snapshotText, future: null, @@ -114,7 +115,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null future', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to null future', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final Completer<String> completer = Completer<String>(); await tester.pumpWidget(FutureBuilder<String>( @@ -129,7 +130,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other future', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to other future', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final Completer<String> completerA = Completer<String>(); final Completer<String> completerB = Completer<String>(); @@ -146,7 +147,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, B, null, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to success', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tracks life-cycle of Future to success', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(FutureBuilder<String>( future: completer.future, builder: snapshotText, @@ -156,7 +157,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, hello, null, null)'), findsOneWidget); }); - testWidgets('tracks life-cycle of Future to error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tracks life-cycle of Future to error', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(FutureBuilder<String>( future: completer.future, builder: snapshotText, @@ -166,7 +167,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, trace)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('runs the builder using given initial data', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(FutureBuilder<String>( key: key, @@ -176,7 +177,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores initialData when reconfiguring', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(FutureBuilder<String>( key: key, @@ -194,7 +195,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsOneWidget); }); - testWidgets('debugRethrowError rethrows caught error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugRethrowError rethrows caught error', (WidgetTester tester) async { FutureBuilder.debugRethrowError = true; final Completer<void> caughtError = Completer<void>(); await runZonedGuarded(() async { @@ -214,7 +215,7 @@ void main() { }); }); group('StreamBuilder', () { - testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition from null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(StreamBuilder<String>( key: key, builder: snapshotText, stream: null, @@ -226,7 +227,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, null, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to null stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controller = StreamController<String>(); await tester.pumpWidget(StreamBuilder<String>( @@ -238,7 +239,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsOneWidget); }); - testWidgets('gracefully handles transition to other stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to other stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controllerA = StreamController<String>(); final StreamController<String> controllerB = StreamController<String>(); @@ -254,7 +255,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.active, B, null, null)'), findsOneWidget); }); - testWidgets('tracks events and errors of stream until completion', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tracks events and errors of stream until completion', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controller = StreamController<String>(); await tester.pumpWidget(StreamBuilder<String>( @@ -274,7 +275,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, 4, null, null)'), findsOneWidget); }); - testWidgets('runs the builder using given initial data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('runs the builder using given initial data', (WidgetTester tester) async { final StreamController<String> controller = StreamController<String>(); await tester.pumpWidget(StreamBuilder<String>( stream: controller.stream, @@ -283,7 +284,7 @@ void main() { )); expect(find.text('AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'), findsOneWidget); }); - testWidgets('ignores initialData when reconfiguring', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores initialData when reconfiguring', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(StreamBuilder<String>( key: key, @@ -303,7 +304,7 @@ void main() { }); }); group('FutureBuilder and StreamBuilder behave identically on Stream from Future', () { - testWidgets('when completing with data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when completing with data', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(future: completer.future, builder: snapshotText), @@ -314,7 +315,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, hello, null, null)'), findsNWidgets(2)); }); - testWidgets('when completing with error and with empty stack trace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when completing with error and with empty stack trace', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(future: completer.future, builder: snapshotText), @@ -325,7 +326,7 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, )'), findsNWidgets(2)); }); - testWidgets('when completing with error and with stack trace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when completing with error and with stack trace', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(future: completer.future, builder: snapshotText), @@ -336,21 +337,21 @@ void main() { await eventFiring(tester); expect(find.text('AsyncSnapshot<String>(ConnectionState.done, null, bad, trace)'), findsNWidgets(2)); }); - testWidgets('when Future is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when Future is null', (WidgetTester tester) async { await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(builder: snapshotText, future: null), StreamBuilder<String>(builder: snapshotText, stream: null,), ])); expect(find.text('AsyncSnapshot<String>(ConnectionState.none, null, null, null)'), findsNWidgets(2)); }); - testWidgets('when initialData is used with null Future and Stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when initialData is used with null Future and Stream', (WidgetTester tester) async { await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(builder: snapshotText, initialData: 'I', future: null), StreamBuilder<String>(builder: snapshotText, initialData: 'I', stream: null), ])); expect(find.text('AsyncSnapshot<String>(ConnectionState.none, I, null, null)'), findsNWidgets(2)); }); - testWidgets('when using initialData and completing with data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when using initialData and completing with data', (WidgetTester tester) async { final Completer<String> completer = Completer<String>(); await tester.pumpWidget(Column(children: <Widget>[ FutureBuilder<String>(future: completer.future, builder: snapshotText, initialData: 'I'), @@ -363,7 +364,7 @@ void main() { }); }); group('StreamBuilderBase', () { - testWidgets('gracefully handles transition from null stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition from null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(StringCollector(key: key)); expect(find.text(''), findsOneWidget); @@ -371,7 +372,7 @@ void main() { await tester.pumpWidget(StringCollector(key: key, stream: controller.stream)); expect(find.text('conn'), findsOneWidget); }); - testWidgets('gracefully handles transition to null stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to null stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controller = StreamController<String>(); await tester.pumpWidget(StringCollector(key: key, stream: controller.stream)); @@ -379,7 +380,7 @@ void main() { await tester.pumpWidget(StringCollector(key: key)); expect(find.text('conn, disc'), findsOneWidget); }); - testWidgets('gracefully handles transition to other stream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('gracefully handles transition to other stream', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controllerA = StreamController<String>(); final StreamController<String> controllerB = StreamController<String>(); @@ -390,7 +391,7 @@ void main() { await eventFiring(tester); expect(find.text('conn, disc, conn, data:B'), findsOneWidget); }); - testWidgets('tracks events and errors until completion', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tracks events and errors until completion', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final StreamController<String> controller = StreamController<String>(); await tester.pumpWidget(StringCollector(key: key, stream: controller.stream)); diff --git a/packages/flutter/test/widgets/autocomplete_test.dart b/packages/flutter/test/widgets/autocomplete_test.dart index a35d324e5d4ef..adcac0830525a 100644 --- a/packages/flutter/test/widgets/autocomplete_test.dart +++ b/packages/flutter/test/widgets/autocomplete_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class User { const User({ @@ -45,7 +46,7 @@ void main() { User(name: 'Charlie', email: 'charlie123@gmail.com'), ]; - testWidgets('can filter and select a list of string options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can filter and select a list of string options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -129,7 +130,7 @@ void main() { expect(lastOptions.elementAt(5), 'northern white rhinoceros'); }); - testWidgets('tapping on an option selects it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapping on an option selects it', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -199,7 +200,7 @@ void main() { expect(textEditingController.text, equals(kOptions[2])); }); - testWidgets('can filter and select a list of custom User options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can filter and select a list of custom User options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<User> lastOptions; @@ -277,7 +278,7 @@ void main() { expect(lastOptions.elementAt(0), kOptionsUsers[1]); }); - testWidgets('can specify a custom display string for a list of custom User options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can specify a custom display string for a list of custom User options', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<User> lastOptions; @@ -360,7 +361,7 @@ void main() { expect(lastOptions.elementAt(0), kOptionsUsers[1]); }); - testWidgets('onFieldSubmitted selects the first option', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onFieldSubmitted selects the first option', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -421,7 +422,149 @@ void main() { expect(textEditingController.text, lastOptions.elementAt(0)); }); - testWidgets('options follow field when it moves', (WidgetTester tester) async { + group('optionsViewOpenDirection', () { + testWidgetsWithLeakTracking('unset (default behavior): open downward', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: RawAutocomplete<String>( + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) { + return TextField(controller: controller, focusNode: focusNode); + }, + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { + return const Text('a'); + }, + ), + ), + ), + ), + ); + await tester.showKeyboard(find.byType(TextField)); + expect(tester.getBottomLeft(find.byType(TextField)), + offsetMoreOrLessEquals(tester.getTopLeft(find.text('a')))); + }); + + testWidgetsWithLeakTracking('down: open downward', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: RawAutocomplete<String>( + optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) { + return TextField(controller: controller, focusNode: focusNode); + }, + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { + return const Text('a'); + }, + ), + ), + ), + ), + ); + await tester.showKeyboard(find.byType(TextField)); + expect(tester.getBottomLeft(find.byType(TextField)), + offsetMoreOrLessEquals(tester.getTopLeft(find.text('a')))); + }); + + testWidgetsWithLeakTracking('up: open upward', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: RawAutocomplete<String>( + optionsViewOpenDirection: OptionsViewOpenDirection.up, + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) { + return TextField(controller: controller, focusNode: focusNode); + }, + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { + return const Text('a'); + }, + ), + ), + ), + ), + ); + await tester.showKeyboard(find.byType(TextField)); + expect(tester.getTopLeft(find.byType(TextField)), + offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a')))); + }); + + group('fieldViewBuilder not passed', () { + testWidgetsWithLeakTracking('down', (WidgetTester tester) async { + final GlobalKey autocompleteKey = GlobalKey(); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + TextField(controller: controller, focusNode: focusNode), + RawAutocomplete<String>( + key: autocompleteKey, + textEditingController: controller, + focusNode: focusNode, + optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { + return const Text('a'); + }, + ), + ], + ), + ), + ), + ); + await tester.showKeyboard(find.byType(TextField)); + expect(tester.getBottomLeft(find.byKey(autocompleteKey)), + offsetMoreOrLessEquals(tester.getTopLeft(find.text('a')))); + }); + + testWidgetsWithLeakTracking('up', (WidgetTester tester) async { + final GlobalKey autocompleteKey = GlobalKey(); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + RawAutocomplete<String>( + key: autocompleteKey, + textEditingController: controller, + focusNode: focusNode, + optionsViewOpenDirection: OptionsViewOpenDirection.up, + optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'], + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) { + return const Text('a'); + }, + ), + TextField(controller: controller, focusNode: focusNode), + ], + ), + ), + ), + ); + await tester.showKeyboard(find.byType(TextField)); + expect(tester.getTopLeft(find.byKey(autocompleteKey)), + offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a')))); + }); + }); + }); + + testWidgetsWithLeakTracking('options follow field when it moves', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late StateSetter setState; @@ -495,7 +638,7 @@ void main() { expect(optionsOffsetOpen.dy, fieldOffset.dy + fieldSize.height); }); - testWidgets('can prevent options from showing by returning an empty iterable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can prevent options from showing by returning an empty iterable', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -558,13 +701,15 @@ void main() { expect(lastOptions.elementAt(1), 'elephant'); }); - testWidgets('can create a field outside of fieldViewBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can create a field outside of fieldViewBuilder', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); final GlobalKey autocompleteKey = GlobalKey(); late Iterable<String> lastOptions; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final TextEditingController textEditingController = TextEditingController(); + addTearDown(textEditingController.dispose); await tester.pumpWidget( MaterialApp( @@ -624,7 +769,7 @@ void main() { expect(textEditingController.text, lastOptions.elementAt(0)); }); - testWidgets('initialValue sets initial text field value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialValue sets initial text field value', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -684,9 +829,11 @@ void main() { expect(textEditingController.text, selection); }); - testWidgets('initialValue cannot be defined if TextEditingController is defined', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initialValue cannot be defined if TextEditingController is defined', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final TextEditingController textEditingController = TextEditingController(); + addTearDown(textEditingController.dispose); expect( () { @@ -716,7 +863,7 @@ void main() { ); }); - testWidgets('support asynchronous options builder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('support asynchronous options builder', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; @@ -777,7 +924,7 @@ void main() { expect(lastOptions, <String>['dingo', 'flamingo']); }); - testWidgets('can navigate options with the keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can navigate options with the keyboard', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -863,7 +1010,7 @@ void main() { expect(textEditingController.text, 'goose'); }); - testWidgets('can hide and show options with the keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can hide and show options with the keyboard', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -953,7 +1100,7 @@ void main() { expect(find.byKey(optionsKey), findsNothing); }); - testWidgets('re-invokes DismissIntent if options not shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('re-invokes DismissIntent if options not shown', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late FocusNode focusNode; @@ -1012,7 +1159,7 @@ void main() { expect(wrappingActionInvoked, true); }); - testWidgets('optionsViewBuilders can use AutocompleteHighlightedOption to highlight selected option', (WidgetTester tester) async { + testWidgetsWithLeakTracking('optionsViewBuilders can use AutocompleteHighlightedOption to highlight selected option', (WidgetTester tester) async { final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); late Iterable<String> lastOptions; @@ -1093,7 +1240,7 @@ void main() { expect(lastHighlighted, 5); }); - testWidgets('floating menu goes away on select', (WidgetTester tester) async { + testWidgetsWithLeakTracking('floating menu goes away on select', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99749. final GlobalKey fieldKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey(); diff --git a/packages/flutter/test/widgets/autofill_group_test.dart b/packages/flutter/test/widgets/autofill_group_test.dart index 9721fca8a588d..3ac6b860744ee 100644 --- a/packages/flutter/test/widgets/autofill_group_test.dart +++ b/packages/flutter/test/widgets/autofill_group_test.dart @@ -4,12 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final Matcher _matchesCommit = isMethodCall('TextInput.finishAutofillContext', arguments: true); final Matcher _matchesCancel = isMethodCall('TextInput.finishAutofillContext', arguments: false); void main() { - testWidgets('AutofillGroup has the right clients', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutofillGroup has the right clients', (WidgetTester tester) async { const Key outerKey = Key('outer'); const Key innerKey = Key('inner'); @@ -44,7 +45,7 @@ void main() { expect(innerState.autofillClients.toList(), <State<TextField>>[clientState2]); }); - testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('new clients can be added & removed to a scope', (WidgetTester tester) async { const Key scopeKey = Key('scope'); const TextField client1 = TextField(autofillHints: <String>['1']); @@ -92,7 +93,7 @@ void main() { expect(scopeState.autofillClients, <State<TextField>>[clientState1]); }); - testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async { const Key outerKey = Key('outer'); const Key innerKey = Key('inner'); final GlobalKey keyClient3 = GlobalKey(); @@ -151,7 +152,7 @@ void main() { expect(innerState.autofillClients, <State<TextField>>[clientState2]); }); - testWidgets('disposing AutofillGroups', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disposing AutofillGroups', (WidgetTester tester) async { late StateSetter setState; const Key group1 = Key('group1'); const Key group2 = Key('group2'); diff --git a/packages/flutter/test/widgets/automatic_keep_alive_test.dart b/packages/flutter/test/widgets/automatic_keep_alive_test.dart index eb2718fbd0543..089a827d02d5b 100644 --- a/packages/flutter/test/widgets/automatic_keep_alive_test.dart +++ b/packages/flutter/test/widgets/automatic_keep_alive_test.dart @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Leaf extends StatefulWidget { const Leaf({ required Key key, required this.child }) : super(key: key); @@ -20,7 +22,7 @@ class _LeafState extends State<Leaf> { @override void deactivate() { - _handle?.release(); + _handle?.dispose(); _handle = null; super.deactivate(); } @@ -33,7 +35,7 @@ class _LeafState extends State<Leaf> { KeepAliveNotification(_handle!).dispatch(context); } } else { - _handle?.release(); + _handle?.dispose(); _handle = null; } } @@ -66,7 +68,7 @@ List<Widget> generateList(Widget child, { required bool impliedMode }) { } void tests({ required bool impliedMode }) { - testWidgets('AutomaticKeepAlive with ListView with itemExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with ListView with itemExtent', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -114,7 +116,7 @@ void tests({ required bool impliedMode }) { expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing); }); - testWidgets('AutomaticKeepAlive with ListView without itemExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with ListView without itemExtent', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -164,7 +166,7 @@ void tests({ required bool impliedMode }) { expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing); }); - testWidgets('AutomaticKeepAlive with GridView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with GridView', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -221,7 +223,7 @@ void main() { group('Explicit automatic keep-alive', () { tests(impliedMode: false); }); group('Implied automatic keep-alive', () { tests(impliedMode: true); }); - testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive double', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -305,7 +307,7 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget); }); - testWidgets('AutomaticKeepAlive double 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive double 2', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -473,7 +475,7 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsNothing); }); - testWidgets('AutomaticKeepAlive with keepAlive set to true before initState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with keepAlive set to true before initState', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -508,7 +510,7 @@ void main() { expect(find.text('FooBar 2'), findsNothing); }); - testWidgets('AutomaticKeepAlive with keepAlive set to true before initState and widget goes out of scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with keepAlive set to true before initState and widget goes out of scope', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -547,19 +549,22 @@ void main() { expect(find.text('FooBar 73'), findsOneWidget); }); - testWidgets('AutomaticKeepAlive with SliverKeepAliveWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AutomaticKeepAlive with SliverKeepAliveWidget', (WidgetTester tester) async { // We're just doing a basic test here to make sure that the functionality of // RenderSliverWithKeepAliveMixin doesn't get regressed or deleted. As testing // the full functionality would be cumbersome. final RenderSliverMultiBoxAdaptorAlt alternate = RenderSliverMultiBoxAdaptorAlt(); + addTearDown(alternate.dispose); final RenderBox child = RenderBoxKeepAlive(); + addTearDown(child.dispose); alternate.insert(child); expect(alternate.children.length, 1); }); - testWidgets('Keep alive Listenable has its listener removed once called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keep alive Listenable has its listener removed once called', (WidgetTester tester) async { final LeakCheckerHandle handle = LeakCheckerHandle(); + addTearDown(handle.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -631,6 +636,12 @@ class RenderSliverMultiBoxAdaptorAlt extends RenderSliver with } class LeakCheckerHandle with ChangeNotifier { + LeakCheckerHandle() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + @override bool get hasListeners => super.hasListeners; } diff --git a/packages/flutter/test/widgets/backdrop_filter_test.dart b/packages/flutter/test/widgets/backdrop_filter_test.dart index a112fe9e666c8..9fc7e9cb4f384 100644 --- a/packages/flutter/test/widgets/backdrop_filter_test.dart +++ b/packages/flutter/test/widgets/backdrop_filter_test.dart @@ -11,9 +11,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets("Material2 - BackdropFilter's cull rect does not shrink", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Material2 - BackdropFilter's cull rect does not shrink", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -51,7 +52,7 @@ void main() { ); }); - testWidgets("Material3 - BackdropFilter's cull rect does not shrink", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Material3 - BackdropFilter's cull rect does not shrink", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), @@ -89,7 +90,7 @@ void main() { ); }); - testWidgets('Material2 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -148,7 +149,7 @@ void main() { ); }); - testWidgets('Material3 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BackdropFilter blendMode on saveLayer', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), diff --git a/packages/flutter/test/widgets/banner_test.dart b/packages/flutter/test/widgets/banner_test.dart index 06745df4af2af..ca3d32b9df068 100644 --- a/packages/flutter/test/widgets/banner_test.dart +++ b/packages/flutter/test/widgets/banner_test.dart @@ -6,8 +6,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestCanvas implements Canvas { final List<Invocation> invocations = <Invocation>[]; @@ -247,7 +246,7 @@ void main() { expect(rotateCommand.positionalArguments[0], equals(math.pi / 4.0)); }); - testWidgets('Banner widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Banner widget', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget( const Directionality( @@ -267,7 +266,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('Banner widget in MaterialApp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Banner widget in MaterialApp', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget(const MaterialApp(home: Placeholder())); expect(find.byType(CheckedModeBanner), paints diff --git a/packages/flutter/test/widgets/baseline_test.dart b/packages/flutter/test/widgets/baseline_test.dart index 707707fdc8fa4..912f639163330 100644 --- a/packages/flutter/test/widgets/baseline_test.dart +++ b/packages/flutter/test/widgets/baseline_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Baseline - control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Baseline - control test', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: DefaultTextStyle( @@ -20,7 +21,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.text('X')).size, const Size(100.0, 100.0)); }); - testWidgets('Baseline - position test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Baseline - position test', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: Baseline( @@ -43,7 +44,7 @@ void main() { ); }); - testWidgets('Chip caches baseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Chip caches baseline', (WidgetTester tester) async { int calls = 0; await tester.pumpWidget( MaterialApp( @@ -68,7 +69,7 @@ void main() { expect(calls, 2); }); - testWidgets('ListTile caches baseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListTile caches baseline', (WidgetTester tester) async { int calls = 0; await tester.pumpWidget( MaterialApp( @@ -93,7 +94,7 @@ void main() { expect(calls, 2); }); - testWidgets("LayoutBuilder returns child's baseline", (WidgetTester tester) async { + testWidgetsWithLeakTracking("LayoutBuilder returns child's baseline", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart index 5fb6bc82ef0b7..b3d97f690a7e9 100644 --- a/packages/flutter/test/widgets/basic_test.dart +++ b/packages/flutter/test/widgets/basic_test.dart @@ -15,12 +15,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group('RawImage', () { - testWidgets('properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properties', (WidgetTester tester) async { final ui.Image image1 = (await tester.runAsync<ui.Image>(() => createTestImage()))!; await tester.pumpWidget( @@ -110,7 +111,7 @@ void main() { }); group('PhysicalShape', () { - testWidgets('properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('properties', (WidgetTester tester) async { await tester.pumpWidget( const PhysicalShape( clipper: ShapeBorderClipper(shape: CircleBorder()), @@ -126,7 +127,7 @@ void main() { expect(renderObject.elevation, 2.0); }); - testWidgets('hit test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hit test', (WidgetTester tester) async { await tester.pumpWidget( PhysicalShape( clipper: const ShapeBorderClipper(shape: CircleBorder()), @@ -156,7 +157,7 @@ void main() { }); group('FractionalTranslation', () { - testWidgets('hit test - entirely inside the bounding box', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hit test - entirely inside the bounding box', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); bool pointerDown = false; @@ -185,7 +186,7 @@ void main() { expect(pointerDown, isTrue); }); - testWidgets('hit test - partially inside the bounding box', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hit test - partially inside the bounding box', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); bool pointerDown = false; @@ -214,7 +215,7 @@ void main() { expect(pointerDown, isTrue); }); - testWidgets('hit test - completely outside the bounding box', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hit test - completely outside the bounding box', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); bool pointerDown = false; @@ -243,7 +244,7 @@ void main() { expect(pointerDown, isTrue); }); - testWidgets('semantics bounds are updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantics bounds are updated', (WidgetTester tester) async { final GlobalKey fractionalTranslationKey = GlobalKey(); final GlobalKey textKey = GlobalKey(); Offset offset = const Offset(0.4, 0.4); @@ -307,7 +308,7 @@ void main() { }); group('Semantics', () { - testWidgets('Semantics can set attributed Text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics can set attributed Text', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -358,7 +359,7 @@ void main() { expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2)); }); - testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics can merge attributed strings', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -413,7 +414,7 @@ void main() { expect(attributedHint.attributes[1].range, const TextRange(start:6, end: 7)); }); - testWidgets('Semantics can merge attributed strings with non attributed string', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics can merge attributed strings with non attributed string', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -453,7 +454,7 @@ void main() { }); group('Row', () { - testWidgets('multiple baseline aligned children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiple baseline aligned children', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); // The point size of the font must be a multiple of 4 until @@ -507,7 +508,7 @@ void main() { expect(tester.getTopLeft(find.byKey(key2)).dy, aboveBaseline1 - aboveBaseline2); }); - testWidgets('baseline aligned children account for a larger, no-baseline child size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('baseline aligned children account for a larger, no-baseline child size', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/58898 final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); @@ -575,7 +576,7 @@ void main() { ); }); - testWidgets('UnconstrainedBox can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UnconstrainedBox can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(const UnconstrainedBox()); final RenderConstraintsTransformBox renderObject = tester.allRenderObjects.whereType<RenderConstraintsTransformBox>().first; expect(renderObject.clipBehavior, equals(Clip.none)); @@ -584,7 +585,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('UnconstrainedBox warns only when clipBehavior is Clip.none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UnconstrainedBox warns only when clipBehavior is Clip.none', (WidgetTester tester) async { for (final Clip? clip in <Clip?>[null, ...Clip.values]) { // Clear any render objects that were there before so that we can see more // than one error. Otherwise, it just throws the first one and skips the @@ -660,7 +661,7 @@ void main() { mockCanvas = mockContext.canvas; }); - testWidgets('ColoredBox - no size, no child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ColoredBox - no size, no child', (WidgetTester tester) async { await tester.pumpWidget(const Flex( direction: Axis.horizontal, textDirection: TextDirection.ltr, @@ -681,7 +682,7 @@ void main() { expect(mockContext.offsets, isEmpty); }); - testWidgets('ColoredBox - no size, child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ColoredBox - no size, child', (WidgetTester tester) async { const ValueKey<int> key = ValueKey<int>(0); const Widget child = SizedBox.expand(key: key); await tester.pumpWidget(const Flex( @@ -705,7 +706,7 @@ void main() { expect(mockContext.offsets.single, Offset.zero); }); - testWidgets('ColoredBox - size, no child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ColoredBox - size, no child', (WidgetTester tester) async { await tester.pumpWidget(const ColoredBox(color: colorToPaint)); expect(find.byType(ColoredBox), findsOneWidget); final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox)); @@ -718,7 +719,7 @@ void main() { expect(mockContext.offsets, isEmpty); }); - testWidgets('ColoredBox - size, child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ColoredBox - size, child', (WidgetTester tester) async { const ValueKey<int> key = ValueKey<int>(0); const Widget child = SizedBox.expand(key: key); await tester.pumpWidget(const ColoredBox(color: colorToPaint, child: child)); @@ -734,7 +735,7 @@ void main() { expect(mockContext.offsets.single, Offset.zero); }); - testWidgets('ColoredBox - debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ColoredBox - debugFillProperties', (WidgetTester tester) async { const ColoredBox box = ColoredBox(color: colorToPaint); final DiagnosticPropertiesBuilder properties = DiagnosticPropertiesBuilder(); box.debugFillProperties(properties); @@ -742,7 +743,7 @@ void main() { expect(properties.properties.first.value, colorToPaint); }); }); - testWidgets('Inconsequential golden test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inconsequential golden test', (WidgetTester tester) async { // The test validates the Flutter Gold integration. Any changes to the // golden file can be approved at any time. await tester.pumpWidget(RepaintBoundary( @@ -758,7 +759,7 @@ void main() { ); }); - testWidgets('IgnorePointer ignores pointers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IgnorePointer ignores pointers', (WidgetTester tester) async { final List<String> logs = <String>[]; Widget target({required bool ignoring}) => Align( alignment: Alignment.topLeft, @@ -836,7 +837,7 @@ void main() { }); group('IgnorePointer semantics', () { - testWidgets('does not change semantics when not ignoring', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not change semantics when not ignoring', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -863,7 +864,7 @@ void main() { ); }); - testWidgets('can toggle the ignoring.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can toggle the ignoring.', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); final UniqueKey key3 = UniqueKey(); @@ -982,7 +983,7 @@ void main() { ); }); - testWidgets('drops semantics when its ignoringSemantics is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drops semantics when its ignoringSemantics is true', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final UniqueKey key = UniqueKey(); await tester.pumpWidget( @@ -1001,7 +1002,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignores user interactions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores user interactions', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1028,7 +1029,7 @@ void main() { }); }); - testWidgets('AbsorbPointer absorbs pointers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AbsorbPointer absorbs pointers', (WidgetTester tester) async { final List<String> logs = <String>[]; Widget target({required bool absorbing}) => Align( alignment: Alignment.topLeft, @@ -1105,7 +1106,7 @@ void main() { logs.clear(); }); - testWidgets('Wrap implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Wrap( spacing: 8.0, // gap between adjacent Text widget @@ -1135,6 +1136,62 @@ void main() { contains('verticalDirection: up'), ])); }); + + testWidgetsWithLeakTracking('Row and IgnoreBaseline (control -- with baseline)', (WidgetTester tester) async { + await tester.pumpWidget( + const Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + textDirection: TextDirection.ltr, + children: <Widget>[ + Text( + 'a', + textDirection: TextDirection.ltr, + style: TextStyle(fontSize: 128.0, fontFamily: 'FlutterTest'), // places baseline at y=96 + ), + Text( + 'b', + textDirection: TextDirection.ltr, + style: TextStyle(fontSize: 32.0, fontFamily: 'FlutterTest'), // 24 above baseline, 8 below baseline + ), + ], + ), + ); + + final Offset aPos = tester.getTopLeft(find.text('a')); + final Offset bPos = tester.getTopLeft(find.text('b')); + expect(aPos.dy, 0.0); + expect(bPos.dy, 96.0 - 24.0); + }); + + testWidgetsWithLeakTracking('Row and IgnoreBaseline (with ignored baseline)', (WidgetTester tester) async { + await tester.pumpWidget( + const Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + textDirection: TextDirection.ltr, + children: <Widget>[ + IgnoreBaseline( + child: Text( + 'a', + textDirection: TextDirection.ltr, + style: TextStyle(fontSize: 128.0, fontFamily: 'FlutterTest'), // places baseline at y=96 + ), + ), + Text( + 'b', + textDirection: TextDirection.ltr, + style: TextStyle(fontSize: 32.0, fontFamily: 'FlutterTest'), // 24 above baseline, 8 below baseline + ), + ], + ), + ); + + final Offset aPos = tester.getTopLeft(find.text('a')); + final Offset bPos = tester.getTopLeft(find.text('b')); + expect(aPos.dy, 0.0); + expect(bPos.dy, 0.0); + }); } HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox); diff --git a/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart index dc7208c5dd17a..afac6ff2a4894 100644 --- a/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart +++ b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart @@ -8,12 +8,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const String _actualContent = 'Actual Content'; const String _loading = 'Loading...'; void main() { - testWidgets('deferFirstFrame/allowFirstFrame stops sending frames to engine', (WidgetTester tester) async { + testWidgetsWithLeakTracking('deferFirstFrame/allowFirstFrame stops sending frames to engine', (WidgetTester tester) async { expect(RendererBinding.instance.sendFramesToEngine, isTrue); final Completer<void> completer = Completer<void>(); @@ -50,7 +51,7 @@ void main() { expect(RendererBinding.instance.sendFramesToEngine, isTrue); }); - testWidgets('Two widgets can defer frames', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Two widgets can defer frames', (WidgetTester tester) async { expect(RendererBinding.instance.sendFramesToEngine, isTrue); final Completer<void> completer1 = Completer<void>(); diff --git a/packages/flutter/test/widgets/binding_test.dart b/packages/flutter/test/widgets/binding_test.dart index 928fc1fa948c2..d0ded5fb100d3 100644 --- a/packages/flutter/test/widgets/binding_test.dart +++ b/packages/flutter/test/widgets/binding_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class MemoryPressureObserver with WidgetsBindingObserver { bool sawMemoryPressure = false; @@ -45,6 +48,94 @@ class PushRouteInformationObserver with WidgetsBindingObserver { } } +// Implements to make sure all methods get coverage. +class RentrantObserver implements WidgetsBindingObserver { + RentrantObserver() { + WidgetsBinding.instance.addObserver(this); + } + + bool active = true; + + int removeSelf() { + active = false; + int count = 0; + while (WidgetsBinding.instance.removeObserver(this)) { + count += 1; + } + return count; + } + + @override + void didChangeAccessibilityFeatures() { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeLocales(List<Locale>? locales) { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeMetrics() { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangePlatformBrightness() { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeTextScaleFactor() { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didHaveMemoryPressure() { + assert(active); + WidgetsBinding.instance.addObserver(this); + } + + @override + Future<bool> didPopRoute() { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future<bool>.value(true); + } + + @override + Future<bool> didPushRoute(String route) { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future<bool>.value(true); + } + + @override + Future<bool> didPushRouteInformation(RouteInformation routeInformation) { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future<bool>.value(true); + } + + @override + Future<AppExitResponse> didRequestAppExit() { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future<AppExitResponse>.value(AppExitResponse.exit); + } +} + void main() { Future<void> setAppLifeCycleState(AppLifecycleState state) async { final ByteData? message = @@ -53,7 +144,24 @@ void main() { .handlePlatformMessage('flutter/lifecycle', message, (_) { }); } - testWidgets('didHaveMemoryPressure callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rentrant observer callbacks do not result in exceptions', (WidgetTester tester) async { + final RentrantObserver observer = RentrantObserver(); + WidgetsBinding.instance.handleAccessibilityFeaturesChanged(); + WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + WidgetsBinding.instance.handleLocaleChanged(); + WidgetsBinding.instance.handleMetricsChanged(); + WidgetsBinding.instance.handlePlatformBrightnessChanged(); + WidgetsBinding.instance.handleTextScaleFactorChanged(); + WidgetsBinding.instance.handleMemoryPressure(); + WidgetsBinding.instance.handlePopRoute(); + WidgetsBinding.instance.handlePushRoute('/'); + WidgetsBinding.instance.handleRequestAppExit(); + await tester.idle(); + expect(observer.removeSelf(), greaterThan(1)); + expect(observer.removeSelf(), 0); + }); + + testWidgetsWithLeakTracking('didHaveMemoryPressure callback', (WidgetTester tester) async { final MemoryPressureObserver observer = MemoryPressureObserver(); WidgetsBinding.instance.addObserver(observer); final ByteData message = const JSONMessageCodec().encodeMessage(<String, dynamic>{'type': 'memoryPressure'})!; @@ -62,7 +170,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('handleLifecycleStateChanged callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handleLifecycleStateChanged callback', (WidgetTester tester) async { final AppLifecycleStateObserver observer = AppLifecycleStateObserver(); WidgetsBinding.instance.addObserver(observer); @@ -118,9 +226,10 @@ void main() { observer.accumulatedStates.clear(); await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError); + WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRoute callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRoute callback', (WidgetTester tester) async { final PushRouteObserver observer = PushRouteObserver(); WidgetsBinding.instance.addObserver(observer); @@ -132,7 +241,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async { final PushRouteObserver observer = PushRouteObserver(); WidgetsBinding.instance.addObserver(observer); @@ -150,7 +259,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRouteInformation calls didPushRoute correctly when handling url', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRouteInformation calls didPushRoute correctly when handling url', (WidgetTester tester) async { final PushRouteObserver observer = PushRouteObserver(); WidgetsBinding.instance.addObserver(observer); @@ -182,7 +291,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRouteInformation callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRouteInformation callback', (WidgetTester tester) async { final PushRouteInformationObserver observer = PushRouteInformationObserver(); WidgetsBinding.instance.addObserver(observer); @@ -199,7 +308,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRouteInformation callback can handle url', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRouteInformation callback can handle url', (WidgetTester tester) async { final PushRouteInformationObserver observer = PushRouteInformationObserver(); WidgetsBinding.instance.addObserver(observer); @@ -217,7 +326,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('didPushRouteInformation callback with null state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didPushRouteInformation callback with null state', (WidgetTester tester) async { final PushRouteInformationObserver observer = PushRouteInformationObserver(); WidgetsBinding.instance.addObserver(observer); @@ -235,7 +344,7 @@ void main() { WidgetsBinding.instance.removeObserver(observer); }); - testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Application lifecycle affects frame scheduling', (WidgetTester tester) async { expect(tester.binding.hasScheduledFrame, isFalse); await setAppLifeCycleState(AppLifecycleState.paused); @@ -289,7 +398,7 @@ void main() { await tester.pump(); }); - testWidgets('scheduleFrameCallback error control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scheduleFrameCallback error control test', (WidgetTester tester) async { late FlutterError error; try { tester.binding.scheduleFrameCallback((Duration _) { }, rescheduling: true); @@ -321,7 +430,7 @@ void main() { ); }); - testWidgets('defaultStackFilter elides framework Element mounting stacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('defaultStackFilter elides framework Element mounting stacks', (WidgetTester tester) async { final FlutterExceptionHandler? oldHandler = FlutterError.onError; late FlutterErrorDetails errorDetails; FlutterError.onError = (FlutterErrorDetails details) { diff --git a/packages/flutter/test/widgets/box_decoration_test.dart b/packages/flutter/test/widgets/box_decoration_test.dart index 0d9d4e1e95e03..f21a9b33118b5 100644 --- a/packages/flutter/test/widgets/box_decoration_test.dart +++ b/packages/flutter/test/widgets/box_decoration_test.dart @@ -9,9 +9,9 @@ import 'dart:ui' as ui show Image; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; -import '../rendering/mock_canvas.dart'; class TestImageProvider extends ImageProvider<TestImageProvider> { TestImageProvider(this.future); @@ -26,7 +26,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { } @override - ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(TestImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( future.then<ImageInfo>((void value) => ImageInfo(image: image)), ); @@ -37,7 +37,7 @@ Future<void> main() async { AutomatedTestWidgetsFlutterBinding(); TestImageProvider.image = await decodeImageFromList(Uint8List.fromList(kTransparentImage)); - testWidgets('DecoratedBox handles loading images', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedBox handles loading images', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final Completer<void> completer = Completer<void>(); await tester.pumpWidget( @@ -60,7 +60,7 @@ Future<void> main() async { expect(tester.binding.hasScheduledFrame, isFalse); }); - testWidgets('Moving a DecoratedBox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving a DecoratedBox', (WidgetTester tester) async { final Completer<void> completer = Completer<void>(); final Widget subtree = KeyedSubtree( key: GlobalKey(), @@ -89,7 +89,7 @@ Future<void> main() async { expect(tester.binding.hasScheduledFrame, isFalse); }); - testWidgets('Circles can have uniform borders', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Circles can have uniform borders', (WidgetTester tester) async { await tester.pumpWidget( Container( padding: const EdgeInsets.all(50.0), @@ -102,7 +102,7 @@ Future<void> main() async { ); }); - testWidgets('Bordered Container insets its child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bordered Container insets its child', (WidgetTester tester) async { const Key key = Key('outerContainer'); await tester.pumpWidget( Center( @@ -119,7 +119,7 @@ Future<void> main() async { expect(tester.getSize(find.byKey(key)), equals(const Size(45.0, 45.0))); }); - testWidgets('BoxDecoration paints its border correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxDecoration paints its border correctly', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/7672 const Key key = Key('Container with BoxDecoration'); @@ -170,7 +170,7 @@ Future<void> main() async { ); }); - testWidgets('BoxDecoration paints its border correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxDecoration paints its border correctly', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/12165 await tester.pumpWidget( Column( @@ -255,7 +255,7 @@ Future<void> main() async { ); }); - testWidgets('Can hit test on BoxDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hit test on BoxDecoration', (WidgetTester tester) async { late List<int> itemsTapped; @@ -292,7 +292,7 @@ Future<void> main() async { }); - testWidgets('Can hit test on BoxDecoration circle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hit test on BoxDecoration circle', (WidgetTester tester) async { late List<int> itemsTapped; @@ -332,7 +332,7 @@ Future<void> main() async { }); - testWidgets('Can hit test on BoxDecoration border', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hit test on BoxDecoration border', (WidgetTester tester) async { late List<int> itemsTapped; const Key key = Key('Container with BoxDecoration'); Widget buildFrame(Border border) { @@ -370,7 +370,7 @@ Future<void> main() async { expect(itemsTapped, <int>[1,1]); }); - testWidgets('BoxDecoration not tap outside rounded angles - Top Left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxDecoration not tap outside rounded angles - Top Left', (WidgetTester tester) async { const double height = 50.0; const double width = 50.0; const double radius = 12.3; @@ -430,7 +430,7 @@ Future<void> main() async { }); - testWidgets('BoxDecoration tap inside rounded angles - Top Left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxDecoration tap inside rounded angles - Top Left', (WidgetTester tester) async { const double height = 50.0; const double width = 50.0; const double radius = 12.3; @@ -478,7 +478,7 @@ Future<void> main() async { expect(itemsTapped, <int>[1,1,1,1]); }); - testWidgets('BoxDecoration rounded angles other corner works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxDecoration rounded angles other corner works', (WidgetTester tester) async { const double height = 50.0; const double width = 50.0; const double radius = 20; @@ -546,7 +546,7 @@ Future<void> main() async { expect(itemsTapped, <int>[1,1,1,1,1], reason: 'top left tapped'); }); - testWidgets("BoxDecoration doesn't crash with BorderRadiusDirectional", (WidgetTester tester) async { + testWidgetsWithLeakTracking("BoxDecoration doesn't crash with BorderRadiusDirectional", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/88039 await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/build_context_test.dart b/packages/flutter/test/widgets/build_context_test.dart index f6b78aa8b5c69..77650f640a957 100644 --- a/packages/flutter/test/widgets/build_context_test.dart +++ b/packages/flutter/test/widgets/build_context_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('StatefulWidget BuildContext.mounted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StatefulWidget BuildContext.mounted', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget(TestStatefulWidget( onBuild: (BuildContext context) { @@ -18,7 +19,7 @@ void main() { expect(capturedContext.mounted, isFalse); }); - testWidgets('StatelessWidget BuildContext.mounted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StatelessWidget BuildContext.mounted', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget(TestStatelessWidget( onBuild: (BuildContext context) { diff --git a/packages/flutter/test/widgets/build_scope_test.dart b/packages/flutter/test/widgets/build_scope_test.dart index 8fb7a786c31b2..8cb0802da9da6 100644 --- a/packages/flutter/test/widgets/build_scope_test.dart +++ b/packages/flutter/test/widgets/build_scope_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -133,7 +134,7 @@ class Wrapper extends StatelessWidget { } void main() { - testWidgets('Legal times for setState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Legal times for setState', (WidgetTester tester) async { final GlobalKey flipKey = GlobalKey(); expect(ProbeWidgetState.buildCount, equals(0)); await tester.pumpWidget(const ProbeWidget(key: Key('a'))); @@ -171,7 +172,7 @@ void main() { expect(tester.takeException(), isNotNull); }); - testWidgets('Dirty element list sort order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dirty element list sort order', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: 'key1'); final GlobalKey key2 = GlobalKey(debugLabel: 'key2'); diff --git a/packages/flutter/test/widgets/center_test.dart b/packages/flutter/test/widgets/center_test.dart index 5d32e3deb46fd..6cdb8d504eefb 100644 --- a/packages/flutter/test/widgets/center_test.dart +++ b/packages/flutter/test/widgets/center_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can be placed in an infinite box', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be placed in an infinite box', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/clamp_overscrolls_test.dart b/packages/flutter/test/widgets/clamp_overscrolls_test.dart index 03457a9dd8bb4..856648f0aa095 100644 --- a/packages/flutter/test/widgets/clamp_overscrolls_test.dart +++ b/packages/flutter/test/widgets/clamp_overscrolls_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // Assuming that the test container is 800x600. The height of the // viewport's contents is 650.0, the top and bottom text children @@ -32,7 +33,7 @@ Widget buildFrame(ScrollPhysics physics, { ScrollController? scrollController }) } void main() { - testWidgets('ClampingScrollPhysics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClampingScrollPhysics', (WidgetTester tester) async { // Scroll the target text widget by offset and then return its origin // in global coordinates. @@ -59,7 +60,7 @@ void main() { expect(origin.dy, equals(500.0)); }); - testWidgets('ClampingScrollPhysics affects ScrollPosition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClampingScrollPhysics affects ScrollPosition', (WidgetTester tester) async { // BouncingScrollPhysics @@ -91,9 +92,10 @@ void main() { expect(scrollable.position.pixels, equals(50.0)); }); - testWidgets('ClampingScrollPhysics handles out of bounds ScrollPosition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClampingScrollPhysics handles out of bounds ScrollPosition', (WidgetTester tester) async { Future<void> testOutOfBounds(ScrollPhysics physics, double initialOffset, double expectedOffset) async { final ScrollController scrollController = ScrollController(initialScrollOffset: initialOffset); + addTearDown(scrollController.dispose); await tester.pumpWidget(buildFrame(physics, scrollController: scrollController)); final ScrollableState scrollable = tester.state(find.byType(Scrollable)); diff --git a/packages/flutter/test/widgets/clip_test.dart b/packages/flutter/test/widgets/clip_test.dart index 1a50011f61c8f..b9e3e64b90fe4 100644 --- a/packages/flutter/test/widgets/clip_test.dart +++ b/packages/flutter/test/widgets/clip_test.dart @@ -11,8 +11,8 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'test_border.dart' show TestBorder; final List<String> log = <String>[]; @@ -59,7 +59,7 @@ class NotifyClipper<T> extends CustomClipper<T> { } void main() { - testWidgets('ClipRect with a FittedBox child sized to zero works with semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRect with a FittedBox child sized to zero works with semantics', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ClipRect( @@ -78,7 +78,7 @@ void main() { expect(find.byType(FittedBox), findsOneWidget); }); - testWidgets('ClipRect updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRect updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget(const ClipRect()); final RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first; @@ -100,7 +100,7 @@ void main() { expect(clipRRect.borderRadius, equals(BorderRadius.zero)); }); - testWidgets('ClipRRect updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRRect updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget(const ClipRRect()); final RenderClipRRect renderClip = tester.allRenderObjects.whereType<RenderClipRRect>().first; @@ -116,7 +116,7 @@ void main() { expect(renderClip.clipBehavior, equals(Clip.none)); }); - testWidgets('ClipOval updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipOval updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget(const ClipOval()); final RenderClipOval renderClip = tester.allRenderObjects.whereType<RenderClipOval>().first; @@ -132,7 +132,7 @@ void main() { expect(renderClip.clipBehavior, equals(Clip.none)); }); - testWidgets('ClipPath updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipPath updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget(const ClipPath()); final RenderClipPath renderClip = tester.allRenderObjects.whereType<RenderClipPath>().first; @@ -148,7 +148,7 @@ void main() { expect(renderClip.clipBehavior, equals(Clip.none)); }); - testWidgets('ClipPath', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipPath', (WidgetTester tester) async { await tester.pumpWidget( ClipPath( clipper: PathClipper(), @@ -169,7 +169,7 @@ void main() { log.clear(); }); - testWidgets('ClipOval', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipOval', (WidgetTester tester) async { await tester.pumpWidget( ClipOval( child: GestureDetector( @@ -189,7 +189,7 @@ void main() { log.clear(); }); - testWidgets('Transparent ClipOval hit test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Transparent ClipOval hit test', (WidgetTester tester) async { await tester.pumpWidget( Opacity( opacity: 0.0, @@ -212,7 +212,7 @@ void main() { log.clear(); }); - testWidgets('ClipRect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRect', (WidgetTester tester) async { await tester.pumpWidget( Align( alignment: Alignment.topLeft, @@ -335,7 +335,7 @@ void main() { log.clear(); }); - testWidgets('debugPaintSizeEnabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugPaintSizeEnabled', (WidgetTester tester) async { await tester.pumpWidget( const ClipRect( child: Placeholder(), @@ -357,7 +357,7 @@ void main() { debugPaintSizeEnabled = false; }); - testWidgets('ClipRect painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRect painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -400,7 +400,7 @@ void main() { ); }); - testWidgets('ClipRect save, overlay, and antialiasing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRect save, overlay, and antialiasing', (WidgetTester tester) async { await tester.pumpWidget( RepaintBoundary( child: Stack( @@ -439,7 +439,7 @@ void main() { ); }); - testWidgets('ClipRRect painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRRect painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -488,7 +488,7 @@ void main() { ); }); - testWidgets('ClipOval painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipOval painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -531,7 +531,7 @@ void main() { ); }); - testWidgets('ClipPath painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipPath painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -616,7 +616,7 @@ void main() { ); } - testWidgets('PhysicalModel painting with Clip.antiAlias', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalModel painting with Clip.antiAlias', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalModel(Clip.antiAlias)); await expectLater( find.byType(RepaintBoundary).first, @@ -624,7 +624,7 @@ void main() { ); }); - testWidgets('PhysicalModel painting with Clip.hardEdge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalModel painting with Clip.hardEdge', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalModel(Clip.hardEdge)); await expectLater( find.byType(RepaintBoundary).first, @@ -634,7 +634,7 @@ void main() { // There will be bleeding edges on the rect edges, but there shouldn't be any bleeding edges on the // round corners. - testWidgets('PhysicalModel painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalModel painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalModel(Clip.antiAliasWithSaveLayer)); await expectLater( find.byType(RepaintBoundary).first, @@ -642,7 +642,7 @@ void main() { ); }); - testWidgets('Default PhysicalModel painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default PhysicalModel painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -725,7 +725,7 @@ void main() { ); } - testWidgets('PhysicalShape painting with Clip.antiAlias', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalShape painting with Clip.antiAlias', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalShape(Clip.antiAlias)); await expectLater( find.byType(RepaintBoundary).first, @@ -733,7 +733,7 @@ void main() { ); }); - testWidgets('PhysicalShape painting with Clip.hardEdge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalShape painting with Clip.hardEdge', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalShape(Clip.hardEdge)); await expectLater( find.byType(RepaintBoundary).first, @@ -741,7 +741,7 @@ void main() { ); }); - testWidgets('PhysicalShape painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalShape painting with Clip.antiAliasWithSaveLayer', (WidgetTester tester) async { await tester.pumpWidget(genPhysicalShape(Clip.antiAliasWithSaveLayer)); await expectLater( find.byType(RepaintBoundary).first, @@ -749,7 +749,7 @@ void main() { ); }); - testWidgets('PhysicalShape painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalShape painting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -795,7 +795,7 @@ void main() { ); }); - testWidgets('ClipPath.shape', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipPath.shape', (WidgetTester tester) async { final List<String> logs = <String>[]; final ShapeBorder shape = TestBorder((String message) { logs.add(message); }); Widget buildClipPath() { @@ -852,8 +852,9 @@ void main() { ]); }); - testWidgets('CustomClipper reclips when notified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomClipper reclips when notified', (WidgetTester tester) async { final ValueNotifier<Rect> clip = ValueNotifier<Rect>(const Rect.fromLTWH(50.0, 50.0, 100.0, 100.0)); + addTearDown(clip.dispose); await tester.pumpWidget( ClipRect( @@ -885,7 +886,7 @@ void main() { ); }); - testWidgets('ClipRRect supports BorderRadiusDirectional', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRRect supports BorderRadiusDirectional', (WidgetTester tester) async { const Radius startRadius = Radius.circular(15.0); const Radius endRadius = Radius.circular(30.0); @@ -912,7 +913,7 @@ void main() { } }); - testWidgets('ClipRRect is direction-aware', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ClipRRect is direction-aware', (WidgetTester tester) async { const Radius startRadius = Radius.circular(15.0); const Radius endRadius = Radius.circular(30.0); TextDirection textDirection = TextDirection.ltr; diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart index 3135844c03db0..fa512f682b672 100644 --- a/packages/flutter/test/widgets/color_filter_test.dart +++ b/packages/flutter/test/widgets/color_filter_test.dart @@ -11,9 +11,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Color filter - red', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Color filter - red', (WidgetTester tester) async { await tester.pumpWidget( const RepaintBoundary( child: ColorFiltered( @@ -28,7 +29,7 @@ void main() { ); }); - testWidgets('Color filter - sepia', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Color filter - sepia', (WidgetTester tester) async { const ColorFilter sepia = ColorFilter.matrix(<double>[ 0.39, 0.769, 0.189, 0, 0, // 0.349, 0.686, 0.168, 0, 0, // @@ -65,7 +66,7 @@ void main() { ); }); - testWidgets('Color filter - reuses its layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Color filter - reuses its layer', (WidgetTester tester) async { Future<void> pumpWithColor(Color color) async { await tester.pumpWidget( RepaintBoundary( diff --git a/packages/flutter/test/widgets/column_test.dart b/packages/flutter/test/widgets/column_test.dart index 7f86f285e9f45..1b45f093d1477 100644 --- a/packages/flutter/test/widgets/column_test.dart +++ b/packages/flutter/test/widgets/column_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // DOWN (default) - testWidgets('Column with one flexible child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with one flexible child', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -55,7 +56,7 @@ void main() { expect(boxParentData.offset.dy, equals(500.0)); }); - testWidgets('Column with default main axis parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with default main axis parameters', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -101,7 +102,7 @@ void main() { expect(boxParentData.offset.dy, equals(200.0)); }); - testWidgets('Column with MainAxisAlignment.center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.center', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -139,7 +140,7 @@ void main() { expect(boxParentData.offset.dy, equals(300.0)); }); - testWidgets('Column with MainAxisAlignment.end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.end', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -185,7 +186,7 @@ void main() { expect(boxParentData.offset.dy, equals(500.0)); }); - testWidgets('Column with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -231,7 +232,7 @@ void main() { expect(boxParentData.offset.dy, equals(500.0)); }); - testWidgets('Column with MainAxisAlignment.spaceAround', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceAround', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -285,7 +286,7 @@ void main() { expect(boxParentData.offset.dy, equals(475.0)); }); - testWidgets('Column with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -331,7 +332,7 @@ void main() { expect(boxParentData.offset.dy, equals(445.0)); }); - testWidgets('Column and MainAxisSize.min', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column and MainAxisSize.min', (WidgetTester tester) async { const Key flexKey = Key('flexKey'); // Default is MainAxisSize.max so the Column should be as high as the test: 600. @@ -364,7 +365,7 @@ void main() { expect(renderBox.size.height, equals(250.0)); }); - testWidgets('Column MainAxisSize.min layout at zero size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column MainAxisSize.min layout at zero size', (WidgetTester tester) async { const Key childKey = Key('childKey'); await tester.pumpWidget(const Center( @@ -390,7 +391,7 @@ void main() { // UP - testWidgets('Column with one flexible child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with one flexible child', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -437,7 +438,7 @@ void main() { expect(boxParentData.offset.dy, equals(0.0)); }); - testWidgets('Column with default main axis parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with default main axis parameters', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -484,7 +485,7 @@ void main() { expect(boxParentData.offset.dy, equals(300.0)); }); - testWidgets('Column with MainAxisAlignment.center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.center', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -523,7 +524,7 @@ void main() { expect(boxParentData.offset.dy, equals(200.0)); }); - testWidgets('Column with MainAxisAlignment.end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.end', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -570,7 +571,7 @@ void main() { expect(boxParentData.offset.dy, equals(0.0)); }); - testWidgets('Column with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceBetween', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -617,7 +618,7 @@ void main() { expect(boxParentData.offset.dy, equals(0.0)); }); - testWidgets('Column with MainAxisAlignment.spaceAround', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceAround', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -672,7 +673,7 @@ void main() { expect(boxParentData.offset.dy, equals(500.0 - 475.0)); }); - testWidgets('Column with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column with MainAxisAlignment.spaceEvenly', (WidgetTester tester) async { const Key columnKey = Key('column'); const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -719,7 +720,7 @@ void main() { expect(boxParentData.offset.dy, equals(600.0 - 445.0 - 20.0)); }); - testWidgets('Column and MainAxisSize.min', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column and MainAxisSize.min', (WidgetTester tester) async { const Key flexKey = Key('flexKey'); // Default is MainAxisSize.max so the Column should be as high as the test: 600. @@ -754,7 +755,7 @@ void main() { expect(renderBox.size.height, equals(250.0)); }); - testWidgets('Column MainAxisSize.min layout at zero size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Column MainAxisSize.min layout at zero size', (WidgetTester tester) async { const Key childKey = Key('childKey'); await tester.pumpWidget(const Center( diff --git a/packages/flutter/test/widgets/composited_transform_test.dart b/packages/flutter/test/widgets/composited_transform_test.dart index f46294f3046e8..319702c277c94 100644 --- a/packages/flutter/test/widgets/composited_transform_test.dart +++ b/packages/flutter/test/widgets/composited_transform_test.dart @@ -7,11 +7,12 @@ import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final LayerLink link = LayerLink(); - testWidgets('Change link during layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Change link during layout', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); Widget build({ LayerLink? linkToUse }) { return Directionality( @@ -56,7 +57,7 @@ void main() { expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0)); }); - testWidgets('LeaderLayer should not cause error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LeaderLayer should not cause error', (WidgetTester tester) async { final LayerLink link = LayerLink(); Widget buildWidget({ @@ -116,19 +117,19 @@ void main() { ); } - testWidgets('topLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft)); final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox; expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0)); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center)); final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox; expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0)); }); - testWidgets('bottomRight - topRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight)); final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox; expect(box.localToGlobal(Offset.zero), const Offset(113.0, 466.0)); @@ -172,7 +173,7 @@ void main() { ), ); } - testWidgets('topLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -181,7 +182,7 @@ void main() { expect(position1, offsetMoreOrLessEquals(position2)); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -190,7 +191,7 @@ void main() { expect(position1, offsetMoreOrLessEquals(position2)); }); - testWidgets('bottomRight - topRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -249,7 +250,7 @@ void main() { ), ); } - testWidgets('topLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -258,7 +259,7 @@ void main() { expect(position1, offsetMoreOrLessEquals(position2)); }); - testWidgets('center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('center', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -267,7 +268,7 @@ void main() { expect(position1, offsetMoreOrLessEquals(position2)); }); - testWidgets('bottomRight - topRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async { await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight)); final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox; final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; @@ -321,7 +322,7 @@ void main() { for (final Alignment targetAlignment in alignments) { for (final Alignment followerAlignment in alignments) { - testWidgets('$targetAlignment - $followerAlignment', (WidgetTester tester) async{ + testWidgetsWithLeakTracking('$targetAlignment - $followerAlignment', (WidgetTester tester) async{ await tester.pumpWidget(build(targetAlignment: targetAlignment, followerAlignment: followerAlignment)); final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox; expect(box2.size, const Size(2.0, 2.0)); @@ -333,7 +334,7 @@ void main() { } }); - testWidgets('Leader after Follower asserts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leader after Follower asserts', (WidgetTester tester) async { final LayerLink link = LayerLink(); await tester.pumpWidget( CompositedTransformFollower( @@ -351,7 +352,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( '`FollowerLayer` (`CompositedTransformFollower`) has null pointer error when using with some kinds of `Layer`s', (WidgetTester tester) async { final LayerLink link = LayerLink(); diff --git a/packages/flutter/test/widgets/constrained_box_test.dart b/packages/flutter/test/widgets/constrained_box_test.dart index 26d7e51df9fd3..b09d9e9a934cc 100644 --- a/packages/flutter/test/widgets/constrained_box_test.dart +++ b/packages/flutter/test/widgets/constrained_box_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Placeholder intrinsics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Placeholder intrinsics', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); expect(tester.renderObject<RenderBox>(find.byType(Placeholder)).getMinIntrinsicWidth(double.infinity), 0.0); expect(tester.renderObject<RenderBox>(find.byType(Placeholder)).getMaxIntrinsicWidth(double.infinity), 0.0); @@ -14,7 +15,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(Placeholder)).getMaxIntrinsicHeight(double.infinity), 0.0); }); - testWidgets('ConstrainedBox intrinsics - minHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - minHeight', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -29,7 +30,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 20.0); }); - testWidgets('ConstrainedBox intrinsics - minWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - minWidth', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -44,7 +45,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 0.0); }); - testWidgets('ConstrainedBox intrinsics - maxHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - maxHeight', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -59,7 +60,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 0.0); }); - testWidgets('ConstrainedBox intrinsics - maxWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - maxWidth', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -74,7 +75,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 0.0); }); - testWidgets('ConstrainedBox intrinsics - tight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - tight', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 10.0, height: 30.0), @@ -88,7 +89,7 @@ void main() { }); - testWidgets('ConstrainedBox intrinsics - minHeight - with infinite width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - minHeight - with infinite width', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -104,7 +105,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 20.0); }); - testWidgets('ConstrainedBox intrinsics - minWidth - with infinite height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - minWidth - with infinite height', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints( @@ -120,7 +121,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(ConstrainedBox)).getMaxIntrinsicHeight(double.infinity), 0.0); }); - testWidgets('ConstrainedBox intrinsics - infinite', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ConstrainedBox intrinsics - infinite', (WidgetTester tester) async { await tester.pumpWidget( ConstrainedBox( constraints: const BoxConstraints.tightFor(width: double.infinity, height: double.infinity), diff --git a/packages/flutter/test/widgets/container_test.dart b/packages/flutter/test/widgets/container_test.dart index 7c327bc975466..19492ee751864 100644 --- a/packages/flutter/test/widgets/container_test.dart +++ b/packages/flutter/test/widgets/container_test.dart @@ -9,8 +9,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('Container control tests:', () { @@ -39,7 +38,7 @@ void main() { ), ); - testWidgets('paints as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('paints as expected', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -56,7 +55,7 @@ void main() { }); group('diagnostics', () { - testWidgets('has reasonable default diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has reasonable default diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -68,7 +67,7 @@ void main() { expect(box, hasAGoodToStringDeep); }); - testWidgets('has expected info diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected info diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -140,7 +139,7 @@ void main() { ); }); - testWidgets('has expected debug diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected debug diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -156,8 +155,9 @@ void main() { equalsIgnoringHashCodes( 'RenderPadding#00000 relayoutBoundary=up1\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← [root]\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ size: Size(63.0, 88.0)\n' @@ -165,8 +165,9 @@ void main() { ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' - ' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n' + ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -174,8 +175,9 @@ void main() { ' │\n' ' └─child: RenderDecoratedBox#00000\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n' + ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -188,8 +190,9 @@ void main() { ' └─child: _RenderColoredBox#00000\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' - ' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -198,8 +201,8 @@ void main() { ' └─child: RenderPadding#00000\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' - ' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -208,8 +211,7 @@ void main() { ' └─child: RenderPositionedBox#00000\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ size: Size(39.0, 64.0)\n' @@ -220,7 +222,7 @@ void main() { ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ size: Size(25.0, 33.0)\n' @@ -242,7 +244,7 @@ void main() { ); }); - testWidgets('has expected fine diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected fine diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -255,8 +257,9 @@ void main() { equalsIgnoringHashCodes( 'RenderPadding#00000 relayoutBoundary=up1\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← [root]\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ layer: null\n' @@ -267,8 +270,9 @@ void main() { ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' - ' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n' + ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ layer: null\n' @@ -278,8 +282,9 @@ void main() { ' │\n' ' └─child: RenderDecoratedBox#00000\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n' + ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -300,8 +305,9 @@ void main() { ' └─child: _RenderColoredBox#00000\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' - ' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -312,8 +318,8 @@ void main() { ' └─child: RenderPadding#00000\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' - ' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -325,8 +331,7 @@ void main() { ' └─child: RenderPositionedBox#00000\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ layer: null\n' @@ -340,7 +345,7 @@ void main() { ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ layer: null\n' @@ -367,12 +372,12 @@ void main() { ' shape: rectangle\n' ' configuration: ImageConfiguration(bundle:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n' - ' android)\n', + ' android)\n' ), ); }); - testWidgets('has expected hidden diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected hidden diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -386,8 +391,9 @@ void main() { 'RenderPadding#00000 relayoutBoundary=up1\n' ' │ needsCompositing: false\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← [root]\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ layer: null\n' @@ -401,8 +407,9 @@ void main() { ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ needsCompositing: false\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' - ' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n' + ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ layer: null\n' @@ -415,8 +422,9 @@ void main() { ' └─child: RenderDecoratedBox#00000\n' ' │ needsCompositing: false\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n' + ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -440,8 +448,9 @@ void main() { ' │ needsCompositing: false\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' - ' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -455,8 +464,8 @@ void main() { ' │ needsCompositing: false\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' - ' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' - ' │ [root]\n' + ' │ ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -471,8 +480,7 @@ void main() { ' │ needsCompositing: false\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ layer: null\n' @@ -489,7 +497,7 @@ void main() { ' │ needsCompositing: false\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ layer: null\n' @@ -521,12 +529,12 @@ void main() { ' shape: rectangle\n' ' configuration: ImageConfiguration(bundle:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n' - ' android)\n', + ' android)\n' ), ); }); - testWidgets('painting error has expected diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('painting error has expected diagnostics', (WidgetTester tester) async { await tester.pumpWidget(Align( alignment: Alignment.topLeft, child: container, @@ -557,7 +565,7 @@ void main() { }); }); - testWidgets('Can be placed in an infinite box', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be placed in an infinite box', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -566,7 +574,7 @@ void main() { ); }); - testWidgets('Container transformAlignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Container transformAlignment', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( @@ -613,7 +621,7 @@ void main() { expect(tester.getBottomRight(finder), equals(const Offset(200, 200))); }); - testWidgets('giving clipBehaviour Clip.None, will not add a ClipPath to the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('giving clipBehaviour Clip.None, will not add a ClipPath to the tree', (WidgetTester tester) async { await tester.pumpWidget( Container( decoration: const BoxDecoration( @@ -628,7 +636,7 @@ void main() { ); }); - testWidgets('giving clipBehaviour not a Clip.None, will add a ClipPath to the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('giving clipBehaviour not a Clip.None, will add a ClipPath to the tree', (WidgetTester tester) async { final Container container = Container( clipBehavior: Clip.hardEdge, decoration: const BoxDecoration( @@ -645,7 +653,7 @@ void main() { ); }); - testWidgets('getClipPath() works for lots of kinds of decorations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getClipPath() works for lots of kinds of decorations', (WidgetTester tester) async { Future<void> test(Decoration decoration) async { await tester.pumpWidget( Directionality( @@ -675,7 +683,7 @@ void main() { await test(const FlutterLogoDecoration()); }); - testWidgets('Container is hittable only when having decorations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Container is hittable only when having decorations', (WidgetTester tester) async { bool tapped = false; await tester.pumpWidget(GestureDetector( onTap: () { tapped = true; }, @@ -729,7 +737,7 @@ void main() { expect(tapped, false); }); - testWidgets('Container discards alignment when the child parameter is null and constraints is not Tight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Container discards alignment when the child parameter is null and constraints is not Tight', (WidgetTester tester) async { await tester.pumpWidget( Container( decoration: const BoxDecoration( @@ -744,7 +752,7 @@ void main() { ); }); - testWidgets('using clipBehaviour and shadow, should not clip the shadow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using clipBehaviour and shadow, should not clip the shadow', (WidgetTester tester) async { final Container container = Container( clipBehavior: Clip.hardEdge, decoration: const BoxDecoration( diff --git a/packages/flutter/test/widgets/context_menu_controller_test.dart b/packages/flutter/test/widgets/context_menu_controller_test.dart index 1a6155881e2c0..2328238dcf80d 100644 --- a/packages/flutter/test/widgets/context_menu_controller_test.dart +++ b/packages/flutter/test/widgets/context_menu_controller_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'clipboard_utils.dart'; import 'editable_text_utils.dart'; @@ -20,7 +21,7 @@ void main() { await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); }); - testWidgets('Hides and shows only a single menu', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hides and shows only a single menu', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); late final BuildContext context; @@ -90,7 +91,7 @@ void main() { expect(find.byKey(key2), findsNothing); }); - testWidgets('A menu can be hidden and then reshown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A menu can be hidden and then reshown', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); late final BuildContext context; @@ -141,7 +142,7 @@ void main() { expect(find.byKey(key1), findsOneWidget); }); - testWidgets('markNeedsBuild causes the builder to update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('markNeedsBuild causes the builder to update', (WidgetTester tester) async { int buildCount = 0; late final BuildContext context; @@ -178,7 +179,7 @@ void main() { controller.remove(); }); - testWidgets('Calling show when a built-in widget is already showing its context menu hides the built-in menu', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Calling show when a built-in widget is already showing its context menu hides the built-in menu', (WidgetTester tester) async { final GlobalKey builtInKey = GlobalKey(); final GlobalKey directKey = GlobalKey(); late final BuildContext context; diff --git a/packages/flutter/test/widgets/coordinates_test.dart b/packages/flutter/test/widgets/coordinates_test.dart index 847f7482f26fd..f2668521cab92 100644 --- a/packages/flutter/test/widgets/coordinates_test.dart +++ b/packages/flutter/test/widgets/coordinates_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Comparing coordinates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Comparing coordinates', (WidgetTester tester) async { final Key keyA = GlobalKey(); final Key keyB = GlobalKey(); diff --git a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart index 0a6db8b1c85b6..8a31b2877548e 100644 --- a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart +++ b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { late BoxConstraints getSizeConstraints; @@ -163,7 +164,7 @@ class LayoutWithMissingId extends ParentDataWidget<MultiChildLayoutParentData> { } void main() { - testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Control test for CustomMultiChildLayout', (WidgetTester tester) async { final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); await tester.pumpWidget(buildFrame(delegate)); @@ -181,7 +182,7 @@ void main() { expect(delegate.performLayoutIsChild, false); }); - testWidgets('Test MultiChildDelegate shouldRelayout method', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test MultiChildDelegate shouldRelayout method', (WidgetTester tester) async { TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); await tester.pumpWidget(buildFrame(delegate)); @@ -204,7 +205,7 @@ void main() { expect(delegate.performLayoutSize, isNotNull); }); - testWidgets('Nested CustomMultiChildLayouts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested CustomMultiChildLayouts', (WidgetTester tester) async { final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); await tester.pumpWidget(Center( child: CustomMultiChildLayout( @@ -227,7 +228,7 @@ void main() { }); - testWidgets('Loose constraints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Loose constraints', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget(Center( child: CustomMultiChildLayout( @@ -251,8 +252,9 @@ void main() { expect(box.size.height, equals(250.0)); }); - testWidgets('Can use listener for relayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can use listener for relayout', (WidgetTester tester) async { final ValueNotifier<Size> size = ValueNotifier<Size>(const Size(100.0, 200.0)); + addTearDown(size.dispose); await tester.pumpWidget( Center( @@ -302,7 +304,7 @@ void main() { expect((errors.first.exception as FlutterError).toStringDeep(), equalsIgnoringHashCodes(message)); } - testWidgets('layoutChild on non existent child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('layoutChild on non existent child', (WidgetTester tester) async { await expectFlutterErrorMessage( tester: tester, delegate: ZeroAndOneIdLayoutDelegate(), @@ -314,7 +316,7 @@ void main() { ); }); - testWidgets('layoutChild more than once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('layoutChild more than once', (WidgetTester tester) async { await expectFlutterErrorMessage( tester: tester, delegate: DuplicateLayoutDelegate(), @@ -326,7 +328,7 @@ void main() { ); }); - testWidgets('layoutChild on invalid size constraint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('layoutChild on invalid size constraint', (WidgetTester tester) async { await expectFlutterErrorMessage( tester: tester, delegate: InvalidConstraintsChildLayoutDelegate(), @@ -345,7 +347,7 @@ void main() { ); }); - testWidgets('positionChild on non existent child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positionChild on non existent child', (WidgetTester tester) async { await expectFlutterErrorMessage( tester: tester, delegate: NonExistentPositionDelegate(), @@ -357,7 +359,7 @@ void main() { ); }); - testWidgets("_callPerformLayout on child that doesn't have id", (WidgetTester tester) async { + testWidgetsWithLeakTracking("_callPerformLayout on child that doesn't have id", (WidgetTester tester) async { await expectFlutterErrorMessage( widget: Center( child: CustomMultiChildLayout( @@ -373,8 +375,9 @@ void main() { ' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n' ' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n' ' CustomMultiChildLayout ← Center ← MediaQuery ←\n' - ' _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' TestFlutterView#00000] ← [root]\n' + ' _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' [root]\n' ' parentData: offset=Offset(0.0, 0.0); id=null\n' ' constraints: MISSING\n' ' size: MISSING\n' @@ -382,7 +385,7 @@ void main() { ); }); - testWidgets('performLayout did not layout a child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('performLayout did not layout a child', (WidgetTester tester) async { await expectFlutterErrorMessage( widget: Center( child: CustomMultiChildLayout( @@ -404,7 +407,7 @@ void main() { ); }); - testWidgets('performLayout did not layout multiple child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('performLayout did not layout multiple child', (WidgetTester tester) async { await expectFlutterErrorMessage( widget: Center( child: CustomMultiChildLayout( diff --git a/packages/flutter/test/widgets/custom_paint_test.dart b/packages/flutter/test/widgets/custom_paint_test.dart index 933b5b6b4f79c..70071f62dae8a 100644 --- a/packages/flutter/test/widgets/custom_paint_test.dart +++ b/packages/flutter/test/widgets/custom_paint_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestCustomPainter extends CustomPainter { TestCustomPainter({ required this.log, this.name }); @@ -40,7 +41,7 @@ class MockPaintingContext extends Fake implements PaintingContext { } void main() { - testWidgets('Control test for custom painting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Control test for custom painting', (WidgetTester tester) async { final List<String?> log = <String?>[]; await tester.pumpWidget(CustomPaint( painter: TestCustomPainter( @@ -62,7 +63,7 @@ void main() { expect(log, equals(<String>['background', 'child', 'foreground'])); }); - testWidgets('Throws FlutterError on custom painter incorrect restore/save calls', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Throws FlutterError on custom painter incorrect restore/save calls', (WidgetTester tester) async { final GlobalKey target = GlobalKey(); final List<String?> log = <String?>[]; await tester.pumpWidget(CustomPaint( @@ -118,7 +119,7 @@ void main() { expect(error.toStringDeep(), contains('2 more times')); }); - testWidgets('CustomPaint sizing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomPaint sizing', (WidgetTester tester) async { final GlobalKey target = GlobalKey(); await tester.pumpWidget(Center( @@ -153,7 +154,7 @@ void main() { }); - testWidgets('Raster cache hints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Raster cache hints', (WidgetTester tester) async { final GlobalKey target = GlobalKey(); final List<String?> log = <String?>[]; diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart index 3d102a4699c0a..322e974c94779 100644 --- a/packages/flutter/test/widgets/custom_painter_test.dart +++ b/packages/flutter/test/widgets/custom_painter_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -22,7 +23,7 @@ void main() { } void _defineTests() { - testWidgets('builds no semantics by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('builds no semantics by default', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( @@ -36,7 +37,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('provides foreground semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('provides foreground semantics', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( @@ -72,7 +73,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('provides background semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('provides background semantics', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( @@ -108,7 +109,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('combines background, child and foreground semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('combines background, child and foreground semantics', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( @@ -167,7 +168,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('applies $SemanticsProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applies $SemanticsProperties', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( @@ -270,7 +271,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can toggle semantics on, off, on without crash', (WidgetTester tester) async { await tester.pumpWidget(CustomPaint( painter: _PainterWithSemantics( semantics: const CustomPainterSemantics( @@ -312,7 +313,7 @@ void _defineTests() { semantics.dispose(); }, semanticsEnabled: false); - testWidgets('Supports all actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Supports all actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<SemanticsAction> performedActions = <SemanticsAction>[]; @@ -410,7 +411,7 @@ void _defineTests() { semantics.dispose(); }); - testWidgets('Supports all flags', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Supports all flags', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // checked state and toggled state are mutually exclusive. await tester.pumpWidget(CustomPaint( @@ -440,6 +441,7 @@ void _defineTests() { image: true, liveRegion: true, toggled: true, + expanded: true, ), ), ), @@ -494,6 +496,7 @@ void _defineTests() { namesRoute: true, image: true, liveRegion: true, + expanded: true, ), ), ), @@ -523,7 +526,7 @@ void _defineTests() { }); group('diffing', () { - testWidgets('complains about duplicate keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('complains about duplicate keys', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); await tester.pumpWidget(CustomPaint( painter: _SemanticsDiffTest(<String>[ @@ -620,7 +623,7 @@ void _defineTests() { }); }); - testWidgets('rebuilds semantics upon resize', (WidgetTester tester) async { + testWidgetsWithLeakTracking('rebuilds semantics upon resize', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); final _PainterWithSemantics painter = _PainterWithSemantics( @@ -665,7 +668,7 @@ void _defineTests() { semanticsTester.dispose(); }); - testWidgets('does not rebuild when shouldRebuildSemantics is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not rebuild when shouldRebuildSemantics is false', (WidgetTester tester) async { final SemanticsTester semanticsTester = SemanticsTester(tester); const CustomPainterSemantics testSemantics = CustomPainterSemantics( @@ -710,7 +713,7 @@ void _defineTests() { } void _testDiff(String description, Future<void> Function(_DiffTester tester) testFunction) { - testWidgets(description, (WidgetTester tester) async { + testWidgetsWithLeakTracking(description, (WidgetTester tester) async { await testFunction(_DiffTester(tester)); }); } diff --git a/packages/flutter/test/widgets/custom_scroll_view_test.dart b/packages/flutter/test/widgets/custom_scroll_view_test.dart index 02c3281332338..20f5f475f38eb 100644 --- a/packages/flutter/test/widgets/custom_scroll_view_test.dart +++ b/packages/flutter/test/widgets/custom_scroll_view_test.dart @@ -4,10 +4,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/96024 - testWidgets('CustomScrollView.center update test 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView.center update test 1', (WidgetTester tester) async { final Key centerKey = UniqueKey(); late StateSetter setState; bool hasKey = false; @@ -49,7 +50,7 @@ void main() { // Pass without throw. }); - testWidgets('CustomScrollView.center update test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView.center update test 2', (WidgetTester tester) async { const List<Widget> slivers1 = <Widget>[ SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)), SliverToBoxAdapter(key: Key('b'), child: SizedBox(height: 100.0)), @@ -81,7 +82,7 @@ void main() { // Pass without throw. }); - testWidgets('CustomScrollView.center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView.center', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( @@ -103,7 +104,7 @@ void main() { ); }); - testWidgets('CustomScrollView.center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView.center', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( @@ -135,7 +136,7 @@ void main() { ); }); - testWidgets('CustomScrollView.anchor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView.anchor', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( diff --git a/packages/flutter/test/widgets/custom_single_child_layout_test.dart b/packages/flutter/test/widgets/custom_single_child_layout_test.dart index 5e2f67e092b67..724f71bde79c0 100644 --- a/packages/flutter/test/widgets/custom_single_child_layout_test.dart +++ b/packages/flutter/test/widgets/custom_single_child_layout_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestSingleChildLayoutDelegate extends SingleChildLayoutDelegate { late BoxConstraints constraintsFromGetSize; @@ -93,7 +94,7 @@ Widget buildFrame(SingleChildLayoutDelegate delegate) { } void main() { - testWidgets('Control test for CustomSingleChildLayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Control test for CustomSingleChildLayout', (WidgetTester tester) async { final TestSingleChildLayoutDelegate delegate = TestSingleChildLayoutDelegate(); await tester.pumpWidget(buildFrame(delegate)); @@ -114,7 +115,7 @@ void main() { expect(delegate.childSizeFromGetPositionForChild.height, 400.0); }); - testWidgets('Test SingleChildDelegate shouldRelayout method', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test SingleChildDelegate shouldRelayout method', (WidgetTester tester) async { TestSingleChildLayoutDelegate delegate = TestSingleChildLayoutDelegate(); await tester.pumpWidget(buildFrame(delegate)); @@ -138,7 +139,7 @@ void main() { expect(delegate.constraintsFromGetConstraintsForChild, isNotNull); }); - testWidgets('Delegate can change size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Delegate can change size', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(FixedSizeLayoutDelegate(const Size(100.0, 200.0)))); RenderBox box = tester.renderObject(find.byType(CustomSingleChildLayout)); @@ -150,8 +151,9 @@ void main() { expect(box.size, equals(const Size(150.0, 240.0))); }); - testWidgets('Can use listener for relayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can use listener for relayout', (WidgetTester tester) async { final ValueNotifier<Size> size = ValueNotifier<Size>(const Size(100.0, 200.0)); + addTearDown(size.dispose); await tester.pumpWidget(buildFrame(NotifierLayoutDelegate(size))); diff --git a/packages/flutter/test/widgets/debug_test.dart b/packages/flutter/test/widgets/debug_test.dart index a98f70fe4ff59..fd362bc06a4f0 100644 --- a/packages/flutter/test/widgets/debug_test.dart +++ b/packages/flutter/test/widgets/debug_test.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('debugChildrenHaveDuplicateKeys control test', () { @@ -64,7 +65,7 @@ void main() { } }); - testWidgets('debugCheckHasTable control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasTable control test', (WidgetTester tester) async { await tester.pumpWidget( Builder( builder: (BuildContext context) { @@ -95,7 +96,7 @@ void main() { ); }); - testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasMediaQuery control test', (WidgetTester tester) async { // Cannot use tester.pumpWidget here because it wraps the widget in a View, // which introduces a MediaQuery ancestor. await pumpWidgetWithoutViewWrapper( @@ -144,7 +145,10 @@ void main() { ), ); } - return Container(); + return View( + view: tester.view, + child: const SizedBox(), + ); }, ), ); @@ -228,7 +232,7 @@ void main() { } }); - testWidgets('debugCheckHasWidgetsLocalizations throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCheckHasWidgetsLocalizations throws', (WidgetTester tester) async { final GlobalKey noLocalizationsAvailable = GlobalKey(); final GlobalKey localizationsAvailable = GlobalKey(); @@ -277,7 +281,7 @@ void main() { debugHighlightDeprecatedWidgets = false; }); - testWidgets('debugCreator of layers should not be null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugCreator of layers should not be null', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Directionality( diff --git a/packages/flutter/test/widgets/decorated_sliver_test.dart b/packages/flutter/test/widgets/decorated_sliver_test.dart index a3d328fe72cd7..5800d8db16146 100644 --- a/packages/flutter/test/widgets/decorated_sliver_test.dart +++ b/packages/flutter/test/widgets/decorated_sliver_test.dart @@ -9,11 +9,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('DecoratedSliver creates, paints, and disposes BoxPainter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver creates, paints, and disposes BoxPainter', (WidgetTester tester) async { final TestDecoration decoration = TestDecoration(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -41,7 +40,7 @@ void main() { expect(decoration.painters.last.disposed, true); }); - testWidgets('DecoratedSliver can update box painter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver can update box painter', (WidgetTester tester) async { final TestDecoration decorationA = TestDecoration(); final TestDecoration decorationB = TestDecoration(); @@ -81,7 +80,7 @@ void main() { expect(decorationB.painters.last.paintCount, 1); }); - testWidgets('DecoratedSliver can update DecorationPosition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver can update DecorationPosition', (WidgetTester tester) async { final TestDecoration decoration = TestDecoration(); DecorationPosition activePosition = DecorationPosition.foreground; @@ -119,7 +118,7 @@ void main() { expect(decoration.painters.last.paintCount, 2); }); - testWidgets('DecoratedSliver golden test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver golden test', (WidgetTester tester) async { const BoxDecoration decoration = BoxDecoration( color: Colors.blue, ); @@ -200,10 +199,11 @@ void main() { await expectLater(find.byKey(foregroundKey), matchesGoldenFile('decorated_sliver.moon.foreground.png')); }); - testWidgets('DecoratedSliver paints its border correctly vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver paints its border correctly vertically', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -236,10 +236,11 @@ void main() { )); }); - testWidgets('DecoratedSliver paints its border correctly vertically reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver paints its border correctly vertically reverse', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -275,10 +276,11 @@ void main() { - testWidgets('DecoratedSliver paints its border correctly horizontally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver paints its border correctly horizontally', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -312,10 +314,11 @@ void main() { )); }); - testWidgets('DecoratedSliver paints its border correctly horizontally reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver paints its border correctly horizontally reverse', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -351,10 +354,11 @@ void main() { }); - testWidgets('DecoratedSliver works with SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver works with SliverMainAxisGroup', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -389,10 +393,11 @@ void main() { )); }); - testWidgets('DecoratedSliver works with SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver works with SliverCrossAxisGroup', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( @@ -427,10 +432,11 @@ void main() { )); }); - testWidgets('DecoratedSliver draws only up to the bottom cache when sliver has infinite scroll extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecoratedSliver draws only up to the bottom cache when sliver has infinite scroll extent', (WidgetTester tester) async { const Key key = Key('DecoratedSliver with border'); const Color black = Color(0xFF000000); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Align( diff --git a/packages/flutter/test/widgets/default_colors_test.dart b/packages/flutter/test/widgets/default_colors_test.dart index af9fa9dca52f0..90b89bfedea47 100644 --- a/packages/flutter/test/widgets/default_colors_test.dart +++ b/packages/flutter/test/widgets/default_colors_test.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const double _crispText = 100.0; // this font size is selected to avoid needing any antialiasing. const String _expText = 'Éxp'; // renders in the test font as: @@ -19,7 +20,7 @@ const String _expText = 'Éxp'; // renders in the test font as: // ÉÉÉÉxxxxpppp void main() { - testWidgets('Default background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default background', (WidgetTester tester) async { await tester.pumpWidget(const Align( alignment: Alignment.topLeft, child: Text(_expText, textDirection: TextDirection.ltr, style: TextStyle(color: Color(0xFF345678), fontSize: _crispText))), @@ -40,7 +41,7 @@ void main() { ); }, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently. - testWidgets('Default text color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default text color', (WidgetTester tester) async { await tester.pumpWidget(const ColoredBox( color: Color(0xFFABCDEF), child: Align( @@ -65,8 +66,22 @@ void main() { ); }, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently. - testWidgets('Default text selection color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default text selection color', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final OverlayEntry overlayEntry = OverlayEntry( + builder: (BuildContext context) => SelectableRegion( + focusNode: focusNode, + selectionControls: emptyTextSelectionControls, + child: Align( + key: key, + alignment: Alignment.topLeft, + child: const Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText, color: Color(0xFF000000))), + ), + ), + ); + addTearDown(() => overlayEntry..remove()..dispose()); await tester.pumpWidget( ColoredBox( color: const Color(0xFFFFFFFF), @@ -75,19 +90,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: Overlay( - initialEntries: <OverlayEntry>[ - OverlayEntry( - builder: (BuildContext context) => SelectableRegion( - focusNode: FocusNode(), - selectionControls: emptyTextSelectionControls, - child: Align( - key: key, - alignment: Alignment.topLeft, - child: const Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText, color: Color(0xFF000000))), - ), - ), - ), - ], + initialEntries: <OverlayEntry>[overlayEntry], ), ), ), @@ -132,6 +135,7 @@ Color _getPixel(ByteData bytes, int x, int y, int width) { Future<void> _expectColors(WidgetTester tester, Finder finder, Set<Color> allowedColors, [ Map<Offset, Color>? spotChecks ]) async { final TestWidgetsFlutterBinding binding = tester.binding; final ui.Image image = (await binding.runAsync<ui.Image>(() => captureImage(finder.evaluate().single)))!; + addTearDown(image.dispose); final ByteData bytes = (await binding.runAsync<ByteData?>(() => image.toByteData(format: ui.ImageByteFormat.rawStraightRgba)))!; final Set<int> actualColorValues = <int>{}; for (int offset = 0; offset < bytes.lengthInBytes; offset += 4) { diff --git a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart index 61b1365083919..c3d8a71aa312c 100644 --- a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart +++ b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'keyboard_utils.dart'; @@ -14,6 +15,9 @@ void main() { required FocusNode editableFocusNode, required FocusNode spyFocusNode, }) { + final TextEditingController controller = TextEditingController(text: 'dummy text'); + addTearDown(controller.dispose); + return MaterialApp( home: Align( alignment: Alignment.topLeft, @@ -24,7 +28,7 @@ void main() { child: ActionSpy( focusNode: spyFocusNode, child: EditableText( - controller: TextEditingController(text: 'dummy text'), + controller: controller, showSelectionHandles: true, autofocus: true, focusNode: editableFocusNode, @@ -50,7 +54,7 @@ void main() { final FocusNode editable = FocusNode(); final FocusNode spy = FocusNode(); - testWidgets('backspace with and without word modifier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace with and without word modifier', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown(tester.binding.testTextInput.register); @@ -74,7 +78,7 @@ void main() { expect(state.lastIntent, isNull); }, variant: iOS); - testWidgets('delete with and without word modifier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete with and without word modifier', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown(tester.binding.testTextInput.register); @@ -98,7 +102,7 @@ void main() { expect(state.lastIntent, isNull); }, variant: iOS); - testWidgets('Exception: deleting to line boundary is handled by the framework', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Exception: deleting to line boundary is handled by the framework', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown(tester.binding.testTextInput.register); @@ -128,13 +132,15 @@ void main() { group('macOS does not accept shortcuts if focus under EditableText', () { final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); - testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -151,13 +157,15 @@ void main() { expect(state.lastIntent, isNull); }, variant: macOSOnly); - testWidgets('word modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -174,13 +182,15 @@ void main() { expect(state.lastIntent, isNull); }, variant: macOSOnly); - testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -197,13 +207,15 @@ void main() { expect(state.lastIntent, isNull); }, variant: macOSOnly); - testWidgets('line modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -220,13 +232,15 @@ void main() { expect(state.lastIntent, isNull); }, variant: macOSOnly); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -255,13 +269,15 @@ void main() { expect(state.lastIntent, isNull); }, variant: macOSOnly); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -293,13 +309,15 @@ void main() { group('macOS does accept shortcuts if focus above EditableText', () { final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); - testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -316,13 +334,15 @@ void main() { expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); - testWidgets('word modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -339,13 +359,15 @@ void main() { expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); - testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowLeft', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -362,13 +384,15 @@ void main() { expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); }, variant: macOSOnly); - testWidgets('line modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowRight', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -385,13 +409,15 @@ void main() { expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>()); }, variant: macOSOnly); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, @@ -422,13 +448,15 @@ void main() { expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>()); }, variant: macOSOnly); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { tester.binding.testTextInput.unregister(); addTearDown((){ tester.binding.testTextInput.register(); }); final FocusNode editable = FocusNode(); + addTearDown(editable.dispose); final FocusNode spy = FocusNode(); + addTearDown(spy.dispose); await tester.pumpWidget( buildSpyAboveEditableText( editableFocusNode: editable, diff --git a/packages/flutter/test/widgets/default_text_height_behavior_test.dart b/packages/flutter/test/widgets/default_text_height_behavior_test.dart index a3974dffb0965..5649d75a2c6c4 100644 --- a/packages/flutter/test/widgets/default_text_height_behavior_test.dart +++ b/packages/flutter/test/widgets/default_text_height_behavior_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Text widget parameter takes precedence over DefaultTextHeightBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text widget parameter takes precedence over DefaultTextHeightBehavior', (WidgetTester tester) async { const TextHeightBehavior behavior1 = TextHeightBehavior( applyHeightToLastDescent: false, applyHeightToFirstAscent: false, @@ -31,7 +32,7 @@ void main() { expect(text.textHeightBehavior, behavior1); }); - testWidgets('DefaultTextStyle.textHeightBehavior takes precedence over DefaultTextHeightBehavior ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DefaultTextStyle.textHeightBehavior takes precedence over DefaultTextHeightBehavior ', (WidgetTester tester) async { const TextHeightBehavior behavior1 = TextHeightBehavior( applyHeightToLastDescent: false, applyHeightToFirstAscent: false, @@ -77,7 +78,7 @@ void main() { expect(text.textHeightBehavior, behavior1); }); - testWidgets('DefaultTextHeightBehavior changes propagate to Text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DefaultTextHeightBehavior changes propagate to Text', (WidgetTester tester) async { const Text textWidget = Text('Hello', textDirection: TextDirection.ltr); const TextHeightBehavior behavior1 = TextHeightBehavior( applyHeightToLastDescent: false, @@ -107,7 +108,7 @@ void main() { expect(text.textHeightBehavior, behavior2); }); - testWidgets( + testWidgetsWithLeakTracking( 'DefaultTextHeightBehavior.of(context) returns null if no ' 'DefaultTextHeightBehavior widget in tree', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/default_text_style_test.dart b/packages/flutter/test/widgets/default_text_style_test.dart index ddd9c6a986d9c..9b26768b91b85 100644 --- a/packages/flutter/test/widgets/default_text_style_test.dart +++ b/packages/flutter/test/widgets/default_text_style_test.dart @@ -6,9 +6,10 @@ import 'dart:ui' as ui show TextHeightBehavior; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('DefaultTextStyle changes propagate to Text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DefaultTextStyle changes propagate to Text', (WidgetTester tester) async { const Text textWidget = Text('Hello', textDirection: TextDirection.ltr); const TextStyle s1 = TextStyle( fontSize: 10.0, @@ -43,7 +44,7 @@ void main() { expect(text.maxLines, 3); }); - testWidgets('AnimatedDefaultTextStyle changes propagate to Text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedDefaultTextStyle changes propagate to Text', (WidgetTester tester) async { const Text textWidget = Text('Hello', textDirection: TextDirection.ltr); const TextStyle s1 = TextStyle( fontSize: 10.0, diff --git a/packages/flutter/test/widgets/did_update_widget_test.dart b/packages/flutter/test/widgets/did_update_widget_test.dart index afe802a5df8ea..d697cdbebd2fa 100644 --- a/packages/flutter/test/widgets/did_update_widget_test.dart +++ b/packages/flutter/test/widgets/did_update_widget_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can call setState from didUpdateWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can call setState from didUpdateWidget', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, child: WidgetUnderTest(text: 'hello'), diff --git a/packages/flutter/test/widgets/directionality_test.dart b/packages/flutter/test/widgets/directionality_test.dart index 8472eb46ad979..4f83dc35fb36b 100644 --- a/packages/flutter/test/widgets/directionality_test.dart +++ b/packages/flutter/test/widgets/directionality_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality', (WidgetTester tester) async { final List<TextDirection> log = <TextDirection>[]; final Widget inner = Builder( builder: (BuildContext context) { @@ -51,7 +52,7 @@ void main() { expect(log, <TextDirection>[TextDirection.ltr, TextDirection.rtl, TextDirection.ltr]); }); - testWidgets('Directionality default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality default', (WidgetTester tester) async { bool good = false; await tester.pumpWidget(Builder( builder: (BuildContext context) { @@ -63,7 +64,7 @@ void main() { expect(good, isTrue); }); - testWidgets('Directionality.maybeOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality.maybeOf', (WidgetTester tester) async { final GlobalKey hasDirectionality = GlobalKey(); final GlobalKey noDirectionality = GlobalKey(); await tester.pumpWidget( @@ -81,7 +82,7 @@ void main() { expect(Directionality.maybeOf(hasDirectionality.currentContext!), TextDirection.rtl); }); - testWidgets('Directionality.of', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directionality.of', (WidgetTester tester) async { final GlobalKey hasDirectionality = GlobalKey(); final GlobalKey noDirectionality = GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/dismissible_test.dart b/packages/flutter/test/widgets/dismissible_test.dart index b948dc1331faa..878ea08bd0cfa 100644 --- a/packages/flutter/test/widgets/dismissible_test.dart +++ b/packages/flutter/test/widgets/dismissible_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const DismissDirection defaultDismissDirection = DismissDirection.horizontal; const double crossAxisEndOffset = 0.5; @@ -252,7 +253,7 @@ void main() { dismissedItems = <int>[]; }); - testWidgets('Horizontal drag triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal drag triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { await tester.pumpWidget( buildTest(), ); @@ -269,7 +270,7 @@ void main() { expect(reportedDismissDirection, DismissDirection.endToStart); }); - testWidgets('Horizontal fling triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal fling triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { await tester.pumpWidget( buildTest(), ); @@ -286,7 +287,7 @@ void main() { expect(reportedDismissDirection, DismissDirection.endToStart); }); - testWidgets('Horizontal fling does not trigger at zero offset, but does otherwise', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal fling does not trigger at zero offset, but does otherwise', (WidgetTester tester) async { await tester.pumpWidget( buildTest( startToEndThreshold: 0.95, @@ -313,7 +314,7 @@ void main() { expect(reportedDismissDirection, DismissDirection.endToStart); }); - testWidgets('Vertical drag triggers dismiss scrollDirection=horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical drag triggers dismiss scrollDirection=horizontal', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -333,7 +334,7 @@ void main() { expect(reportedDismissDirection, DismissDirection.down); }); - testWidgets('drag-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( dismissDirection: DismissDirection.endToStart, @@ -352,7 +353,7 @@ void main() { await dismissItem(tester, 1, gestureDirection: AxisDirection.left); }); - testWidgets('drag-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( dismissDirection: DismissDirection.startToEnd, @@ -369,7 +370,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('drag-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( textDirection: TextDirection.rtl, @@ -388,7 +389,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('drag-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( textDirection: TextDirection.rtl, @@ -408,7 +409,7 @@ void main() { await dismissItem(tester, 1, gestureDirection: AxisDirection.left); }); - testWidgets('fling-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( dismissDirection: DismissDirection.endToStart, @@ -427,7 +428,7 @@ void main() { await dismissItem(tester, 1, gestureDirection: AxisDirection.left); }); - testWidgets('fling-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( dismissDirection: DismissDirection.startToEnd, @@ -445,7 +446,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('fling-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( textDirection: TextDirection.rtl, @@ -463,7 +464,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('fling-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { await tester.pumpWidget( buildTest( textDirection: TextDirection.rtl, @@ -483,7 +484,7 @@ void main() { await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.left); }); - testWidgets('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -501,7 +502,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('drag-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -519,7 +520,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('fling-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -537,7 +538,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('fling-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -555,7 +556,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('drag-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('drag-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { await tester.pumpWidget( buildTest( startToEndThreshold: 1.0, @@ -572,7 +573,7 @@ void main() { expect(dismissedItems, equals(<int>[0])); }); - testWidgets('fling-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { await tester.pumpWidget( buildTest( startToEndThreshold: 1.0, @@ -595,7 +596,7 @@ void main() { // now since we migrated to the new repo. The bug was fixed by // https://github.com/flutter/engine/pull/1134 at the time, and later made // irrelevant by fn3, but just in case... - testWidgets('Verify that drag-move events do not assert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Verify that drag-move events do not assert', (WidgetTester tester) async { await tester.pumpWidget( buildTest( scrollDirection: Axis.horizontal, @@ -622,7 +623,7 @@ void main() { // died in the migration to the new repo). Don't copy this test; it doesn't // actually remove the dismissed widget, which is a violation of the // Dismissible contract. This is not an example of good practice. - testWidgets('dismissing bottom then top (smoketest)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dismissing bottom then top (smoketest)', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -654,7 +655,7 @@ void main() { expect(find.text('2'), findsNothing); }); - testWidgets('Dismissible starts from the full size when collapsing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible starts from the full size when collapsing', (WidgetTester tester) async { await tester.pumpWidget( buildTest( background: const Text('background'), @@ -672,7 +673,7 @@ void main() { expect(backgroundBox.size.height, equals(100.0)); }); - testWidgets('Checking fling item before movementDuration completes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checking fling item before movementDuration completes', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); @@ -683,7 +684,7 @@ void main() { expect(find.text('1'), findsOneWidget); }); - testWidgets('Checking fling item after movementDuration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Checking fling item after movementDuration', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); @@ -694,7 +695,7 @@ void main() { expect(find.text('0'), findsNothing); }); - testWidgets('Horizontal fling less than threshold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal fling less than threshold', (WidgetTester tester) async { await tester.pumpWidget(buildTest(scrollDirection: Axis.horizontal)); expect(dismissedItems, isEmpty); @@ -707,7 +708,7 @@ void main() { expect(dismissedItems, isEmpty); }); - testWidgets('Vertical fling less than threshold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical fling less than threshold', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); @@ -720,7 +721,7 @@ void main() { expect(dismissedItems, isEmpty); }); - testWidgets('confirmDismiss returns values: true, false, null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('confirmDismiss returns values: true, false, null', (WidgetTester tester) async { late DismissDirection confirmDismissDirection; Widget buildFrame(bool? confirmDismissValue) { @@ -777,7 +778,7 @@ void main() { expect(confirmDismissDirection, DismissDirection.endToStart); }); - testWidgets('Pending confirmDismiss does not cause errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pending confirmDismiss does not cause errors', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/54990 late Completer<bool?> completer; @@ -839,7 +840,7 @@ void main() { await tester.pump(); }); - testWidgets('Dismissible cannot be dragged with pending confirmDismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible cannot be dragged with pending confirmDismiss', (WidgetTester tester) async { final Completer<bool?> completer = Completer<bool?>(); await tester.pumpWidget( buildTest( @@ -863,7 +864,7 @@ void main() { expect(tester.getTopLeft(find.text('0')), position); }); - testWidgets('Drag to end and release - items does not get stuck if confirmDismiss returns false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag to end and release - items does not get stuck if confirmDismiss returns false', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/87556 final Completer<bool?> completer = Completer<bool?>(); @@ -882,7 +883,7 @@ void main() { expect(tester.getTopLeft(find.text('0')), position); }); - testWidgets('Dismissible with null resizeDuration calls onDismissed immediately', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible with null resizeDuration calls onDismissed immediately', (WidgetTester tester) async { bool resized = false; bool dismissed = false; @@ -914,7 +915,7 @@ void main() { expect(resized, false); }); - testWidgets('setState that does not remove the Dismissible from tree should throw Error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState that does not remove the Dismissible from tree should throw Error', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( @@ -971,7 +972,7 @@ void main() { ); }); - testWidgets('Dismissible.behavior should behave correctly during hit testing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible.behavior should behave correctly during hit testing', (WidgetTester tester) async { bool didReceivePointerDown = false; Widget buildStack({required Widget child}) { @@ -1040,7 +1041,7 @@ void main() { expect(didReceivePointerDown, isTrue); }); - testWidgets('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async { await tester.pumpWidget(buildTest( dismissDirection: DismissDirection.none, scrollPhysics: const NeverScrollableScrollPhysics(), @@ -1054,7 +1055,7 @@ void main() { expect(find.text('0'), findsOneWidget); }); - testWidgets('DismissDirection.none does not prevent scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DismissDirection.none does not prevent scrolling', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget( @@ -1077,7 +1078,7 @@ void main() { controller.dispose(); }); - testWidgets('onUpdate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onUpdate', (WidgetTester tester) async { await tester.pumpWidget(buildTest( scrollDirection: Axis.horizontal, )); @@ -1131,7 +1132,7 @@ void main() { expect(reportedDismissUpdateProgress, 0.0); }); - testWidgets('Change direction does not lose child state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Change direction does not lose child state', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/108961 Widget buildFrame(DismissDirection direction) { return Directionality( diff --git a/packages/flutter/test/widgets/display_feature_sub_screen_test.dart b/packages/flutter/test/widgets/display_feature_sub_screen_test.dart index 926a459a81d62..e61042887f05f 100644 --- a/packages/flutter/test/widgets/display_feature_sub_screen_test.dart +++ b/packages/flutter/test/widgets/display_feature_sub_screen_test.dart @@ -6,10 +6,11 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('DisplayFeatureSubScreen', () { - testWidgets('without Directionality or anchor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('without Directionality or anchor', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -37,7 +38,7 @@ void main() { expect(message, contains('Directionality')); }); - testWidgets('with anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with anchorPoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -68,7 +69,7 @@ void main() { expect(renderBox.localToGlobal(Offset.zero), equals(const Offset(410,0))); }); - testWidgets('with infinity anchorpoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with infinity anchorpoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -99,7 +100,7 @@ void main() { expect(renderBox.localToGlobal(Offset.zero), equals(const Offset(410,0))); }); - testWidgets('with horizontal hinge and anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with horizontal hinge and anchorPoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -129,7 +130,7 @@ void main() { expect(renderBox.localToGlobal(Offset.zero), equals(const Offset(0,310))); }); - testWidgets('with multiple display features and anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with multiple display features and anchorPoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -164,7 +165,7 @@ void main() { expect(renderBox.localToGlobal(Offset.zero), equals(const Offset(410,310))); }); - testWidgets('with non-splitting display features and anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with non-splitting display features and anchorPoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ @@ -209,7 +210,7 @@ void main() { expect(renderBox.localToGlobal(Offset.zero), equals(Offset.zero)); }); - testWidgets('with size 0 display feature in half-opened posture and anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with size 0 display feature in half-opened posture and anchorPoint', (WidgetTester tester) async { const Key childKey = Key('childKey'); final MediaQueryData mediaQuery = MediaQueryData.fromView(tester.view).copyWith( displayFeatures: <DisplayFeature>[ diff --git a/packages/flutter/test/widgets/disposable_build_context_test.dart b/packages/flutter/test/widgets/disposable_build_context_test.dart index 4acac47d8749d..c9473665dc47f 100644 --- a/packages/flutter/test/widgets/disposable_build_context_test.dart +++ b/packages/flutter/test/widgets/disposable_build_context_test.dart @@ -4,10 +4,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('DisposableBuildContext asserts on disposed state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DisposableBuildContext asserts on disposed state', (WidgetTester tester) async { final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>(); await tester.pumpWidget(TestWidget(key)); diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart index db3fbb8d2d62a..c9da896314c61 100644 --- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart +++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { Widget boilerplateWidget(VoidCallback? onButtonPressed, { @@ -71,7 +72,7 @@ void main() { ); } - testWidgets('Do not crash when replacing scroll position during the drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when replacing scroll position during the drag', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/89681 bool showScrollbars = false; await tester.pumpWidget( @@ -119,7 +120,7 @@ void main() { // Go without throw. }); - testWidgets('Scrolls correct amount when maxChildSize < 1.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolls correct amount when maxChildSize < 1.0', (WidgetTester tester) async { const Key key = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget( null, @@ -135,7 +136,7 @@ void main() { expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0)); }); - testWidgets('Scrolls correct amount when maxChildSize == 1.0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolls correct amount when maxChildSize == 1.0', (WidgetTester tester) async { const Key key = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget( null, @@ -172,7 +173,7 @@ void main() { }); group('Scroll Physics', () { - testWidgets('Can be dragged up without covering its container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be dragged up without covering its container', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -193,7 +194,7 @@ void main() { expect(find.text('Item 31'), findsOneWidget); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be dragged down when not full height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be dragged down when not full height', (WidgetTester tester) async { await tester.pumpWidget(boilerplateWidget(null)); expect(find.text('Item 1'), findsOneWidget); expect(find.text('Item 21'), findsOneWidget); @@ -206,7 +207,7 @@ void main() { expect(find.text('Item 36'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be dragged down when list is shorter than full height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be dragged down when list is shorter than full height', (WidgetTester tester) async { await tester.pumpWidget(boilerplateWidget(null, itemCount: 30, initialChildSize: .25)); expect(find.text('Item 1').hitTestable(), findsOneWidget); @@ -223,7 +224,7 @@ void main() { expect(find.text('Item 29').hitTestable(), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be dragged up and cover its container and scroll in single motion, and then dragged back down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be dragged up and cover its container and scroll in single motion, and then dragged back down', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -252,7 +253,7 @@ void main() { expect(find.text('Item 36'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be flung up gently', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be flung up gently', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -275,7 +276,7 @@ void main() { expect(find.text('Item 70'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be flung up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be flung up', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -301,7 +302,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('Can be flung down when not full height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be flung down when not full height', (WidgetTester tester) async { await tester.pumpWidget(boilerplateWidget(null)); expect(find.text('Item 1'), findsOneWidget); expect(find.text('Item 21'), findsOneWidget); @@ -314,7 +315,7 @@ void main() { expect(find.text('Item 36'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Can be flung up and then back down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be flung up and then back down', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -358,7 +359,7 @@ void main() { expect(find.text('Item 70'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Ballistic animation on fling can be interrupted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ballistic animation on fling can be interrupted', (WidgetTester tester) async { int taps = 0; await tester.pumpWidget(boilerplateWidget(() => taps++)); @@ -392,7 +393,7 @@ void main() { expect(find.text('Item 70'), findsNothing); }, variant: TargetPlatformVariant.all()); - testWidgets('Ballistic animation on fling should not leak Ticker', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ballistic animation on fling should not leak Ticker', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/101061 await tester.pumpWidget( Directionality( @@ -447,7 +448,7 @@ void main() { }); }); - testWidgets('Does not snap away from initial child on build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not snap away from initial child on build', (WidgetTester tester) async { const Key containerKey = ValueKey<String>('container'); const Key stackKey = ValueKey<String>('stack'); await tester.pumpWidget(boilerplateWidget(null, @@ -467,10 +468,11 @@ void main() { }, variant: TargetPlatformVariant.all()); for (final bool useActuator in <bool>[false, true]) { - testWidgets('Does not snap away from initial child on ${useActuator ? 'actuator' : 'controller'}.reset()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not snap away from initial child on ${useActuator ? 'actuator' : 'controller'}.reset()', (WidgetTester tester) async { const Key containerKey = ValueKey<String>('container'); const Key stackKey = ValueKey<String>('stack'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -504,7 +506,7 @@ void main() { } for (final Duration? snapAnimationDuration in <Duration?>[null, const Duration(seconds: 2)]) { - testWidgets( + testWidgetsWithLeakTracking( 'Zero velocity drag snaps to nearest snap target with ' 'snapAnimationDuration: $snapAnimationDuration', (WidgetTester tester) async { @@ -563,7 +565,7 @@ void main() { } for (final List<double>? snapSizes in <List<double>?>[null, <double>[]]) { - testWidgets('Setting snapSizes to $snapSizes resolves to min and max', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting snapSizes to $snapSizes resolves to min and max', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget(null, @@ -591,7 +593,7 @@ void main() { }, variant: TargetPlatformVariant.all()); } - testWidgets('Min and max are implicitly added to snapSizes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Min and max are implicitly added to snapSizes', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget(null, @@ -618,7 +620,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('Changes to widget parameters are propagated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changes to widget parameters are propagated', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget( @@ -726,7 +728,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('Fling snaps in direction of momentum', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fling snaps in direction of momentum', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); await tester.pumpWidget(boilerplateWidget(null, @@ -754,7 +756,7 @@ void main() { }, variant: TargetPlatformVariant.all()); - testWidgets("Changing parameters with an un-listened controller doesn't throw", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Changing parameters with an un-listened controller doesn't throw", (WidgetTester tester) async { await tester.pumpWidget(boilerplateWidget( null, snap: true, @@ -769,7 +771,7 @@ void main() { await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.all()); - testWidgets('Transitioning between scrollable children sharing a scroll controller will not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Transitioning between scrollable children sharing a scroll controller will not throw', (WidgetTester tester) async { int s = 0; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( @@ -826,7 +828,7 @@ void main() { // Completes without throwing }); - testWidgets('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async { final List<Type> notificationTypes = <Type>[]; await tester.pumpWidget(boilerplateWidget( null, @@ -847,7 +849,7 @@ void main() { expect(notificationTypes, equals(types)); }); - testWidgets('ScrollNotification correctly dispatched when flung with contents scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollNotification correctly dispatched when flung with contents scroll', (WidgetTester tester) async { final List<Type> notificationTypes = <Type>[]; await tester.pumpWidget(boilerplateWidget( null, @@ -870,7 +872,7 @@ void main() { expect(notificationTypes, types); }); - testWidgets('Emits DraggableScrollableNotification with shouldCloseOnMinExtent set to non-default value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Emits DraggableScrollableNotification with shouldCloseOnMinExtent set to non-default value', (WidgetTester tester) async { DraggableScrollableNotification? receivedNotification; await tester.pumpWidget(boilerplateWidget( null, @@ -886,7 +888,7 @@ void main() { expect(receivedNotification!.shouldCloseOnMinExtent, isFalse); }); - testWidgets('Do not crash when remove the tree during animation.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when remove the tree during animation.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/89214 await tester.pumpWidget(boilerplateWidget( null, @@ -905,10 +907,11 @@ void main() { }); for (final bool shouldAnimate in <bool>[true, false]) { - testWidgets('Can ${shouldAnimate ? 'animate' : 'jump'} to arbitrary positions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can ${shouldAnimate ? 'animate' : 'jump'} to arbitrary positions', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -980,10 +983,11 @@ void main() { }); } - testWidgets('Can animateTo with a nonlinear curve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can animateTo with a nonlinear curve', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -1027,10 +1031,11 @@ void main() { ); }); - testWidgets('Can animateTo with a Curves.easeInOutBack curve begin min-size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can animateTo with a Curves.easeInOutBack curve begin min-size', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, initialChildSize: 0.25, @@ -1050,10 +1055,11 @@ void main() { ); }); - testWidgets('Can reuse a controller after the old controller is disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can reuse a controller after the old controller is disposed', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -1080,10 +1086,11 @@ void main() { ); }); - testWidgets('animateTo interrupts other animations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animateTo interrupts other animations', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: boilerplateWidget( @@ -1116,10 +1123,11 @@ void main() { expect(find.text('Item 1'), findsOneWidget); }); - testWidgets('Other animations interrupt animateTo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Other animations interrupt animateTo', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: boilerplateWidget( @@ -1149,10 +1157,11 @@ void main() { ); }); - testWidgets('animateTo can be interrupted by other animateTo or jumpTo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animateTo can be interrupted by other animateTo or jumpTo', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: boilerplateWidget( @@ -1191,10 +1200,11 @@ void main() { ); }); - testWidgets('Can get size and pixels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can get size and pixels', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -1223,6 +1233,7 @@ void main() { testWidgets('Cannot attach a controller to multiple sheets', (WidgetTester tester) async { final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Stack( @@ -1241,11 +1252,12 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('Can listen for changes in sheet size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can listen for changes in sheet size', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final List<double> loggedSizes = <double>[]; final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); controller.addListener(() { loggedSizes.add(controller.size); }); @@ -1289,11 +1301,12 @@ void main() { loggedSizes.clear(); }); - testWidgets('Listener does not fire on parameter change and persists after change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Listener does not fire on parameter change and persists after change', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final List<double> loggedSizes = <double>[]; final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); controller.addListener(() { loggedSizes.add(controller.size); }); @@ -1331,11 +1344,12 @@ void main() { loggedSizes.clear(); }); - testWidgets('Listener fires if a parameter change forces a change in size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Listener fires if a parameter change forces a change in size', (WidgetTester tester) async { const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final List<double> loggedSizes = <double>[]; final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); controller.addListener(() { loggedSizes.add(controller.size); }); @@ -1382,8 +1396,9 @@ void main() { loggedSizes.clear(); }); - testWidgets('Invalid controller interactions throw assertion errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Invalid controller interactions throw assertion errors', (WidgetTester tester) async { final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); // Can't use a controller before attaching it. expect(() => controller.jumpTo(.1), throwsAssertionError); @@ -1414,8 +1429,9 @@ void main() { expect(() => controller.animateTo(.5, duration: Duration.zero, curve: Curves.linear), throwsAssertionError); }); - testWidgets('DraggableScrollableController must be attached before using any of its parameters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableController must be attached before using any of its parameters', (WidgetTester tester) async { final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); expect(controller.isAttached, false); expect(()=>controller.size, throwsAssertionError); final Widget boilerplate = boilerplateWidget( @@ -1428,8 +1444,9 @@ void main() { expect(controller.size, isNotNull); }); - testWidgets('DraggableScrollableController.animateTo after detach', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableController.animateTo after detach', (WidgetTester tester) async { final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget(() {}, controller: controller)); controller.animateTo(0.0, curve: Curves.linear, duration: const Duration(milliseconds: 200)); @@ -1442,11 +1459,12 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('DraggableScrollableSheet should not reset programmatic drag on rebuild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableSheet should not reset programmatic drag on rebuild', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/101114 const Key stackKey = ValueKey<String>('stack'); const Key containerKey = ValueKey<String>('container'); final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); await tester.pumpWidget(boilerplateWidget( null, controller: controller, @@ -1508,9 +1526,10 @@ void main() { ); }); - testWidgets('DraggableScrollableSheet should respect NeverScrollableScrollPhysics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableSheet should respect NeverScrollableScrollPhysics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/121021 final DraggableScrollableController controller = DraggableScrollableController(); + addTearDown(controller.dispose); Widget buildFrame(ScrollPhysics? physics) { return MaterialApp( home: Scaffold( @@ -1553,7 +1572,7 @@ void main() { expect(controller.pixels, initPixels + 300.0); }); - testWidgets('DraggableScrollableSheet should not rebuild every frame while dragging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableSheet should not rebuild every frame while dragging', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/67219 int buildCount = 0; await tester.pumpWidget(MaterialApp( @@ -1600,9 +1619,11 @@ void main() { expect(buildCount, 2); }); - testWidgets('DraggableScrollableSheet controller can be changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableSheet controller can be changed', (WidgetTester tester) async { final DraggableScrollableController controller1 = DraggableScrollableController(); + addTearDown(controller1.dispose); final DraggableScrollableController controller2 = DraggableScrollableController(); + addTearDown(controller2.dispose); final List<double> loggedSizes = <double>[]; DraggableScrollableController controller = controller1; @@ -1658,7 +1679,7 @@ void main() { expect(loggedSizes, <double>[1.0].map((double v) => closeTo(v, precisionErrorTolerance))); }); - testWidgets('DraggableScrollableSheet controller can be changed while animating', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DraggableScrollableSheet controller can be changed while animating', (WidgetTester tester) async { final DraggableScrollableController controller1 = DraggableScrollableController(); final DraggableScrollableController controller2 = DraggableScrollableController(); diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index 3336a6feb6e7d..f03125c352bd8 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -14,11 +14,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Drag and drop - control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - control test', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; int dragStartedCount = 0; @@ -93,7 +94,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/76825 - testWidgets('Drag and drop - onLeave callback fires correctly with generic parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onLeave callback fires correctly with generic parameter', (WidgetTester tester) async { final Map<String,int> leftBehind = <String,int>{ 'Target 1': 0, 'Target 2': 0, @@ -168,7 +169,7 @@ void main() { expect(leftBehind['Target 2'], equals(1)); }); - testWidgets('Drag and drop - onLeave callback fires correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onLeave callback fires correctly', (WidgetTester tester) async { final Map<String,int> leftBehind = <String,int>{ 'Target 1': 0, 'Target 2': 0, @@ -244,7 +245,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/76825 - testWidgets('Drag and drop - onMove callback fires correctly with generic parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onMove callback fires correctly with generic parameter', (WidgetTester tester) async { final Map<String,int> targetMoveCount = <String,int>{ 'Target 1': 0, 'Target 2': 0, @@ -317,7 +318,7 @@ void main() { expect(targetMoveCount['Target 2'], equals(1)); }); - testWidgets('Drag and drop - onMove callback fires correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onMove callback fires correctly', (WidgetTester tester) async { final Map<String,int> targetMoveCount = <String,int>{ 'Target 1': 0, 'Target 2': 0, @@ -394,7 +395,7 @@ void main() { expect(targetMoveCount['Target 2'], equals(1)); }); - testWidgets('Drag and drop - dragging over button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - dragging over button', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; @@ -487,7 +488,7 @@ void main() { events.clear(); }); - testWidgets('Drag and drop - tapping button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - tapping button', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; @@ -544,7 +545,7 @@ void main() { events.clear(); }); - testWidgets('Drag and drop - long press draggable, short press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - long press draggable, short press', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; @@ -593,7 +594,7 @@ void main() { expect(events, isEmpty); }); - testWidgets('Drag and drop - long press draggable, long press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - long press draggable, long press', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; @@ -644,7 +645,7 @@ void main() { expect(events, equals(<String>['drop', 'details'])); }); - testWidgets('Drag and drop - horizontal and vertical draggables in vertical block', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - horizontal and vertical draggables in vertical block', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation, thirdLocation; @@ -754,7 +755,7 @@ void main() { events.clear(); }); - testWidgets('Drag and drop - horizontal and vertical draggables in horizontal block', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - horizontal and vertical draggables in horizontal block', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation, thirdLocation; @@ -913,7 +914,7 @@ void main() { ), ); } - testWidgets('Null axis draggable moves along all axes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Null axis draggable moves along all axes', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('N')); final Offset secondLocation = firstLocation + const Offset(300.0, 300.0); @@ -928,7 +929,7 @@ void main() { expect(tester.getTopLeft(find.text('N')), thirdLocation); }); - testWidgets('Horizontal axis draggable moves horizontally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal axis draggable moves horizontally', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('H')); final Offset secondLocation = firstLocation + const Offset(300.0, 0.0); @@ -943,7 +944,7 @@ void main() { expect(tester.getTopLeft(find.text('H')), thirdLocation); }); - testWidgets('Horizontal axis draggable does not move vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal axis draggable does not move vertically', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('H')); final Offset secondDragLocation = firstLocation + const Offset(300.0, 200.0); @@ -961,7 +962,7 @@ void main() { expect(tester.getTopLeft(find.text('H')), thirdWidgetLocation); }); - testWidgets('Vertical axis draggable moves vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical axis draggable moves vertically', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('V')); final Offset secondLocation = firstLocation + const Offset(0.0, 300.0); @@ -976,7 +977,7 @@ void main() { expect(tester.getTopLeft(find.text('V')), thirdLocation); }); - testWidgets('Vertical axis draggable does not move horizontally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical axis draggable does not move horizontally', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('V')); final Offset secondDragLocation = firstLocation + const Offset(200.0, 300.0); @@ -1042,7 +1043,7 @@ void main() { ); } - testWidgets('Null axis onDragUpdate called only if draggable moves in any direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Null axis onDragUpdate called only if draggable moves in any direction', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); @@ -1077,7 +1078,7 @@ void main() { expect(dragDelta.dy, 10); }); - testWidgets('Vertical axis onDragUpdate only called if draggable moves vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical axis onDragUpdate only called if draggable moves vertical', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); @@ -1112,7 +1113,7 @@ void main() { expect(dragDelta.dy, 10); }); - testWidgets('Horizontal axis onDragUpdate only called if draggable moves horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal axis onDragUpdate only called if draggable moves horizontal', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); @@ -1148,7 +1149,7 @@ void main() { }); }); - testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; @@ -1216,7 +1217,7 @@ void main() { expect(onDraggableCanceledCalled, isFalse); }); - testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDraggableCanceled called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; @@ -1293,7 +1294,84 @@ void main() { expect(onDraggableCanceledOffset, equals(Offset(secondLocation.dx, secondLocation.dy))); }); - testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List<int> accepted = <int>[]; + final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; + bool onDraggableCanceledCalled = false; + late Velocity onDraggableCanceledVelocity; + late Offset onDraggableCanceledOffset; + + await tester.pumpWidget(MaterialApp( + home: Column( + children: <Widget>[ + Draggable<int>( + data: 1, + feedback: const Text('Dragging'), + onDraggableCanceled: (Velocity velocity, Offset offset) { + onDraggableCanceledCalled = true; + onDraggableCanceledVelocity = velocity; + onDraggableCanceledOffset = offset; + }, + child: const Text('Source'), + ), + DragTarget<int>( + builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { + return const SizedBox( + height: 100.0, + child: Text('Target'), + ); + }, + onWillAcceptWithDetails: (DragTargetDetails<int> details) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isFalse); + + final Offset firstLocation = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDraggableCanceledCalled, isTrue); + expect(onDraggableCanceledVelocity, equals(Velocity.zero)); + expect(onDraggableCanceledOffset, equals(Offset(secondLocation.dx, secondLocation.dy))); + }); + + testWidgetsWithLeakTracking('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; @@ -1345,7 +1423,7 @@ void main() { expect(onDraggableCanceledOffset, equals(Offset(flingStart.dx, flingStart.dy) + const Offset(0.0, 100.0))); }); - testWidgets('Drag and drop - onDragEnd not called if dropped on non-accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDragEnd not called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; @@ -1421,7 +1499,83 @@ void main() { ); }); - testWidgets('Drag and drop - DragTarget rebuilds with and without rejected data when a rejected draggable enters and leaves', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDragEnd not called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List<int> accepted = <int>[]; + final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; + bool onDragEndCalled = false; + late DraggableDetails onDragEndDraggableDetails; + await tester.pumpWidget(MaterialApp( + home: Column( + children: <Widget>[ + Draggable<int>( + data: 1, + feedback: const Text('Dragging'), + onDragEnd: (DraggableDetails details) { + onDragEndCalled = true; + onDragEndDraggableDetails = details; + }, + child: const Text('Source'), + ), + DragTarget<int>( + builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { + return const SizedBox(height: 100.0, child: Text('Target')); + }, + onWillAcceptWithDetails: (DragTargetDetails<int> data) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isFalse); + + final Offset firstLocation = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragEndCalled, isTrue); + expect(onDragEndDraggableDetails, isNotNull); + expect(onDragEndDraggableDetails.wasAccepted, isFalse); + expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero)); + expect( + onDragEndDraggableDetails.offset, + equals(Offset(secondLocation.dx, secondLocation.dy - firstLocation.dy)), + ); + }); + + testWidgetsWithLeakTracking('Drag and drop - DragTarget rebuilds with and without rejected data when a rejected draggable enters and leaves', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ @@ -1475,7 +1629,7 @@ void main() { }); - testWidgets('Drag and drop - Can drag and drop over a non-accepting target multiple times', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - Can drag and drop over a non-accepting target multiple times', (WidgetTester tester) async { int numberOfTimesOnDraggableCanceledCalled = 0; await tester.pumpWidget(MaterialApp( home: Column( @@ -1557,7 +1711,7 @@ void main() { expect(find.text('Rejected'), findsNothing); }); - testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; @@ -1628,7 +1782,78 @@ void main() { expect(onDragCompletedCalled, isFalse); }); - testWidgets('Drag and drop - onDragEnd called if dropped on accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDragCompleted not called if dropped on non-accepting target with details', (WidgetTester tester) async { + final List<int> accepted = <int>[]; + final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(MaterialApp( + home: Column( + children: <Widget>[ + Draggable<int>( + data: 1, + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + }, + child: const Text('Source'), + ), + DragTarget<int>( + builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { + return const SizedBox( + height: 100.0, + child: Text('Target'), + ); + }, + onWillAcceptWithDetails: (DragTargetDetails<int> data) => false, + onAccept: accepted.add, + onAcceptWithDetails: acceptedDetails.add, + ), + ], + ), + )); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset firstLocation = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(acceptedDetails, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + }); + + testWidgetsWithLeakTracking('Drag and drop - onDragEnd called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; @@ -1704,7 +1929,7 @@ void main() { expect(onDragEndDraggableDetails.offset, equals(expectedDropOffset)); }); - testWidgets('DragTarget does not call onDragEnd when remove from the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DragTarget does not call onDragEnd when remove from the tree', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; int timesOnDragEndCalled = 0; @@ -1770,7 +1995,7 @@ void main() { await tester.pump(); }); - testWidgets('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; @@ -1838,7 +2063,7 @@ void main() { expect(onDragCompletedCalled, isTrue); }); - testWidgets('Drag and drop - allow pass through of unaccepted data test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - allow pass through of unaccepted data test', (WidgetTester tester) async { final List<int> acceptedInts = <int>[]; final List<DragTargetDetails<int>> acceptedIntsDetails = <DragTargetDetails<int>>[]; final List<double> acceptedDoubles = <double>[]; @@ -1972,7 +2197,7 @@ void main() { expect(find.text('DoubleDragging'), findsNothing); }); - testWidgets('Drag and drop - allow pass through of unaccepted data twice test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - allow pass through of unaccepted data twice test', (WidgetTester tester) async { final List<DragTargetData> acceptedDragTargetDatas = <DragTargetData>[]; final List<DragTargetDetails<DragTargetData>> acceptedDragTargetDataDetails = <DragTargetDetails<DragTargetData>>[]; final List<ExtendedDragTargetData> acceptedExtendedDragTargetDatas = <ExtendedDragTargetData>[]; @@ -2040,7 +2265,7 @@ void main() { } }); - testWidgets('Drag and drop - maxSimultaneousDrags', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - maxSimultaneousDrags', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; @@ -2167,14 +2392,17 @@ void main() { expect(find.text('Target'), findsOneWidget); }); - testWidgets('Draggable disposes recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Draggable disposes recognizer', (WidgetTester tester) async { + late final OverlayEntry entry; + addTearDown(() => entry..remove()..dispose()); + bool didTap = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + entry = OverlayEntry( builder: (BuildContext context) => GestureDetector( onTap: () { didTap = true; @@ -2206,7 +2434,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/6128. - testWidgets('Draggable plays nice with onTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Draggable plays nice with onTap', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2239,7 +2467,7 @@ void main() { await secondGesture.up(); }); - testWidgets('DragTarget does not set state when remove from the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DragTarget does not set state when remove from the tree', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; @@ -2301,7 +2529,7 @@ void main() { await tester.pump(); }); - testWidgets('Drag and drop - remove draggable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - remove draggable', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; @@ -2381,7 +2609,7 @@ void main() { expect(find.text('Target'), findsOneWidget); }); - testWidgets('Tap above long-press draggable works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap above long-press draggable works', (WidgetTester tester) async { final List<String> events = <String>[]; await tester.pumpWidget(MaterialApp( @@ -2405,7 +2633,7 @@ void main() { expect(events, equals(<String>['tap'])); }); - testWidgets('long-press draggable calls onDragEnd called if dropped on accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press draggable calls onDragEnd called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; @@ -2492,7 +2720,7 @@ void main() { expect(onDragEndDraggableDetails.offset, equals(expectedDropOffset)); }); - testWidgets('long-press draggable calls onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press draggable calls onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; @@ -2568,7 +2796,7 @@ void main() { expect(onDragCompletedCalled, isTrue); }); - testWidgets('long-press draggable calls onDragStartedCalled after long press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press draggable calls onDragStartedCalled after long press', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( @@ -2601,7 +2829,7 @@ void main() { expect(onDragStartedCalled, isTrue); }); - testWidgets('Custom long press delay for LongPressDraggable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom long press delay for LongPressDraggable', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( @@ -2635,7 +2863,7 @@ void main() { expect(onDragStartedCalled, isTrue); }); - testWidgets('Default long press delay for LongPressDraggable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default long press delay for LongPressDraggable', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( @@ -2668,23 +2896,23 @@ void main() { expect(onDragStartedCalled, isTrue); }); - testWidgets('long-press draggable calls Haptic Feedback onStart', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press draggable calls Haptic Feedback onStart', (WidgetTester tester) async { await _testLongPressDraggableHapticFeedback(tester: tester, hapticFeedbackOnStart: true, expectedHapticFeedbackCount: 1); }); - testWidgets('long-press draggable can disable Haptic Feedback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long-press draggable can disable Haptic Feedback', (WidgetTester tester) async { await _testLongPressDraggableHapticFeedback(tester: tester, hapticFeedbackOnStart: false, expectedHapticFeedbackCount: 0); }); - testWidgets('Drag feedback with child anchor positions correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag feedback with child anchor positions correctly', (WidgetTester tester) async { await _testChildAnchorFeedbackPosition(tester: tester); }); - testWidgets('Drag feedback with child anchor within a non-global Overlay positions correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag feedback with child anchor within a non-global Overlay positions correctly', (WidgetTester tester) async { await _testChildAnchorFeedbackPosition(tester: tester, left: 100.0, top: 100.0); }); - testWidgets('Drag feedback is put on root overlay with [rootOverlay] flag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag feedback is put on root overlay with [rootOverlay] flag', (WidgetTester tester) async { final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> childNavigatorKey = GlobalKey<NavigatorState>(); // Create a [MaterialApp], with a nested [Navigator], which has the @@ -2752,7 +2980,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/72483 - testWidgets('Drag and drop - DragTarget<Object> can accept Draggable<int> data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - DragTarget<Object> can accept Draggable<int> data', (WidgetTester tester) async { final List<Object> accepted = <Object>[]; await tester.pumpWidget(MaterialApp( home: Column( @@ -2788,7 +3016,7 @@ void main() { expect(accepted, equals(<int>[1])); }); - testWidgets('Drag and drop - DragTarget<int> can accept Draggable<Object> data when runtime type is int', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - DragTarget<int> can accept Draggable<Object> data when runtime type is int', (WidgetTester tester) async { final List<int> accepted = <int>[]; await tester.pumpWidget(MaterialApp( home: Column( @@ -2824,7 +3052,7 @@ void main() { expect(accepted, equals(<int>[1])); }); - testWidgets('Drag and drop - DragTarget<int> should not accept Draggable<Object> data when runtime type null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - DragTarget<int> should not accept Draggable<Object> data when runtime type null', (WidgetTester tester) async { final List<int> accepted = <int>[]; bool isReceiveNullDataForCheck = false; await tester.pumpWidget(MaterialApp( @@ -2867,7 +3095,7 @@ void main() { expect(isReceiveNullDataForCheck, true); }); - testWidgets('Drag and drop can contribute semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop can contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( home: ListView( @@ -3033,7 +3261,7 @@ void main() { semantics.dispose(); }); - testWidgets('Drag and drop - when a dragAnchorStrategy is provided it gets called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag and drop - when a dragAnchorStrategy is provided it gets called', (WidgetTester tester) async { bool dragAnchorStrategyCalled = false; await tester.pumpWidget(MaterialApp( @@ -3057,7 +3285,7 @@ void main() { expect(dragAnchorStrategyCalled, true); }); - testWidgets('configurable Draggable hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('configurable Draggable hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild; await tester.pumpWidget( @@ -3077,7 +3305,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/92083 - testWidgets('feedback respect the MouseRegion cursor configure', (WidgetTester tester) async { + testWidgetsWithLeakTracking('feedback respect the MouseRegion cursor configure', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Column( @@ -3105,7 +3333,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grabbing); }); - testWidgets('configurable feedback ignore pointer behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('configurable feedback ignore pointer behavior', (WidgetTester tester) async { bool onTap = false; await tester.pumpWidget( MaterialApp( @@ -3134,7 +3362,7 @@ void main() { expect(onTap, true); }); - testWidgets('configurable feedback ignore pointer behavior - LongPressDraggable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('configurable feedback ignore pointer behavior - LongPressDraggable', (WidgetTester tester) async { bool onTap = false; await tester.pumpWidget( MaterialApp( @@ -3165,7 +3393,7 @@ void main() { expect(onTap, true); }); - testWidgets('configurable DragTarget hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('configurable DragTarget hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild; await tester.pumpWidget( @@ -3186,7 +3414,7 @@ void main() { expect(tester.widget<MetaData>(find.byType(MetaData)).behavior, hitTestBehavior); }); - testWidgets('LongPressDraggable.dragAnchorStrategy', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LongPressDraggable.dragAnchorStrategy', (WidgetTester tester) async { const Widget widget1 = Placeholder(key: ValueKey<int>(1)); const Widget widget2 = Placeholder(key: ValueKey<int>(2)); Offset dummyStrategy(Draggable<Object> draggable, BuildContext context, Offset position) => Offset.zero; @@ -3196,7 +3424,7 @@ void main() { expect(LongPressDraggable<int>(feedback: widget2, dragAnchorStrategy: dummyStrategy, child: widget1).dragAnchorStrategy, dummyStrategy); }); - testWidgets('Test allowedButtonsFilter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test allowedButtonsFilter', (WidgetTester tester) async { Widget build(bool Function(int buttons)? allowedButtonsFilter) { return MaterialApp( home: Draggable<int>( @@ -3237,6 +3465,16 @@ void main() { expect(find.text('Dragging'), findsNothing); await gesture3.up(); }); + + testWidgetsWithLeakTracking('throws error when both onWillAccept and onWillAcceptWithDetails are provided', (WidgetTester tester) async { + expect(() => DragTarget<int>( + builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { + return const SizedBox(height: 100.0, child: Text('Target')); + }, + onWillAccept: (int? data) => true, + onWillAcceptWithDetails: (DragTargetDetails<int> details) => false, + ), throwsAssertionError); + }); } Future<void> _testLongPressDraggableHapticFeedback({ required WidgetTester tester, required bool hapticFeedbackOnStart, required int expectedHapticFeedbackCount }) async { diff --git a/packages/flutter/test/widgets/drawer_test.dart b/packages/flutter/test/widgets/drawer_test.dart index cb3aba7fd8280..d1a7ce8f3b22c 100644 --- a/packages/flutter/test/widgets/drawer_test.dart +++ b/packages/flutter/test/widgets/drawer_test.dart @@ -8,12 +8,13 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Drawer control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer control test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); late BuildContext savedContext; await tester.pumpWidget( @@ -44,7 +45,7 @@ void main() { expect(find.text('drawer'), findsNothing); }); - testWidgets('Drawer tap test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer tap test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -76,7 +77,7 @@ void main() { expect(find.text('drawer'), findsNothing); }); - testWidgets('Drawer hover test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer hover test', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final List<String> logs = <String>[]; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); @@ -146,7 +147,7 @@ void main() { logs.clear(); }); - testWidgets('Drawer drag cancel resume (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer drag cancel resume (LTR)', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -197,7 +198,7 @@ void main() { await gesture.up(); }); - testWidgets('Drawer drag cancel resume (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer drag cancel resume (RTL)', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); await tester.pumpWidget( MaterialApp( @@ -251,7 +252,7 @@ void main() { await gesture.up(); }); - testWidgets('Drawer navigator back button', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer navigator back button', (WidgetTester tester) async { final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); bool buttonPressed = false; @@ -299,7 +300,7 @@ void main() { expect(buttonPressed, equals(true)); }); - testWidgets('Dismissible ModalBarrier includes button in semantic tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible ModalBarrier includes button in semantic tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -326,7 +327,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); @@ -354,7 +355,7 @@ void main() { semantics.dispose(); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('Drawer contains route semantics flags', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drawer contains route semantics flags', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); diff --git a/packages/flutter/test/widgets/dual_transition_builder_test.dart b/packages/flutter/test/widgets/dual_transition_builder_test.dart index 4ffca538d978e..531aeead57ea3 100644 --- a/packages/flutter/test/widgets/dual_transition_builder_test.dart +++ b/packages/flutter/test/widgets/dual_transition_builder_test.dart @@ -4,13 +4,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('runs animations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('runs animations', (WidgetTester tester) async { final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(milliseconds: 300), ); + addTearDown(controller.dispose); await tester.pumpWidget(Center( child: DualTransitionBuilder( @@ -74,11 +76,12 @@ void main() { expect(_getOpacity(tester), 1.0); }); - testWidgets('keeps state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keeps state', (WidgetTester tester) async { final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(milliseconds: 300), ); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -138,11 +141,13 @@ void main() { expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); }); - testWidgets('does not jump when interrupted - forward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not jump when interrupted - forward', (WidgetTester tester) async { final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(milliseconds: 300), ); + addTearDown(controller.dispose); + await tester.pumpWidget(Center( child: DualTransitionBuilder( animation: controller, @@ -202,12 +207,14 @@ void main() { expect(_getOpacity(tester), 1.0); }); - testWidgets('does not jump when interrupted - reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not jump when interrupted - reverse', (WidgetTester tester) async { final AnimationController controller = AnimationController( value: 1.0, vsync: const TestVSync(), duration: const Duration(milliseconds: 300), ); + addTearDown(controller.dispose); + await tester.pumpWidget(Center( child: DualTransitionBuilder( animation: controller, diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart index 62640cb5c1a2d..ab1d8137b721a 100644 --- a/packages/flutter/test/widgets/editable_text_cursor_test.dart +++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart @@ -15,24 +15,34 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'editable_text_utils.dart'; -final TextEditingController controller = TextEditingController(); -final FocusNode focusNode = FocusNode(); -final FocusScopeNode focusScopeNode = FocusScopeNode(); const TextStyle textStyle = TextStyle(); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); void main() { + late TextEditingController controller; + late FocusNode focusNode; + late FocusScopeNode focusScopeNode; + setUp(() async { // Fill the clipboard so that the Paste option is available in the text // selection menu. await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); + controller = TextEditingController(); + focusNode = FocusNode(); + focusScopeNode = FocusScopeNode(); + }); + + tearDown(() { + controller.dispose(); + focusNode.dispose(); + focusScopeNode.dispose(); }); - testWidgets('cursor has expected width, height, and radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor has expected width, height, and radius', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -58,7 +68,7 @@ void main() { expect(editableText.cursorRadius!.x, 2.0); }); - testWidgets('cursor layout has correct width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct width', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); @@ -69,8 +79,8 @@ void main() { child: EditableText( backgroundCursorColor: Colors.grey, key: editableTextKey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -114,7 +124,7 @@ void main() { EditableText.debugDeterministicCursor = false; }); - testWidgets('cursor layout has correct radius', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct radius', (WidgetTester tester) async { final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); late String changedValue; @@ -124,8 +134,8 @@ void main() { child: EditableText( backgroundCursorColor: Colors.grey, key: editableTextKey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -169,7 +179,7 @@ void main() { ); }); - testWidgets('Cursor animates on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor animates on iOS', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -220,7 +230,7 @@ void main() { await verifyKeyFrame(opacity: 1.0, at: 1000000); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('Cursor does not animate on non-iOS platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor does not animate on non-iOS platforms', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material(child: TextField(maxLines: 3)), @@ -239,7 +249,7 @@ void main() { } }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('Cursor does not animate on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor does not animate on Android', (WidgetTester tester) async { final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); const Widget widget = MaterialApp( home: Material( @@ -277,9 +287,14 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); expect(renderEditable.cursorColor!.alpha, 0); expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); - }); - - testWidgets('Cursor does not animates when debugDeterministicCursor is set', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 5}, + )); + + testWidgetsWithLeakTracking('Cursor does not animates when debugDeterministicCursor is set', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); const Widget widget = MaterialApp( @@ -315,9 +330,15 @@ void main() { expect(renderEditable, paints..rrect(color: defaultCursorColor)); EditableText.debugDeterministicCursor = false; - }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - - testWidgets('Cursor does not animate on Android when debugDeterministicCursor is set', (WidgetTester tester) async { + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 6}, + )); + + testWidgetsWithLeakTracking('Cursor does not animate on Android when debugDeterministicCursor is set', (WidgetTester tester) async { final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value); EditableText.debugDeterministicCursor = true; const Widget widget = MaterialApp( @@ -354,17 +375,23 @@ void main() { expect(renderEditable, paints..rect(color: defaultCursorColor)); EditableText.debugDeterministicCursor = false; - }); - - testWidgets('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 4}, + )); + + testWidgetsWithLeakTracking('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.macOS; const String testText = 'Some text long enough to move the cursor around'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; + final Widget widget = MaterialApp( home: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(fontSize: 20.0), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -431,9 +458,15 @@ void main() { expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); debugDefaultTargetPlatformOverride = null; - }, variant: KeySimulatorTransitModeVariant.all()); - - testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async { + }, + variant: KeySimulatorTransitModeVariant.all(), + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 18}, + )); + + testWidgetsWithLeakTracking('Cursor does not show when showCursor set to false', (WidgetTester tester) async { const Widget widget = MaterialApp( home: Material( child: TextField( @@ -459,11 +492,15 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0)); - }); - - testWidgets('Cursor does not show when not focused', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 3}, + )); + + testWidgetsWithLeakTracking('Cursor does not show when not focused', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/106512 . - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MaterialApp( home: Material( @@ -490,9 +527,14 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(renderEditable, isNot(paintsExactlyCountTimes(#drawRect, 0))); - }); - - testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 2}, + )); + + testWidgetsWithLeakTracking('Cursor radius is 2.0', (WidgetTester tester) async { const Widget widget = MaterialApp( home: Material( child: TextField( @@ -508,10 +550,9 @@ void main() { expect(renderEditable.cursorRadius, const Radius.circular(2.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Cursor gets placed correctly after going out of bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor gets placed correctly after going out of bounds', (WidgetTester tester) async { const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MediaQuery( @@ -603,10 +644,9 @@ void main() { expect(controller.selection.baseOffset, 10); }); - testWidgets('Updating the floating cursor correctly moves the cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updating the floating cursor correctly moves the cursor', (WidgetTester tester) async { const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MediaQuery( @@ -660,10 +700,9 @@ void main() { expect(controller.selection.baseOffset, 10); }); - testWidgets('Updating the floating cursor can end without update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updating the floating cursor can end without update', (WidgetTester tester) async { const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MediaQuery( @@ -704,10 +743,9 @@ void main() { expect(tester.takeException(), null); }); - testWidgets("Drag the floating cursor, it won't blink.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Drag the floating cursor, it won't blink.", (WidgetTester tester) async { const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MediaQuery( @@ -771,7 +809,7 @@ void main() { await checkCursorBlinking(); }); - testWidgets('Turning showCursor off stops the cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Turning showCursor off stops the cursor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/108187. final bool debugDeterministicCursor = EditableText.debugDeterministicCursor; // This doesn't really matter. @@ -820,10 +858,9 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/pull/30475. - testWidgets('Trying to select with the floating cursor does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Trying to select with the floating cursor does not crash', (WidgetTester tester) async { const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MediaQuery( @@ -886,12 +923,10 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('autofocus sets cursor to the end of text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autofocus sets cursor to the end of text', (WidgetTester tester) async { const String text = 'hello world'; - final FocusScopeNode focusScopeNode = FocusScopeNode(); - final FocusNode focusNode = FocusNode(); - controller.text = text; + await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -918,12 +953,10 @@ void main() { expect(controller.selection.baseOffset, text.length); }); - testWidgets('Floating cursor is painted', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('Floating cursor is painted', (WidgetTester tester) async { const TextStyle textStyle = TextStyle(); const String text = 'hello world this is fun and cool and awesome!'; controller.text = text; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MaterialApp( @@ -996,9 +1029,15 @@ void main() { editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); await tester.pumpAndSettle(); debugDefaultTargetPlatformOverride = null; - }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - - testWidgets('cursor layout', (WidgetTester tester) async { + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 4}, + )); + + testWidgetsWithLeakTracking('cursor layout', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); @@ -1012,8 +1051,8 @@ void main() { EditableText( backgroundCursorColor: Colors.grey, key: editableTextKey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018(platform: TargetPlatform.iOS).black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -1059,7 +1098,7 @@ void main() { EditableText.debugDeterministicCursor = false; }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('cursor layout has correct height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cursor layout has correct height', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); @@ -1073,8 +1112,8 @@ void main() { EditableText( backgroundCursorColor: Colors.grey, key: editableTextKey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018(platform: TargetPlatform.iOS).black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -1121,7 +1160,7 @@ void main() { EditableText.debugDeterministicCursor = false; }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('password briefly does not show last character when disabled by system', (WidgetTester tester) async { + testWidgetsWithLeakTracking('password briefly does not show last character when disabled by system', (WidgetTester tester) async { final bool debugDeterministicCursor = EditableText.debugDeterministicCursor; EditableText.debugDeterministicCursor = false; addTearDown(() { @@ -1157,23 +1196,23 @@ void main() { expect((findRenderEditable(tester).text! as TextSpan).text, '•••'); }); - testWidgets('getLocalRectForCaret with empty text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getLocalRectForCaret with empty text', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; addTearDown(() { EditableText.debugDeterministicCursor = false; }); const String text = '12'; - final TextEditingController controller = TextEditingController.fromValue( const TextEditingValue( text: text, selection: TextSelection.collapsed(offset: text.length), ), ); + addTearDown(controller.dispose); final Widget widget = EditableText( autofocus: true, backgroundCursorColor: Colors.grey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(fontSize: 20), textAlign: TextAlign.center, keyboardType: TextInputType.text, @@ -1202,21 +1241,23 @@ void main() { expect(controller.text, isEmpty); }); - testWidgets('Caret center space test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret center space test', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; addTearDown(() { EditableText.debugDeterministicCursor = false; }); final String text = 'test${' ' * 1000}'; + final TextEditingController controller = TextEditingController.fromValue( + TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length, affinity: TextAffinity.upstream), + ), + ); + addTearDown(controller.dispose); final Widget widget = EditableText( autofocus: true, backgroundCursorColor: Colors.grey, - controller: TextEditingController.fromValue( - TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: text.length, affinity: TextAffinity.upstream), - ), - ), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), textAlign: TextAlign.center, keyboardType: TextInputType.text, @@ -1244,25 +1285,31 @@ void main() { renderEditable, paints..rect(color: cursorColor, rect: caretRect), ); - }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 - - testWidgets('getLocalRectForCaret reports the real caret Rect', (WidgetTester tester) async { + }, + skip: isBrowser && !isCanvasKit, // https://github.com/flutter/flutter/issues/56308 + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 1}, + )); + + testWidgetsWithLeakTracking('getLocalRectForCaret reports the real caret Rect', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; addTearDown(() { EditableText.debugDeterministicCursor = false; }); final String text = 'test${' ' * 50}\n' '2nd line\n' '\n'; - final TextEditingController controller = TextEditingController.fromValue(TextEditingValue( text: text, selection: const TextSelection.collapsed(offset: 0), )); + addTearDown(controller.dispose); final Widget widget = EditableText( autofocus: true, backgroundCursorColor: Colors.grey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle(fontSize: 20), textAlign: TextAlign.center, keyboardType: TextInputType.text, @@ -1288,5 +1335,11 @@ void main() { paints..rect(color: cursorColor, rect: localRect.shift(editableTextRect.topLeft)), ); } - }, variant: TargetPlatformVariant.all()); + }, + variant: TargetPlatformVariant.all(), + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 792}, + )); } diff --git a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart index b6efb36c221db..449fe0029eb50 100644 --- a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart +++ b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'clipboard_utils.dart'; import 'keyboard_utils.dart'; @@ -97,7 +98,7 @@ void main() { group('backspace', () { const LogicalKeyboardKey trigger = LogicalKeyboardKey.backspace; - testWidgets('backspace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace', (WidgetTester tester) async { controller.text = testText; // Move the selection to the beginning of the 2nd line (after the newline // character). @@ -122,7 +123,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('backspace readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 20, @@ -140,7 +141,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('backspace at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -163,7 +164,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('backspace at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -187,7 +188,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('backspace inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -208,7 +209,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('backspace at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('backspace at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -233,7 +234,7 @@ void main() { group('delete: ', () { const LogicalKeyboardKey trigger = LogicalKeyboardKey.delete; - testWidgets('delete', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete', (WidgetTester tester) async { controller.text = testText; // Move the selection to the beginning of the 2nd line (after the newline // character). @@ -259,7 +260,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('delete readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 20, @@ -277,7 +278,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('delete at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -300,7 +301,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('delete at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -324,7 +325,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('delete inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -345,7 +346,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('delete at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('delete at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -371,7 +372,7 @@ void main() { // This shares the same logic as backspace. const LogicalKeyboardKey trigger = LogicalKeyboardKey.delete; - testWidgets('inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection( baseOffset: 9, @@ -392,7 +393,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('at the boundaries of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at the boundaries of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection( baseOffset: 8, @@ -413,7 +414,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('cross-cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cross-cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection( baseOffset: 1, @@ -434,7 +435,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('cross-cluster obscured text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cross-cluster obscured text', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection( baseOffset: 1, @@ -464,7 +465,7 @@ void main() { return SingleActivator(trigger, control: !isApple, alt: isApple); } - testWidgets('WordModifier-backspace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WordModifier-backspace', (WidgetTester tester) async { controller.text = testText; // Place the caret before "people". controller.selection = const TextSelection.collapsed( @@ -489,7 +490,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 29, @@ -507,7 +508,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -530,7 +531,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -554,7 +555,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -575,7 +576,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -604,7 +605,7 @@ void main() { return SingleActivator(trigger, control: !isApple, alt: isApple); } - testWidgets('WordModifier-delete', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WordModifier-delete', (WidgetTester tester) async { controller.text = testText; // Place the caret after "all". controller.selection = const TextSelection.collapsed( @@ -629,7 +630,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 23, @@ -647,7 +648,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -670,7 +671,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -687,7 +688,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -708,7 +709,7 @@ void main() { ); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -737,7 +738,7 @@ void main() { return SingleActivator(trigger, meta: isApple, alt: !isApple); } - testWidgets('alt-backspace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alt-backspace', (WidgetTester tester) async { controller.text = testText; // Place the caret before "people". controller.selection = const TextSelection.collapsed( @@ -762,7 +763,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('softwrap line boundary, upstream', (WidgetTester tester) async { controller.text = testSoftwrapText; // Place the caret at the end of the 2nd line. controller.selection = const TextSelection.collapsed( @@ -786,7 +787,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('softwrap line boundary, downstream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('softwrap line boundary, downstream', (WidgetTester tester) async { controller.text = testSoftwrapText; // Place the caret at the beginning of the 3rd line. controller.selection = const TextSelection.collapsed( @@ -803,7 +804,7 @@ void main() { expect(controller.text, testSoftwrapText); }, variant: TargetPlatformVariant.all()); - testWidgets('readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 29, @@ -821,7 +822,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -844,7 +845,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -867,7 +868,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -888,7 +889,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -917,7 +918,7 @@ void main() { return SingleActivator(trigger, meta: isApple, alt: !isApple); } - testWidgets('alt-delete', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alt-delete', (WidgetTester tester) async { controller.text = testText; // Place the caret after "all". controller.selection = const TextSelection.collapsed( @@ -942,7 +943,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('softwrap line boundary, upstream', (WidgetTester tester) async { controller.text = testSoftwrapText; // Place the caret at the end of the 2nd line. controller.selection = const TextSelection.collapsed( @@ -961,7 +962,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('softwrap line boundary, downstream', (WidgetTester tester) async { + testWidgetsWithLeakTracking('softwrap line boundary, downstream', (WidgetTester tester) async { controller.text = testSoftwrapText; // Place the caret at the beginning of the 3rd line. controller.selection = const TextSelection.collapsed( @@ -984,7 +985,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('readonly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('readonly', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 23, @@ -1002,7 +1003,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -1025,7 +1026,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -1042,7 +1043,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('inside of a cluster', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inside of a cluster', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 1, @@ -1063,7 +1064,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('at cluster boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at cluster boundary', (WidgetTester tester) async { controller.text = testCluster; controller.selection = const TextSelection.collapsed( offset: 8, @@ -1089,7 +1090,7 @@ void main() { group('left', () { const LogicalKeyboardKey trigger = LogicalKeyboardKey.arrowLeft; - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -1109,7 +1110,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('base arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('base arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 20, @@ -1123,7 +1124,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1137,7 +1138,7 @@ void main() { )); }, variant: allExceptApple); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1155,7 +1156,7 @@ void main() { group('right', () { const LogicalKeyboardKey trigger = LogicalKeyboardKey.arrowRight; - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -1172,7 +1173,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('base arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('base arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 20, @@ -1186,7 +1187,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1200,7 +1201,7 @@ void main() { )); }, variant: allExceptApple); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1217,7 +1218,7 @@ void main() { }); group('With initial non-collapsed selection', () { - testWidgets('base arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('base arrow key movement', (WidgetTester tester) async { controller.text = testText; // The word "all" is selected. controller.selection = const TextSelection( @@ -1269,7 +1270,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; // "good" to "come" is selected. controller.selection = const TextSelection( @@ -1322,7 +1323,7 @@ void main() { )); }, variant: allExceptApple); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; // "good" to "come" is selected. controller.selection = const TextSelection( @@ -1378,7 +1379,7 @@ void main() { }); group('vertical movement', () { - testWidgets('at start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at start', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -1411,7 +1412,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('at end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('at end', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 72, @@ -1438,7 +1439,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('run', (WidgetTester tester) async { + testWidgetsWithLeakTracking('run', (WidgetTester tester) async { controller.text = 'aa\n' // 3 'a\n' // 3 + 2 = 5 @@ -1525,7 +1526,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('run with page down/up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('run with page down/up', (WidgetTester tester) async { controller.text = 'aa\n' // 3 'a\n' // 3 + 2 = 5 @@ -1560,7 +1561,7 @@ void main() { )); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.macOS})); // intended: on macOS Page Up/Down only scrolls - testWidgets('run can be interrupted by layout changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('run can be interrupted by layout changes', (WidgetTester tester) async { controller.text = 'aa\n' // 3 'a\n' // 3 + 2 = 5 @@ -1589,7 +1590,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('run can be interrupted by selection changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('run can be interrupted by selection changes', (WidgetTester tester) async { controller.text = 'aa\n' // 3 'a\n' // 3 + 2 = 5 @@ -1624,7 +1625,7 @@ void main() { )); }, variant: TargetPlatformVariant.all()); - testWidgets('long run with fractional text height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long run with fractional text height', (WidgetTester tester) async { controller.text = "${'źdźbło\n' * 49}źdźbło"; controller.selection = const TextSelection.collapsed(offset: 2); await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 13.0, height: 1.17))); @@ -1658,7 +1659,7 @@ void main() { group('macOS shortcuts', () { final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS); - testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowLeft', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1672,7 +1673,7 @@ void main() { )); }, variant: macOSOnly); - testWidgets('word modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowRight', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1686,7 +1687,7 @@ void main() { )); }, variant: macOSOnly); - testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowLeft', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1700,7 +1701,7 @@ void main() { )); }, variant: macOSOnly); - testWidgets('line modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowRight', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1715,7 +1716,7 @@ void main() { )); }, variant: macOSOnly); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; // "good" to "come" is selected. controller.selection = const TextSelection( @@ -1768,7 +1769,7 @@ void main() { )); }, variant: macOSOnly); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; // "good" to "come" is selected. controller.selection = const TextSelection( @@ -1828,7 +1829,7 @@ void main() { const TargetPlatformVariant appleOnly = TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.iOS }); group('macOS shortcuts', () { - testWidgets('word modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowLeft', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1840,7 +1841,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 7)); }, variant: appleOnly); - testWidgets('word modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrowRight', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 7, // Before the first "the" @@ -1852,7 +1853,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 7)); }, variant: appleOnly); - testWidgets('line modifier + arrowLeft', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowLeft', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1864,7 +1865,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 24,)); }, variant: appleOnly); - testWidgets('line modifier + arrowRight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrowRight', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 24, // Before the "good". @@ -1878,7 +1879,7 @@ void main() { )); }, variant: appleOnly); - testWidgets('word modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('word modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection( baseOffset: 24, @@ -1931,7 +1932,7 @@ void main() { )); }, variant: appleOnly); - testWidgets('line modifier + arrow key movement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('line modifier + arrow key movement', (WidgetTester tester) async { controller.text = testText; // "good" to "come" is selected. controller.selection = const TextSelection( @@ -1988,7 +1989,7 @@ void main() { }, variant: appleOnly); }); - testWidgets('vertical movement outside of selection', + testWidgetsWithLeakTracking('vertical movement outside of selection', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( @@ -2014,7 +2015,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('select all non apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all non apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -2027,7 +2028,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 0)); }, variant: allExceptApple); - testWidgets('select all apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed( offset: 0, @@ -2040,7 +2041,7 @@ void main() { expect(controller.selection, const TextSelection.collapsed(offset: 0)); }, variant: appleOnly); - testWidgets('copy non apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('copy non apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, @@ -2055,7 +2056,7 @@ void main() { expect(clipboardData['text'], 'empty'); }, variant: allExceptApple); - testWidgets('copy apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('copy apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, @@ -2070,7 +2071,7 @@ void main() { expect(clipboardData['text'], 'empty'); }, variant: appleOnly); - testWidgets('cut non apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cut non apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, @@ -2089,7 +2090,7 @@ void main() { )); }, variant: allExceptApple); - testWidgets('cut apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cut apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, @@ -2108,7 +2109,7 @@ void main() { )); }, variant: appleOnly); - testWidgets('paste non apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('paste non apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed(offset: 0); mockClipboard.clipboardData = <String, dynamic>{ @@ -2121,7 +2122,7 @@ void main() { expect(controller.text, testText); }, variant: allExceptApple); - testWidgets('paste apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('paste apple', (WidgetTester tester) async { controller.text = testText; controller.selection = const TextSelection.collapsed(offset: 0); mockClipboard.clipboardData = <String, dynamic>{ @@ -2137,7 +2138,7 @@ void main() { }, skip: !kIsWeb);// [intended] specific tests target web. group('Web does accept', () { - testWidgets('select up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select up', (WidgetTester tester) async { const SingleActivator selectUp = SingleActivator(LogicalKeyboardKey.arrowUp, shift: true); controller.text = testVerticalText; @@ -2159,7 +2160,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select down', (WidgetTester tester) async { const SingleActivator selectDown = SingleActivator(LogicalKeyboardKey.arrowDown, shift: true); controller.text = testVerticalText; @@ -2181,7 +2182,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select all up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all up', (WidgetTester tester) async { final bool isMacOS = defaultTargetPlatform == TargetPlatform.macOS; final SingleActivator selectAllUp = isMacOS ? const SingleActivator(LogicalKeyboardKey.arrowUp, @@ -2207,7 +2208,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select all down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all down', (WidgetTester tester) async { final bool isMacOS = defaultTargetPlatform == TargetPlatform.macOS; final SingleActivator selectAllDown = isMacOS ? const SingleActivator(LogicalKeyboardKey.arrowDown, @@ -2233,7 +2234,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select left', (WidgetTester tester) async { const SingleActivator selectLeft = SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true); controller.text = 'testing'; @@ -2253,7 +2254,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select right', (WidgetTester tester) async { const SingleActivator selectRight = SingleActivator(LogicalKeyboardKey.arrowRight, shift: true); controller.text = 'testing'; @@ -2273,7 +2274,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets( + testWidgetsWithLeakTracking( 'select left should not expand selection if selection is disabled', (WidgetTester tester) async { const SingleActivator selectLeft = @@ -2296,7 +2297,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets( + testWidgetsWithLeakTracking( 'select right should not expand selection if selection is disabled', (WidgetTester tester) async { const SingleActivator selectRight = @@ -2317,7 +2318,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select all left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all left', (WidgetTester tester) async { final bool isMacOS = defaultTargetPlatform == TargetPlatform.macOS; final SingleActivator selectAllLeft = isMacOS ? const SingleActivator(LogicalKeyboardKey.arrowLeft, @@ -2341,7 +2342,7 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('select all right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all right', (WidgetTester tester) async { final bool isMacOS = defaultTargetPlatform == TargetPlatform.macOS; final SingleActivator selectAllRight = isMacOS ? const SingleActivator(LogicalKeyboardKey.arrowRight, diff --git a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart index 2c4a9edbc5576..33b51dc4ccd42 100644 --- a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart +++ b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/foundation/constants.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { _TestSliverPersistentHeaderDelegate({ @@ -40,11 +41,23 @@ class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate void main() { const TextStyle textStyle = TextStyle(); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); - final FocusNode focusNode = FocusNode(); - testWidgets('tapping on a partly visible editable brings it fully on screen', (WidgetTester tester) async { + late TextEditingController controller; + late FocusNode focusNode; + + setUp(() { + controller = TextEditingController(); + focusNode = FocusNode(); + }); + + tearDown(() { + controller.dispose(); + focusNode.dispose(); + }); + + testWidgetsWithLeakTracking('tapping on a partly visible editable brings it fully on screen', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( home: Center( @@ -80,10 +93,9 @@ void main() { expect(scrollController.offset, 0.0); }); - testWidgets('tapping on a partly visible editable brings it fully on screen with scrollInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapping on a partly visible editable brings it fully on screen with scrollInsets', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( home: Center( @@ -126,10 +138,9 @@ void main() { expect(scrollController.offset, greaterThan(200.0 - 50.0 - 5.0)); }); - testWidgets('editable comes back on screen when entering text while it is off-screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('editable comes back on screen when entering text while it is off-screen', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( home: Center( @@ -173,12 +184,10 @@ void main() { expect(find.byType(EditableText), findsOneWidget); }); - testWidgets('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/19523 - final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( home: Center( @@ -223,11 +232,10 @@ void main() { expect(find.byType(EditableText), findsNothing); }); - testWidgets('entering text does not scroll a surrounding PageView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('entering text does not scroll a surrounding PageView', (WidgetTester tester) async { // regression test for https://github.com/flutter/flutter/issues/19523 - - final TextEditingController textController = TextEditingController(); final PageController pageController = PageController(initialPage: 1); + addTearDown(pageController.dispose); await tester.pumpWidget( MaterialApp( @@ -245,7 +253,7 @@ void main() { ColoredBox( color: Colors.green, child: TextField( - controller: textController, + controller: controller, ), ), Container( @@ -261,7 +269,7 @@ void main() { await tester.showKeyboard(find.byType(EditableText)); await tester.pumpAndSettle(); - expect(textController.text, ''); + expect(controller.text, ''); tester.testTextInput.enterText('H'); final int frames = await tester.pumpAndSettle(); @@ -269,13 +277,12 @@ void main() { // that the surrounding PageView is incorrectly scrolling back-and-forth. expect(frames, 1); - expect(textController.text, 'H'); + expect(controller.text, 'H'); }); - testWidgets('focused multi-line editable scrolls caret back into view when typing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('focused multi-line editable scrolls caret back into view when typing', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); controller.text = "Start${'\n' * 39}End"; await tester.pumpWidget(MaterialApp( @@ -322,10 +329,9 @@ void main() { expect(scrollController.offset, greaterThan(0.0)); }); - testWidgets('focused multi-line editable does not scroll to old position when non-collapsed selection set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('focused multi-line editable does not scroll to old position when non-collapsed selection set', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); final String text = "Start${'\n' * 39}End"; controller.value = TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length - 3)); @@ -374,11 +380,9 @@ void main() { expect(scrollController.offset, 28.0); }); - testWidgets('scrolls into view with scrollInserts after the keyboard pops up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolls into view with scrollInserts after the keyboard pops up', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - + addTearDown(scrollController.dispose); const Key container = Key('container'); await tester.pumpWidget(MaterialApp( @@ -418,15 +422,14 @@ void main() { expect(find.byKey(container), findsNothing); }); - testWidgets( + testWidgetsWithLeakTracking( 'A pinned persistent header should not scroll when its descendant EditableText gains focus', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25507. - ScrollController controller; - final TextEditingController textEditingController = TextEditingController(); - final FocusNode focusNode = FocusNode(); - + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); const Key headerKey = Key('header'); + await tester.pumpWidget( MaterialApp( home: Center( @@ -434,7 +437,7 @@ void main() { height: 600.0, width: 600.0, child: CustomScrollView( - controller: controller = ScrollController(), + controller: scrollController, slivers: List<Widget>.generate(50, (int i) { return i == 10 ? SliverPersistentHeader( @@ -447,7 +450,7 @@ void main() { child: EditableText( key: headerKey, backgroundCursorColor: Colors.grey, - controller: textEditingController, + controller: controller, focusNode: focusNode, style: textStyle, cursorColor: cursorColor, @@ -469,24 +472,23 @@ void main() { ); // The persistent header should now be pinned at the top. - controller.jumpTo(100.0 * 15); + scrollController.jumpTo(100.0 * 15); await tester.pumpAndSettle(); - expect(controller.offset, 100.0 * 15); + expect(scrollController.offset, 100.0 * 15); focusNode.requestFocus(); await tester.pumpAndSettle(); // The scroll offset should remain the same. - expect(controller.offset, 100.0 * 15); + expect(scrollController.offset, 100.0 * 15); }, ); - testWidgets( + testWidgetsWithLeakTracking( 'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25507. - ScrollController controller; - final TextEditingController textEditingController = TextEditingController(); - final FocusNode focusNode = FocusNode(); + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); const Key headerKey = Key('header'); await tester.pumpWidget( @@ -496,7 +498,7 @@ void main() { height: 600.0, width: 600.0, child: CustomScrollView( - controller: controller = ScrollController(), + controller: scrollController, slivers: List<Widget>.generate(50, (int i) { return i == 10 ? SliverPersistentHeader( @@ -510,7 +512,7 @@ void main() { child: EditableText( key: headerKey, backgroundCursorColor: Colors.grey, - controller: textEditingController, + controller: controller, focusNode: focusNode, style: textStyle, cursorColor: cursorColor, @@ -532,20 +534,19 @@ void main() { ); // The persistent header should now be pinned at the top. - controller.jumpTo(100.0 * 15); + scrollController.jumpTo(100.0 * 15); await tester.pumpAndSettle(); - expect(controller.offset, 100.0 * 15); + expect(scrollController.offset, 100.0 * 15); focusNode.requestFocus(); await tester.pumpAndSettle(); // The scroll offset should remain the same. - expect(controller.offset, 100.0 * 15); + expect(scrollController.offset, 100.0 * 15); }, ); void testShowCaretOnScreen({ required bool readOnly }) { group('EditableText._showCaretOnScreen, readOnly=$readOnly', () { - final TextEditingController textEditingController = TextEditingController(); final TextInputFormatter rejectEverythingFormatter = TextInputFormatter.withFunction((TextEditingValue old, TextEditingValue value) => old); bool isCaretOnScreen(WidgetTester tester) { @@ -574,7 +575,7 @@ void main() { const SizedBox(height: 599), EditableText( backgroundCursorColor: Colors.grey, - controller: textEditingController, + controller: controller, scrollController: editableScrollController, inputFormatters: <TextInputFormatter>[if (rejectUserInputs) rejectEverythingFormatter], focusNode: focusNode, @@ -588,11 +589,13 @@ void main() { ); } - testWidgets('focus-triggered showCaretOnScreen', (WidgetTester tester) async { - textEditingController.text = 'a' * 100; - textEditingController.selection = const TextSelection.collapsed(offset: 100); + testWidgetsWithLeakTracking('focus-triggered showCaretOnScreen', (WidgetTester tester) async { + controller.text = 'a' * 100; + controller.selection = const TextSelection.collapsed(offset: 100); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final ScrollController editableScrollController = ScrollController(); + addTearDown(editableScrollController.dispose); await tester.pumpWidget( buildEditableText( @@ -617,11 +620,13 @@ void main() { expect(editableScrollController.offset, readOnly ? 0.0 : greaterThan(0.0)); }); - testWidgets('selection-triggered showCaretOnScreen: virtual keyboard', (WidgetTester tester) async { - textEditingController.text = 'a' * 100; - textEditingController.selection = const TextSelection.collapsed(offset: 80); + testWidgetsWithLeakTracking('selection-triggered showCaretOnScreen: virtual keyboard', (WidgetTester tester) async { + controller.text = 'a' * 100; + controller.selection = const TextSelection.collapsed(offset: 80); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final ScrollController editableScrollController = ScrollController(); + addTearDown(editableScrollController.dispose); await tester.pumpWidget( buildEditableText( @@ -675,11 +680,13 @@ void main() { expect(editableScrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0)); }); - testWidgets('selection-triggered showCaretOnScreen: text selection delegate', (WidgetTester tester) async { - textEditingController.text = 'a' * 100; - textEditingController.selection = const TextSelection.collapsed(offset: 80); + testWidgetsWithLeakTracking('selection-triggered showCaretOnScreen: text selection delegate', (WidgetTester tester) async { + controller.text = 'a' * 100; + controller.selection = const TextSelection.collapsed(offset: 80); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final ScrollController editableScrollController = ScrollController(); + addTearDown(editableScrollController.dispose); await tester.pumpWidget( buildEditableText( @@ -739,10 +746,11 @@ void main() { }); // Regression text for https://github.com/flutter/flutter/pull/74722. - testWidgets('does NOT randomly trigger when cursor blinks', (WidgetTester tester) async { - textEditingController.text = 'a' * 100; - textEditingController.selection = const TextSelection.collapsed(offset: 0); + testWidgetsWithLeakTracking('does NOT randomly trigger when cursor blinks', (WidgetTester tester) async { + controller.text = 'a' * 100; + controller.selection = const TextSelection.collapsed(offset: 0); final ScrollController editableScrollController = ScrollController(); + addTearDown(editableScrollController.dispose); final bool deterministicCursor = EditableText.debugDeterministicCursor; EditableText.debugDeterministicCursor = false; @@ -751,7 +759,7 @@ void main() { home: Scaffold( body: EditableText( backgroundCursorColor: Colors.grey, - controller: textEditingController, + controller: controller, scrollController: editableScrollController, focusNode: focusNode, style: textStyle, @@ -770,7 +778,7 @@ void main() { expect(editableScrollController.offset, 0.0); // Change the text but keep the cursor location. - state.updateEditingValue(textEditingController.value.copyWith( + state.updateEditingValue(controller.value.copyWith( text: 'a' * 101, )); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 4cc86a032ac4d..e3dfc46c78e6d 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -11,8 +11,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/clipboard_utils.dart'; import 'editable_text_utils.dart'; import 'live_text_utils.dart'; @@ -44,9 +44,6 @@ class _MatchesMethodCall extends Matcher { } } -late TextEditingController controller; -final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node'); -final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node'); const TextStyle textStyle = TextStyle(); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); @@ -64,20 +61,29 @@ TextEditingValue collapsedAtEnd(String text) { } void main() { - final MockClipboard mockClipboard = MockClipboard(); - TestWidgetsFlutterBinding.ensureInitialized() - .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); + late TextEditingController controller; + late FocusNode focusNode; + late FocusScopeNode focusScopeNode; setUp(() async { + final MockClipboard mockClipboard = MockClipboard(); + TestWidgetsFlutterBinding.ensureInitialized() + .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); debugResetSemanticsIdCounter(); - controller = TextEditingController(); // Fill the clipboard so that the Paste option is available in the text // selection menu. await Clipboard.setData(const ClipboardData(text: 'Clipboard data')); + controller = TextEditingController(); + focusNode = FocusNode(debugLabel: 'EditableText Node'); + focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node'); }); tearDown(() { + TestWidgetsFlutterBinding.ensureInitialized() + .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); controller.dispose(); + focusNode.dispose(); + focusScopeNode.dispose(); }); // Tests that the desired keyboard action button is requested. @@ -119,12 +125,11 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals(serializedActionName)); } - testWidgets( + testWidgetsWithLeakTracking( 'Tapping the Live Text button calls onLiveTextInput', (WidgetTester tester) async { bool invokedLiveTextInputSuccessfully = false; final GlobalKey key = GlobalKey(); - final TextEditingController controller = TextEditingController(text: ''); await tester.pumpWidget( MaterialApp( home: Align( @@ -136,7 +141,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.subtitle1!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -154,6 +159,9 @@ void main() { onCut: null, onPaste: null, onSelectAll: null, + onLookUp: null, + onSearchWeb: null, + onShare: null, onLiveTextInput: () { invokedLiveTextInputSuccessfully = true; }, @@ -186,15 +194,20 @@ void main() { ); // Regression test for https://github.com/flutter/flutter/issues/126312. - testWidgets('when open input connection in didUpdateWidget, should not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when open input connection in didUpdateWidget, should not throw', (WidgetTester tester) async { final Key key = GlobalKey(); + final TextEditingController controller1 = TextEditingController(text: 'blah blah'); + addTearDown(controller1.dispose); + final TextEditingController controller2 = TextEditingController(text: 'blah blah'); + addTearDown(controller2.dispose); + await tester.pumpWidget( MaterialApp( home: EditableText( key: key, backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller1, focusNode: focusNode, readOnly: true, style: textStyle, @@ -216,7 +229,7 @@ void main() { child: EditableText( key: key, backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller2, focusNode: focusNode, style: textStyle, cursorColor: cursorColor, @@ -227,14 +240,13 @@ void main() { ); }); - testWidgets('Text with selection can be shown on the screen when the keyboard shown', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text with selection can be shown on the screen when the keyboard shown', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/119628 addTearDown(tester.view.reset); final ScrollController scrollController = ScrollController(); - final TextEditingController textController = TextEditingController.fromValue( - const TextEditingValue(text: 'I love flutter'), - ); + addTearDown(scrollController.dispose); + controller.value = const TextEditingValue(text: 'I love flutter'); final Widget widget = MaterialApp( home: Scaffold( @@ -246,7 +258,7 @@ void main() { SizedBox( height: 20.0, child: EditableText( - controller: textController, + controller: controller, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(), @@ -262,9 +274,9 @@ void main() { await tester.showKeyboard(find.byType(EditableText)); tester.view.viewInsets = const FakeViewPadding(bottom: 500); - textController.selection = TextSelection( + controller.selection = TextSelection( baseOffset: 0, - extentOffset: textController.text.length, + extentOffset: controller.text.length, ); await tester.pump(); @@ -275,10 +287,11 @@ void main() { }); // Related issue: https://github.com/flutter/flutter/issues/98115 - testWidgets('ScheduleShowCaretOnScreen with no animation when the view changes metrics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScheduleShowCaretOnScreen with no animation when the view changes metrics', (WidgetTester tester) async { addTearDown(tester.view.reset); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final Widget widget = MaterialApp( home: Scaffold( body: SingleChildScrollView( @@ -299,7 +312,7 @@ void main() { SizedBox( height: 20, child: EditableText( - controller: TextEditingController(), + controller: controller, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(), @@ -322,8 +335,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/34538. - testWidgets('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -376,7 +388,7 @@ void main() { expect(state.currentTextEditingValue.text, equals('گیگ ')); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/78550. - testWidgets('has expected defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -406,7 +418,7 @@ void main() { expect(editableText.textHeightBehavior, isNull); }); - testWidgets('when backgroundCursorColor is updated, RenderEditable should be updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when backgroundCursorColor is updated, RenderEditable should be updated', (WidgetTester tester) async { Widget buildWidget(Color backgroundCursorColor) { return MediaQuery( data: const MediaQueryData(), @@ -430,7 +442,7 @@ void main() { expect(render.backgroundCursorColor, Colors.green); }); - testWidgets('text keyboard is requested when maxLines is default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text keyboard is requested when maxLines is default', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -462,7 +474,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); }); - testWidgets('Keyboard is configured for "unspecified" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "unspecified" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.unspecified, @@ -470,7 +482,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "none" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "none" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.none, @@ -478,7 +490,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "done" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "done" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.done, @@ -486,7 +498,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.send, @@ -494,7 +506,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "go" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "go" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.go, @@ -502,7 +514,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "search" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "search" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.search, @@ -510,7 +522,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "send" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.send, @@ -518,7 +530,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "next" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "next" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.next, @@ -526,7 +538,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "previous" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "previous" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.previous, @@ -534,7 +546,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "continue" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "continue" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.continueAction, @@ -542,7 +554,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "join" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "join" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.join, @@ -550,7 +562,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "route" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "route" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.route, @@ -558,7 +570,7 @@ void main() { ); }); - testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard is configured for "emergencyCall" action when explicitly requested', (WidgetTester tester) async { await desiredKeyboardActionIsRequested( tester: tester, action: TextInputAction.emergencyCall, @@ -566,7 +578,7 @@ void main() { ); }); - testWidgets('insertContent does not throw and parses data correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('insertContent does not throw and parses data correctly', (WidgetTester tester) async { String? latestUri; await tester.pumpWidget( MediaQuery( @@ -622,7 +634,7 @@ void main() { expect(latestUri, equals(uri)); }); - testWidgets('onAppPrivateCommand does not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onAppPrivateCommand does not throw', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -670,7 +682,7 @@ void main() { }); group('Infer keyboardType from autofillHints', () { - testWidgets( + testWidgetsWithLeakTracking( 'infer keyboard types from autofillHints: ios', (WidgetTester tester) async { await tester.pumpWidget( @@ -709,7 +721,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'infer keyboard types from autofillHints: non-ios', (WidgetTester tester) async { await tester.pumpWidget( @@ -742,7 +754,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'inferred keyboard types can be overridden: ios', (WidgetTester tester) async { await tester.pumpWidget( @@ -777,7 +789,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'inferred keyboard types can be overridden: non-ios', (WidgetTester tester) async { await tester.pumpWidget( @@ -812,7 +824,7 @@ void main() { ); }); - testWidgets('multiline keyboard is requested when set explicitly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline keyboard is requested when set explicitly', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -843,7 +855,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); }); - testWidgets('EditableText sends enableInteractiveSelection to config', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText sends enableInteractiveSelection to config', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -895,7 +907,7 @@ void main() { expect(state.textInputConfiguration.enableInteractiveSelection, isFalse); }); - testWidgets('selection persists when unfocused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection persists when unfocused', (WidgetTester tester) async { const TextEditingValue value = TextEditingValue( text: 'test test', selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 5, extentOffset: 7), @@ -949,7 +961,7 @@ void main() { expect(focusNode.hasFocus, isFalse); }); - testWidgets('selection rects re-sent when refocused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection rects re-sent when refocused', (WidgetTester tester) async { final List<List<SelectionRect>> log = <List<SelectionRect>>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { if (methodCall.method == 'TextInput.setSelectionRects') { @@ -966,8 +978,8 @@ void main() { return null; }); - final TextEditingController controller = TextEditingController(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); controller.text = 'Text1'; Future<void> pumpEditableText({ double? width, double? height, TextAlign textAlign = TextAlign.start }) async { @@ -1031,7 +1043,7 @@ void main() { // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('EditableText does not derive selection color from DefaultSelectionStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText does not derive selection color from DefaultSelectionStyle', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103341. const TextEditingValue value = TextEditingValue( text: 'test test', @@ -1062,7 +1074,7 @@ void main() { expect(state.renderEditable.selectionColor, null); }); - testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1093,8 +1105,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); }); - testWidgets('enableSuggestions flag is sent to the engine properly', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('enableSuggestions flag is sent to the engine properly', (WidgetTester tester) async { const bool enableSuggestions = false; await tester.pumpWidget( MediaQuery( @@ -1123,8 +1134,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['enableSuggestions'], enableSuggestions); }); - testWidgets('enableIMEPersonalizedLearning flag is sent to the engine properly', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('enableIMEPersonalizedLearning flag is sent to the engine properly', (WidgetTester tester) async { const bool enableIMEPersonalizedLearning = false; await tester.pumpWidget( MediaQuery( @@ -1154,8 +1164,7 @@ void main() { }); group('smartDashesType and smartQuotesType', () { - testWidgets('sent to the engine properly', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('sent to the engine properly', (WidgetTester tester) async { const SmartDashesType smartDashesType = SmartDashesType.disabled; const SmartQuotesType smartQuotesType = SmartQuotesType.disabled; await tester.pumpWidget( @@ -1187,8 +1196,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['smartQuotesType'], smartQuotesType.index.toString()); }); - testWidgets('default to true when obscureText is false', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('default to true when obscureText is false', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1216,8 +1224,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['smartQuotesType'], '1'); }); - testWidgets('default to false when obscureText is true', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('default to false when obscureText is true', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1247,12 +1254,9 @@ void main() { }); }); - testWidgets('selection overlay will update when text grow bigger', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController.fromValue( - const TextEditingValue( - text: 'initial value', - ), - ); + testWidgetsWithLeakTracking('selection overlay will update when text grow bigger', (WidgetTester tester) async { + controller.value = const TextEditingValue(text: 'initial value'); + Future<void> pumpEditableTextWithTextStyle(TextStyle style) async { await tester.pumpWidget( MaterialApp( @@ -1307,9 +1311,18 @@ void main() { expect(handles[1].localToGlobal(Offset.zero), const Offset(197.0, 17.0)); }); - testWidgets('can update style of previous activated EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can update style of previous activated EditableText', (WidgetTester tester) async { + final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); + final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); + final TextEditingController controller3 = TextEditingController(); + addTearDown(controller3.dispose); + final TextEditingController controller4 = TextEditingController(); + addTearDown(controller4.dispose); final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); + await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1322,7 +1335,7 @@ void main() { children: <Widget>[ EditableText( key: key1, - controller: TextEditingController(), + controller: controller1, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(fontSize: 9), @@ -1330,7 +1343,7 @@ void main() { ), EditableText( key: key2, - controller: TextEditingController(), + controller: controller2, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(fontSize: 9), @@ -1365,7 +1378,7 @@ void main() { children: <Widget>[ EditableText( key: key1, - controller: TextEditingController(), + controller: controller3, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(fontSize: 20), @@ -1373,7 +1386,7 @@ void main() { ), EditableText( key: key2, - controller: TextEditingController(), + controller: controller4, backgroundCursorColor: Colors.grey, focusNode: focusNode, style: const TextStyle(fontSize: 9), @@ -1390,7 +1403,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1421,7 +1434,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); }); - testWidgets('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1453,7 +1466,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); }); - testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Correct keyboard is requested when set explicitly and maxLines > 1', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1485,7 +1498,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); }); - testWidgets('multiline keyboard is requested when set implicitly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('multiline keyboard is requested when set implicitly', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1516,7 +1529,7 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.newline')); }); - testWidgets('single line inputs have correct default keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('single line inputs have correct default keyboard', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1546,8 +1559,10 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals('TextInputAction.done')); }); - // Test case for https://github.com/flutter/flutter/issues/123523. - testWidgets( + // Test case for + // https://github.com/flutter/flutter/issues/123523 + // https://github.com/flutter/flutter/issues/134846 . + testWidgetsWithLeakTracking( 'The focus and callback behavior are correct when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async { bool onSubmittedInvoked = false; @@ -1584,20 +1599,12 @@ void main() { editableText.connectionClosed(); await tester.pump(); - if (kIsWeb) { - expect(onSubmittedInvoked, isTrue); - expect(onEditingCompleteInvoked, isTrue); - // Because we add the onEditingComplete and we didn't unfocus there, so focus still exists. - expect(focusNode.hasFocus, isTrue); - } else { - // For mobile and other platforms, calling connectionClosed will only unfocus. - expect(focusNode.hasFocus, isFalse); - expect(onEditingCompleteInvoked, isFalse); - expect(onSubmittedInvoked, isFalse); - } + expect(focusNode.hasFocus, isFalse); + expect(onEditingCompleteInvoked, isFalse); + expect(onSubmittedInvoked, isFalse); }); - testWidgets('connection is closed when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async { + testWidgetsWithLeakTracking('connection is closed when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1640,7 +1647,7 @@ void main() { expect(tester.testTextInput.log, isEmpty); }); - testWidgets('closed connection reopened when user focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('closed connection reopened when user focused', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -1691,7 +1698,7 @@ void main() { expect(state.wantKeepAlive, true); }); - testWidgets('closed connection reopened when user focused on another field', (WidgetTester tester) async { + testWidgetsWithLeakTracking('closed connection reopened when user focused on another field', (WidgetTester tester) async { final EditableText testNameField = EditableText( backgroundCursorColor: Colors.grey, @@ -1766,7 +1773,7 @@ void main() { expect(state.wantKeepAlive, true); }); - testWidgets( + testWidgetsWithLeakTracking( 'kept-alive EditableText does not crash when layout is skipped', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84896. @@ -1844,7 +1851,7 @@ void main() { // cut. It might also provide additional functionality depending on the // browser (such as translation). Due to this, in browsers, we should not // show a Flutter toolbar for the editable text elements. - testWidgets('can show toolbar when there is text and a selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show toolbar when there is text and a selection', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -1909,7 +1916,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null); }); - testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -1957,7 +1964,7 @@ void main() { ); }); - testWidgets('can hide toolbar with DismissIntent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can hide toolbar with DismissIntent', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -1992,7 +1999,7 @@ void main() { expect(find.text('Paste'), findsNothing); }); - testWidgets('toolbar hidden on mobile when orientation changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toolbar hidden on mobile when orientation changes', (WidgetTester tester) async { addTearDown(tester.view.reset); await tester.pumpWidget( @@ -2040,7 +2047,7 @@ void main() { // toolbar. Until we change that, this test should remain skipped. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); // [intended] - testWidgets('Paste is shown only when there is something to paste', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paste is shown only when there is something to paste', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2090,8 +2097,10 @@ void main() { expect(find.text('Paste'), findsNothing); }); - testWidgets('Copy selection does not collapse selection on desktop and iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Copy selection does not collapse selection on desktop and iOS', (WidgetTester tester) async { final TextEditingController localController = TextEditingController(text: 'Hello world'); + addTearDown(localController.dispose); + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2128,8 +2137,10 @@ void main() { expect(find.text('Copy'), findsNothing); }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows })); // [intended] - testWidgets('Copy selection collapses selection and hides the toolbar on Android and Fuchsia', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Copy selection collapses selection and hides the toolbar on Android and Fuchsia', (WidgetTester tester) async { final TextEditingController localController = TextEditingController(text: 'Hello world'); + addTearDown(localController.dispose); + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2144,7 +2155,7 @@ void main() { ); final EditableTextState state = - tester.state<EditableTextState>(find.byType(EditableText)); + tester.state<EditableTextState>(find.byType(EditableText)); // Show the toolbar. state.renderEditable.selectWordsInRange( @@ -2155,9 +2166,11 @@ void main() { final TextSelection copySelectionRange = localController.selection; + expect(find.byType(TextSelectionToolbar), findsNothing); state.showToolbar(); await tester.pumpAndSettle(); + expect(find.byType(TextSelectionToolbar), findsOneWidget); expect(find.text('Copy'), findsOneWidget); await tester.tap(find.text('Copy')); @@ -2166,7 +2179,7 @@ void main() { expect(find.text('Copy'), findsNothing); }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia })); // [intended] - testWidgets('can show the toolbar after clearing all text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show the toolbar after clearing all text', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/35998. await tester.pumpWidget( MaterialApp( @@ -2205,12 +2218,14 @@ void main() { expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); }); - testWidgets('can dynamically disable options in toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can dynamically disable options in toolbar', (WidgetTester tester) async { + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, toolbarOptions: const ToolbarOptions( copy: true, @@ -2241,13 +2256,15 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('can dynamically disable select all option in toolbar - cupertino', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can dynamically disable select all option in toolbar - cupertino', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/40711 + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, toolbarOptions: ToolbarOptions.empty, style: textStyle, @@ -2270,13 +2287,15 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('can dynamically disable select all option in toolbar - material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can dynamically disable select all option in toolbar - material', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/40711 + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, toolbarOptions: const ToolbarOptions( copy: true, @@ -2306,12 +2325,14 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('cut and paste are disabled in read only mode even if explicitly set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cut and paste are disabled in read only mode even if explicitly set', (WidgetTester tester) async { + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, readOnly: true, toolbarOptions: const ToolbarOptions( @@ -2345,12 +2366,14 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('cut and copy are disabled in obscured mode even if explicitly set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cut and copy are disabled in obscured mode even if explicitly set', (WidgetTester tester) async { + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -2387,12 +2410,14 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('cut and copy do nothing in obscured mode even if explicitly called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cut and copy do nothing in obscured mode even if explicitly called', (WidgetTester tester) async { + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, obscureText: true, style: textStyle, @@ -2427,12 +2452,14 @@ void main() { expect(data!.text, isEmpty); }); - testWidgets('select all does nothing if obscured and read-only, even if explicitly called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all does nothing if obscured and read-only, even if explicitly called', (WidgetTester tester) async { + controller.text = 'blah blah'; + await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(text: 'blah blah'), + controller: controller, focusNode: focusNode, obscureText: true, readOnly: true, @@ -2453,11 +2480,13 @@ void main() { }); group('buttonItemsForToolbarOptions', () { - testWidgets('returns null when toolbarOptions are empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns null when toolbarOptions are empty', (WidgetTester tester) async { + controller.text = 'TEXT'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'TEXT'), + controller: controller, toolbarOptions: ToolbarOptions.empty, focusNode: focusNode, style: textStyle, @@ -2474,11 +2503,13 @@ void main() { expect(state.buttonItemsForToolbarOptions(), isNull); }); - testWidgets('returns empty array when only cut is selected in toolbarOptions but cut is not enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns empty array when only cut is selected in toolbarOptions but cut is not enabled', (WidgetTester tester) async { + controller.text = 'TEXT'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'TEXT'), + controller: controller, toolbarOptions: const ToolbarOptions(cut: true), readOnly: true, focusNode: focusNode, @@ -2497,9 +2528,9 @@ void main() { expect(state.buttonItemsForToolbarOptions(), isEmpty); }); - testWidgets('returns only cut button when only cut is selected in toolbarOptions and cut is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns only cut button when only cut is selected in toolbarOptions and cut is enabled', (WidgetTester tester) async { const String text = 'TEXT'; - final TextEditingController controller = TextEditingController(text: text); + controller.text = text; await tester.pumpWidget( MaterialApp( @@ -2542,11 +2573,13 @@ void main() { expect(data!.text, equals(text)); }); - testWidgets('returns empty array when only copy is selected in toolbarOptions but copy is not enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns empty array when only copy is selected in toolbarOptions but copy is not enabled', (WidgetTester tester) async { + controller.text = 'TEXT'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'TEXT'), + controller: controller, toolbarOptions: const ToolbarOptions(copy: true), obscureText: true, focusNode: focusNode, @@ -2565,9 +2598,9 @@ void main() { expect(state.buttonItemsForToolbarOptions(), isEmpty); }); - testWidgets('returns only copy button when only copy is selected in toolbarOptions and copy is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns only copy button when only copy is selected in toolbarOptions and copy is enabled', (WidgetTester tester) async { const String text = 'TEXT'; - final TextEditingController controller = TextEditingController(text: text); + controller.text = text; await tester.pumpWidget( MaterialApp( @@ -2610,11 +2643,13 @@ void main() { expect(data!.text, equals(text)); }); - testWidgets('returns empty array when only paste is selected in toolbarOptions but paste is not enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns empty array when only paste is selected in toolbarOptions but paste is not enabled', (WidgetTester tester) async { + controller.text = 'TEXT'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'TEXT'), + controller: controller, toolbarOptions: const ToolbarOptions(paste: true), readOnly: true, focusNode: focusNode, @@ -2633,9 +2668,9 @@ void main() { expect(state.buttonItemsForToolbarOptions(), isEmpty); }); - testWidgets('returns only paste button when only paste is selected in toolbarOptions and paste is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns only paste button when only paste is selected in toolbarOptions and paste is enabled', (WidgetTester tester) async { const String text = 'TEXT'; - final TextEditingController controller = TextEditingController(text: text); + controller.text = text; await tester.pumpWidget( MaterialApp( @@ -2675,11 +2710,13 @@ void main() { expect(controller.text, equals(text + text)); }); - testWidgets('returns empty array when only selectAll is selected in toolbarOptions but selectAll is not enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns empty array when only selectAll is selected in toolbarOptions but selectAll is not enabled', (WidgetTester tester) async { + controller.text = 'TEXT'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'TEXT'), + controller: controller, toolbarOptions: const ToolbarOptions(selectAll: true), readOnly: true, obscureText: true, @@ -2699,9 +2736,9 @@ void main() { expect(state.buttonItemsForToolbarOptions(), isEmpty); }); - testWidgets('returns only selectAll button when only selectAll is selected in toolbarOptions and selectAll is enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('returns only selectAll button when only selectAll is selected in toolbarOptions and selectAll is enabled', (WidgetTester tester) async { const String text = 'TEXT'; - final TextEditingController controller = TextEditingController(text: text); + controller.text = text; await tester.pumpWidget( MaterialApp( @@ -2736,9 +2773,9 @@ void main() { }); }); - testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('Handles the read-only flag correctly', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2778,9 +2815,9 @@ void main() { } }); - testWidgets('Does not accept updates when read-only', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('Does not accept updates when read-only', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2819,12 +2856,10 @@ void main() { } }); - testWidgets('Read-only fields do not format text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Read-only fields do not format text', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; late SelectionChangedCause selectionCause; - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); - await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2859,12 +2894,10 @@ void main() { } }); - testWidgets('Selection changes during Scribble interaction should have the scribble cause', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selection changes during Scribble interaction should have the scribble cause', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; late SelectionChangedCause selectionCause; - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); - await tester.pumpWidget( MaterialApp( home: EditableText( @@ -2907,9 +2940,8 @@ void main() { await tester.testTextInput.finishScribbleInteraction(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('Requests focus and changes the selection when onScribbleFocus is called', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('Requests focus and changes the selection when onScribbleFocus is called', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; late SelectionChangedCause selectionCause; await tester.pumpWidget( @@ -2939,9 +2971,8 @@ void main() { // will never be SelectionChangedCause.scribble. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; await tester.pumpWidget( MaterialApp( @@ -3033,9 +3064,8 @@ void main() { // never request the scribble elements. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('single line Scribble fields can show a horizontal placeholder', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('single line Scribble fields can show a horizontal placeholder', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; await tester.pumpWidget( MaterialApp( @@ -3108,9 +3138,8 @@ void main() { // will not handle placeholders. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('multiline Scribble fields can show a vertical placeholder', (WidgetTester tester) async { - final TextEditingController controller = - TextEditingController(text: 'Lorem ipsum dolor sit amet'); + testWidgetsWithLeakTracking('multiline Scribble fields can show a vertical placeholder', (WidgetTester tester) async { + controller.text = 'Lorem ipsum dolor sit amet'; await tester.pumpWidget( MaterialApp( @@ -3186,10 +3215,10 @@ void main() { // will not handle placeholders. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('Sends "updateConfig" when read-only flag is flipped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sends "updateConfig" when read-only flag is flipped', (WidgetTester tester) async { bool readOnly = true; late StateSetter setState; - final TextEditingController controller = TextEditingController(text: 'Lorem ipsum dolor sit amet'); + controller.text = 'Lorem ipsum dolor sit amet'; await tester.pumpWidget( MaterialApp( @@ -3224,10 +3253,10 @@ void main() { expect(tester.testTextInput.setClientArgs!['readOnly'], isFalse); }); - testWidgets('Sends "updateConfig" when obscureText is flipped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sends "updateConfig" when obscureText is flipped', (WidgetTester tester) async { bool obscureText = true; late StateSetter setState; - final TextEditingController controller = TextEditingController(text: 'Lorem'); + controller.text = 'Lorem'; await tester.pumpWidget( MaterialApp( @@ -3260,13 +3289,13 @@ void main() { expect(tester.testTextInput.setClientArgs!['obscureText'], isFalse); }); - testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { late String changedValue; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -3303,7 +3332,7 @@ void main() { TextInputAction.values.toSet(), ); - testWidgets('Handles focus correctly when action is invoked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Handles focus correctly when action is invoked', (WidgetTester tester) async { // The expectations for each of the types of TextInputAction. const Map<TextInputAction, bool> actionShouldLoseFocus = <TextInputAction, bool>{ TextInputAction.none: false, @@ -3330,7 +3359,6 @@ void main() { bool shouldFocusNext = false, bool shouldFocusPrevious = false, }) async { - final FocusNode focusNode = FocusNode(); final GlobalKey previousKey = GlobalKey(); final GlobalKey nextKey = GlobalKey(); @@ -3343,7 +3371,7 @@ void main() { ), EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3383,13 +3411,11 @@ void main() { } }, variant: focusVariants); - testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - + testWidgetsWithLeakTracking('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async { final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3417,16 +3443,14 @@ void main() { expect(focusNode.hasFocus, true); }); - testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - + testWidgetsWithLeakTracking('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { bool onEditingCompleteCalled = false; bool onSubmittedCalled = false; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3457,16 +3481,14 @@ void main() { // and onSubmission callbacks. }); - testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - + testWidgetsWithLeakTracking('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { bool onEditingCompleteCalled = false; bool onSubmittedCalled = false; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3497,16 +3519,14 @@ void main() { // and onSubmission callbacks. }); - testWidgets('When "newline" action is called on a Editable text with maxLines == 1 callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - + testWidgetsWithLeakTracking('When "newline" action is called on a Editable text with maxLines == 1 callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { bool onEditingCompleteCalled = false; bool onSubmittedCalled = false; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3536,16 +3556,14 @@ void main() { // and onSubmission callbacks. }); - testWidgets('When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); - + testWidgetsWithLeakTracking('When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async { bool onEditingCompleteCalled = false; bool onSubmittedCalled = false; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -3576,7 +3594,7 @@ void main() { assert(!onEditingCompleteCalled); }); - testWidgets( + testWidgetsWithLeakTracking( 'finalizeEditing should reset the input connection when shouldUnfocus is true but the unfocus is cancelled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84240 . @@ -3641,7 +3659,7 @@ void main() { ]))); }); - testWidgets( + testWidgetsWithLeakTracking( 'requesting focus in the onSubmitted callback should keep the onscreen keyboard visible', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/95154 . @@ -3684,10 +3702,11 @@ void main() { ]))); }); - testWidgets( + testWidgetsWithLeakTracking( 'iOS autocorrection rectangle should appear on demand and dismiss when the text changes or when focus is lost', (WidgetTester tester) async { const Color rectColor = Color(0xFFFF0000); + controller.text = 'ABCDEFG'; void verifyAutocorrectionRectVisibility({ required bool expectVisible }) { PaintPattern evaluate() { @@ -3716,9 +3735,6 @@ void main() { expect(findRenderEditable(tester), evaluate()); } - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController(text: 'ABCDEFG'); - final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, @@ -3764,17 +3780,23 @@ void main() { verifyAutocorrectionRectVisibility(expectVisible: false); }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 5}, + ), ); - testWidgets('Changing controller updates EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing controller updates EditableText', (WidgetTester tester) async { final TextEditingController controller1 = TextEditingController(text: 'Wibble'); + addTearDown(controller1.dispose); final TextEditingController controller2 = TextEditingController(text: 'Wobble'); + addTearDown(controller2.dispose); TextEditingController currentController = controller1; late StateSetter setState; - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node'); Widget builder() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -3853,7 +3875,7 @@ void main() { ); }); - testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -3893,7 +3915,7 @@ void main() { semantics.dispose(); }); - testWidgets('EditableText sets multi-line flag in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText sets multi-line flag in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -3953,11 +3975,10 @@ void main() { semantics.dispose(); }); - testWidgets('EditableText includes text as value in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText includes text as value in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const String value1 = 'EditableText content'; - controller.text = value1; await tester.pumpWidget( @@ -4003,7 +4024,7 @@ void main() { semantics.dispose(); }); - testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('exposes correct cursor movement semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); controller.text = 'test'; @@ -4086,7 +4107,7 @@ void main() { semantics.dispose(); }); - testWidgets('can move cursor with a11y means - character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can move cursor with a11y means - character', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const bool doNotExtendSelection = false; @@ -4191,7 +4212,7 @@ void main() { semantics.dispose(); }); - testWidgets('can move cursor with a11y means - word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can move cursor with a11y means - word', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const bool doNotExtendSelection = false; @@ -4304,7 +4325,7 @@ void main() { semantics.dispose(); }); - testWidgets('can extend selection with a11y means - character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can extend selection with a11y means - character', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const bool extendSelection = true; const bool doNotExtendSelection = false; @@ -4420,7 +4441,7 @@ void main() { semantics.dispose(); }); - testWidgets('can extend selection with a11y means - word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can extend selection with a11y means - word', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const bool extendSelection = true; const bool doNotExtendSelection = false; @@ -4534,7 +4555,7 @@ void main() { semantics.dispose(); }); - testWidgets('password fields have correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('password fields have correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); controller.text = 'super-secret-password!!1'; @@ -4589,7 +4610,7 @@ void main() { semantics.dispose(); }); - testWidgets('password fields become obscured with the right semantics when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('password fields become obscured with the right semantics when set', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const String originalText = 'super-secret-password!!1'; @@ -4704,7 +4725,7 @@ void main() { semantics.dispose(); }); - testWidgets('password fields can have their obscuring character customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('password fields can have their obscuring character customized', (WidgetTester tester) async { const String originalText = 'super-secret-password!!1'; controller.text = originalText; @@ -4725,7 +4746,7 @@ void main() { expect((findRenderEditable(tester).text! as TextSpan).text, expectedValue); }); - testWidgets('password briefly shows last character when entered on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('password briefly shows last character when entered on mobile', (WidgetTester tester) async { final bool debugDeterministicCursor = EditableText.debugDeterministicCursor; EditableText.debugDeterministicCursor = false; addTearDown(() { @@ -4779,7 +4800,7 @@ void main() { controls = MockTextSelectionControls(); }); - testWidgets('are exposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('are exposed', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); controls.testCanCopy = false; @@ -4876,7 +4897,7 @@ void main() { semantics.dispose(); }); - testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can copy/cut/paste with a11y', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); controls.testCanCopy = true; @@ -4949,10 +4970,11 @@ void main() { }); // Regression test for b/201218542. - testWidgets('copying with a11y works even when toolbar is hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('copying with a11y works even when toolbar is hidden', (WidgetTester tester) async { Future<void> testByControls(TextSelectionControls controls) async { final SemanticsTester semantics = SemanticsTester(tester); final TextEditingController controller = TextEditingController(text: 'ABCDEFG'); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: EditableText( @@ -4994,7 +5016,7 @@ void main() { }); }); - testWidgets('can set text with a11y', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can set text with a11y', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( home: EditableText( @@ -5059,7 +5081,7 @@ void main() { semantics.dispose(); }); - testWidgets('allows customizing text style in subclasses', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows customizing text style in subclasses', (WidgetTester tester) async { controller.text = 'Hello World'; await tester.pumpWidget(MaterialApp( @@ -5076,9 +5098,8 @@ void main() { expect(render.text!.style!.fontStyle, FontStyle.italic); }); - testWidgets('onChanged callback only invoked on text changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onChanged callback only invoked on text changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/111651 . - final TextEditingController controller = TextEditingController(); int onChangedCount = 0; bool preventInput = false; final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { @@ -5091,7 +5112,7 @@ void main() { controller: controller, backgroundCursorColor: Colors.red, cursorColor: Colors.red, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, onChanged: (String newString) { onChangedCount += 1; }, inputFormatters: <TextInputFormatter>[formatter], @@ -5122,20 +5143,19 @@ void main() { expect(onChangedCount , 2); }); - testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Formatters are skipped if text has not changed', (WidgetTester tester) async { int called = 0; final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { called += 1; return newValue; }); - final TextEditingController controller = TextEditingController(); final MediaQuery mediaQuery = MediaQuery( data: const MediaQueryData(), child: EditableText( controller: controller, backgroundCursorColor: Colors.red, cursorColor: Colors.red, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, inputFormatters: <TextInputFormatter>[ formatter, @@ -5166,7 +5186,7 @@ void main() { expect(called, 2); }); - testWidgets('default keyboardAppearance is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default keyboardAppearance is respected', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/22212. final List<MethodCall> log = <MethodCall>[]; @@ -5175,7 +5195,6 @@ void main() { return null; }); - final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -5183,7 +5202,7 @@ void main() { textDirection: TextDirection.ltr, child: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -5198,14 +5217,13 @@ void main() { expect(((setClient.arguments as Iterable<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.light'); }); - testWidgets('location of widget is sent on show keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('location of widget is sent on show keyboard', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -5213,7 +5231,7 @@ void main() { textDirection: TextDirection.ltr, child: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -5234,7 +5252,7 @@ void main() { ); }); - testWidgets('transform and size is reset when text connection opens', (WidgetTester tester) async { + testWidgetsWithLeakTracking('transform and size is reset when text connection opens', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); @@ -5242,7 +5260,13 @@ void main() { }); final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); + final FocusNode focusNode1 = FocusNode(); + addTearDown(focusNode1.dispose); final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); + final FocusNode focusNode2 = FocusNode(); + addTearDown(focusNode2.dispose); controller1.text = 'Text1'; controller2.text = 'Text2'; @@ -5257,7 +5281,7 @@ void main() { EditableText( key: ValueKey<String>(controller1.text), controller: controller1, - focusNode: FocusNode(), + focusNode: focusNode1, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -5266,7 +5290,7 @@ void main() { EditableText( key: ValueKey<String>(controller2.text), controller: controller2, - focusNode: FocusNode(), + focusNode: focusNode2, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -5320,7 +5344,7 @@ void main() { ); }); - testWidgets('size and transform are sent when they change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('size and transform are sent when they change', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); @@ -5363,7 +5387,7 @@ void main() { ); }); - testWidgets('selection rects are sent when they change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection rects are sent when they change', (WidgetTester tester) async { addTearDown(tester.view.reset); // Ensure selection rects are sent on iPhone (using SE 3rd gen size) tester.view.physicalSize = const Size(750.0, 1334.0); @@ -5384,8 +5408,8 @@ void main() { return null; }); - final TextEditingController controller = TextEditingController(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); controller.text = 'Text1'; Future<void> pumpEditableText({ double? width, double? height, TextAlign textAlign = TextAlign.start }) async { @@ -5503,14 +5527,13 @@ void main() { // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('selection rects are not sent if scribbleEnabled is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection rects are not sent if scribbleEnabled is false', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); controller.text = 'Text1'; await tester.pumpWidget( @@ -5524,7 +5547,7 @@ void main() { EditableText( key: ValueKey<String>(controller.text), controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -5543,7 +5566,7 @@ void main() { // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('selection rects sent even when character corners are outside of paintBounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection rects sent even when character corners are outside of paintBounds', (WidgetTester tester) async { final List<List<SelectionRect>> log = <List<SelectionRect>>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) { if (methodCall.method == 'TextInput.setSelectionRects') { @@ -5560,8 +5583,8 @@ void main() { return null; }); - final TextEditingController controller = TextEditingController(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); controller.text = 'Text1'; final GlobalKey<EditableTextState> editableTextKey = GlobalKey(); @@ -5601,7 +5624,9 @@ void main() { // Scroll so that the top of each character is above the top of the renderEditable // and the bottom of each character is below the bottom of the renderEditable. - editableTextKey.currentState!.renderEditable.offset = ViewportOffset.fixed(0.5); + final ViewportOffset offset = ViewportOffset.fixed(0.5); + addTearDown(offset.dispose); + editableTextKey.currentState!.renderEditable.offset = offset; await tester.showKeyboard(find.byType(EditableText)); // We should get all the rects. @@ -5617,21 +5642,20 @@ void main() { // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. }, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended] - testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text styling info is sent on show keyboard', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), child: EditableText( textDirection: TextDirection.rtl, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle( fontSize: 20.0, fontFamily: 'Roboto', @@ -5657,21 +5681,20 @@ void main() { ); }); - testWidgets('text styling info is sent on show keyboard (bold override)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text styling info is sent on show keyboard (bold override)', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(boldText: true), child: EditableText( textDirection: TextDirection.rtl, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: const TextStyle( fontSize: 20.0, fontFamily: 'Roboto', @@ -5697,7 +5720,7 @@ void main() { ); }); - testWidgets('text styling info is sent on style update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text styling info is sent on style update', (WidgetTester tester) async { final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); late StateSetter setState; const TextStyle textStyle1 = TextStyle( @@ -5727,7 +5750,7 @@ void main() { backgroundCursorColor: Colors.grey, key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: currentTextStyle, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -5782,7 +5805,7 @@ void main() { child: EditableText( backgroundCursorColor: Colors.grey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -5796,7 +5819,7 @@ void main() { ); } - testWidgets( + testWidgetsWithLeakTracking( 'called with proper coordinates', (WidgetTester tester) async { controller.value = TextEditingValue(text: 'a' * 50); @@ -5838,7 +5861,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'only send updates when necessary', (WidgetTester tester) async { controller.value = TextEditingValue(text: 'a' * 100); @@ -5856,7 +5879,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'not sent with selection', (WidgetTester tester) async { controller.value = TextEditingValue( @@ -5883,7 +5906,7 @@ void main() { child: EditableText( backgroundCursorColor: Colors.grey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -5897,7 +5920,7 @@ void main() { ); } - testWidgets( + testWidgetsWithLeakTracking( 'called when the composing range changes', (WidgetTester tester) async { controller.value = TextEditingValue(text: 'a' * 100); @@ -5931,7 +5954,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'only send updates when necessary', (WidgetTester tester) async { controller.value = TextEditingValue(text: 'a' * 100, composing: const TextRange(start: 0, end: 10)); @@ -5949,7 +5972,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'zero matrix paint transform', (WidgetTester tester) async { controller.value = TextEditingValue(text: 'a' * 100, composing: const TextRange(start: 0, end: 10)); @@ -5971,7 +5994,7 @@ void main() { }); - testWidgets('custom keyboardAppearance is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('custom keyboardAppearance is respected', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/22212. final List<MethodCall> log = <MethodCall>[]; @@ -5980,7 +6003,6 @@ void main() { return null; }); - final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -5988,7 +6010,7 @@ void main() { textDirection: TextDirection.ltr, child: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -6004,15 +6026,12 @@ void main() { expect(((setClient.arguments as Iterable<dynamic>).last as Map<String, dynamic>)['keyboardAppearance'], 'Brightness.dark'); }); - testWidgets('Composing text is underlined and underline is cleared when losing focus', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController.fromValue( - const TextEditingValue( - text: 'text composing text', - selection: TextSelection.collapsed(offset: 14), - composing: TextRange(start: 5, end: 14), - ), + testWidgetsWithLeakTracking('Composing text is underlined and underline is cleared when losing focus', (WidgetTester tester) async { + controller.value = const TextEditingValue( + text: 'text composing text', + selection: TextSelection.collapsed(offset: 14), + composing: TextRange(start: 5, end: 14), ); - final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node'); await tester.pumpWidget(MaterialApp( // So we can show overlays. home: EditableText( @@ -6052,9 +6071,8 @@ void main() { expect(renderEditable.text!.style!.decoration, isNull); }); - testWidgets('text selection toolbar visibility', (WidgetTester tester) async { - const String testText = 'hello \n world \n this \n is \n text'; - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('text selection toolbar visibility', (WidgetTester tester) async { + controller.text = 'hello \n world \n this \n is \n text'; await tester.pumpWidget(MaterialApp( home: Align( @@ -6065,7 +6083,7 @@ void main() { child: EditableText( showSelectionHandles: true, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -6121,10 +6139,9 @@ void main() { // toolbar. Until we change that, this test should remain skipped. }, skip: kIsWeb); // [intended] - testWidgets('text selection handle visibility', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text selection handle visibility', (WidgetTester tester) async { // Text with two separate words to select. - const String testText = 'XXXXX XXXXX'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'XXXXX XXXXX'; await tester.pumpWidget(MaterialApp( home: Align( @@ -6134,7 +6151,7 @@ void main() { child: EditableText( showSelectionHandles: true, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -6293,10 +6310,9 @@ void main() { // toolbar. Until we change that, this test should remain skipped. }, skip: kIsWeb); // [intended] - testWidgets('text selection handle visibility RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text selection handle visibility RTL', (WidgetTester tester) async { // Text with two separate words to select. - const String testText = 'XXXXX XXXXX'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'XXXXX XXXXX'; await tester.pumpWidget(MaterialApp( home: Align( @@ -6306,7 +6322,7 @@ void main() { child: EditableText( controller: controller, showSelectionHandles: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -6363,7 +6379,7 @@ void main() { Future<void> testTextEditing(WidgetTester tester, {required TargetPlatform targetPlatform}) async { final String targetPlatformString = targetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -6381,7 +6397,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -7362,7 +7378,7 @@ void main() { } } - testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; await testTextEditing(tester, targetPlatform: defaultTargetPlatform); @@ -7372,7 +7388,7 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async { debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData; await testTextEditing(tester, targetPlatform: defaultTargetPlatform); @@ -7382,11 +7398,11 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets( + testWidgetsWithLeakTracking( 'keyboard shortcuts respect read-only', (WidgetTester tester) async { final String platform = defaultTargetPlatform.name.toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: testText.length ~/2, @@ -7402,7 +7418,7 @@ void main() { readOnly: true, controller: controller, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -7561,10 +7577,10 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('home/end keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('home/end keys', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -7582,7 +7598,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -7707,11 +7723,11 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('home keys and wordwraps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('home keys and wordwraps', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -7729,7 +7745,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -7863,11 +7879,11 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('end keys and wordwraps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('end keys and wordwraps', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -7885,7 +7901,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8021,10 +8037,10 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('shift + home/end keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shift + home/end keys', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8042,7 +8058,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8215,8 +8231,8 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('shift + home/end keys (Windows only)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('shift + home/end keys (Windows only)', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8232,7 +8248,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8326,9 +8342,9 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows }) ); - testWidgets('home/end keys scrolling (Mac only)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('home/end keys scrolling (Mac only)', (WidgetTester tester) async { const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8344,7 +8360,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8387,11 +8403,11 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }) ); - testWidgets('shift + home keys and wordwraps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shift + home keys and wordwraps', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8409,7 +8425,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8572,11 +8588,11 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('shift + end keys and wordwraps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shift + end keys and wordwraps', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8594,7 +8610,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8759,9 +8775,9 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets('shift + home/end keys to document boundary (Mac only)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shift + home/end keys to document boundary (Mac only)', (WidgetTester tester) async { const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8778,7 +8794,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8863,8 +8879,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }) ); - testWidgets('control + home/end keys (Windows only)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('control + home/end keys (Windows only)', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8880,7 +8896,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -8928,8 +8944,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows }) ); - testWidgets('control + shift + home/end keys (Windows only)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('control + shift + home/end keys (Windows only)', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -8945,7 +8961,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9015,14 +9031,15 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows }) ); - testWidgets('pageup/pagedown keys on Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('pageup/pagedown keys on Apple platforms', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, affinity: TextAffinity.upstream, ); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); const int lines = 2; await tester.pumpWidget(MaterialApp( home: Align( @@ -9036,7 +9053,7 @@ void main() { scrollController: scrollController, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.subtitle1!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9110,14 +9127,15 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('pageup/pagedown keys in a one line field on Apple platforms', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('pageup/pagedown keys in a one line field on Apple platforms', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, affinity: TextAffinity.upstream, ); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(MaterialApp( home: Align( alignment: Alignment.topLeft, @@ -9129,7 +9147,7 @@ void main() { scrollController: scrollController, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.subtitle1!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9188,10 +9206,9 @@ void main() { ); // Regression test for https://github.com/flutter/flutter/issues/31287 - testWidgets('text selection handle visibility', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text selection handle visibility', (WidgetTester tester) async { // Text with two separate words to select. - const String testText = 'XXXXX XXXXX'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'XXXXX XXXXX'; await tester.pumpWidget(MaterialApp( home: Align( @@ -9201,7 +9218,7 @@ void main() { child: EditableText( showSelectionHandles: true, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018(platform: TargetPlatform.iOS).black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9360,10 +9377,9 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }) ); - testWidgets("scrolling doesn't bounce", (WidgetTester tester) async { + testWidgetsWithLeakTracking("scrolling doesn't bounce", (WidgetTester tester) async { // 3 lines of text, where the last line overflows and requires scrolling. - const String testText = 'XXXXX\nXXXXX\nXXXXX'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'XXXXX\nXXXXX\nXXXXX'; await tester.pumpWidget(MaterialApp( home: Align( @@ -9374,7 +9390,7 @@ void main() { showSelectionHandles: true, maxLines: 2, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9410,11 +9426,13 @@ void main() { expect(scrollable.controller!.position.pixels, equals(renderEditable.maxScrollExtent)); }); - testWidgets('bringIntoView brings the caret into view when in a viewport', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bringIntoView brings the caret into view when in a viewport', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/55547. - final TextEditingController controller = TextEditingController(text: testText * 20); + controller.text = testText * 20; final ScrollController editableScrollController = ScrollController(); + addTearDown(editableScrollController.dispose); final ScrollController outerController = ScrollController(); + addTearDown(outerController.dispose); await tester.pumpWidget(MaterialApp( home: Align( @@ -9428,7 +9446,7 @@ void main() { maxLines: null, controller: controller, scrollController: editableScrollController, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9452,9 +9470,10 @@ void main() { expect(editableScrollController.offset, 0); }); - testWidgets('bringIntoView does nothing if the physics prohibits implicit scrolling', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText * 20); + testWidgetsWithLeakTracking('bringIntoView does nothing if the physics prohibits implicit scrolling', (WidgetTester tester) async { + controller.text = testText * 20; final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Future<void> buildWithPhysics({ ScrollPhysics? physics }) async { await tester.pumpWidget(MaterialApp( @@ -9467,7 +9486,7 @@ void main() { maxLines: null, controller: controller, scrollController: scrollController, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9499,15 +9518,18 @@ void main() { expect(scrollController.offset, 0); }); - testWidgets('can change scroll controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can change scroll controller', (WidgetTester tester) async { + controller.text = 'A' * 1000; final _TestScrollController scrollController1 = _TestScrollController(); + addTearDown(scrollController1.dispose); final _TestScrollController scrollController2 = _TestScrollController(); + addTearDown(scrollController2.dispose); await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A' * 1000), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9523,8 +9545,8 @@ void main() { await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A' * 1000), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9540,8 +9562,8 @@ void main() { await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A' * 1000), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9556,8 +9578,8 @@ void main() { await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A' * 1000), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9570,15 +9592,15 @@ void main() { expect(scrollController2.attached, isTrue); }); - testWidgets('getLocalRectForCaret does not throw when it sees an infinite point', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getLocalRectForCaret does not throw when it sees an infinite point', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SkipPainting( child: Transform( transform: Matrix4.zero(), child: EditableText( - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -9594,8 +9616,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('obscured multiline fields throw an exception', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('obscured multiline fields throw an exception', (WidgetTester tester) async { expect( () { EditableText( @@ -9626,33 +9647,32 @@ void main() { }); group('batch editing', () { - final TextEditingController controller = TextEditingController(text: testText); - final EditableText editableText = EditableText( - showSelectionHandles: true, - maxLines: 2, - controller: controller, - focusNode: FocusNode(), - cursorColor: Colors.red, - backgroundCursorColor: Colors.blue, - style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), - keyboardType: TextInputType.text, - ); - - final Widget widget = MediaQuery( - data: const MediaQueryData(), - child: Directionality( - textDirection: TextDirection.ltr, - child: editableText, - ), - ); + Widget buildWidget() { + return MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + showSelectionHandles: true, + maxLines: 2, + controller: controller, + focusNode: focusNode, + cursorColor: Colors.red, + backgroundCursorColor: Colors.blue, + style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), + keyboardType: TextInputType.text, + ), + ), + ); + } - testWidgets('batch editing works', (WidgetTester tester) async { - await tester.pumpWidget(widget); + testWidgetsWithLeakTracking('batch editing works', (WidgetTester tester) async { + await tester.pumpWidget(buildWidget()); // Connect. await tester.showKeyboard(find.byType(EditableText)); - final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); + final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); state.updateEditingValue(const TextEditingValue(text: 'remote value')); tester.testTextInput.log.clear(); @@ -9685,13 +9705,13 @@ void main() { ); }); - testWidgets('batch edits need to be nested properly', (WidgetTester tester) async { - await tester.pumpWidget(widget); + testWidgetsWithLeakTracking('batch edits need to be nested properly', (WidgetTester tester) async { + await tester.pumpWidget(buildWidget()); // Connect. await tester.showKeyboard(find.byType(EditableText)); - final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); + final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); state.updateEditingValue(const TextEditingValue(text: 'remote value')); tester.testTextInput.log.clear(); @@ -9705,13 +9725,13 @@ void main() { expect(errorString, contains('Unbalanced call to endBatchEdit')); }); - testWidgets('catch unfinished batch edits on disposal', (WidgetTester tester) async { - await tester.pumpWidget(widget); + testWidgetsWithLeakTracking('catch unfinished batch edits on disposal', (WidgetTester tester) async { + await tester.pumpWidget(buildWidget()); // Connect. await tester.showKeyboard(find.byType(EditableText)); - final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); + final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); state.updateEditingValue(const TextEditingValue(text: 'remote value')); tester.testTextInput.log.clear(); @@ -9724,12 +9744,12 @@ void main() { }); group('EditableText does not send editing values more than once', () { - Widget boilerplate(TextEditingController controller) { + Widget boilerplate() { final EditableText editableText = EditableText( showSelectionHandles: true, maxLines: 2, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -9755,9 +9775,9 @@ void main() { ); } - testWidgets('input from text input plugin', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); - await tester.pumpWidget(boilerplate(controller)); + testWidgetsWithLeakTracking('input from text input plugin', (WidgetTester tester) async { + controller.text = testText; + await tester.pumpWidget(boilerplate()); // Connect. await tester.showKeyboard(find.byType(EditableText)); @@ -9785,9 +9805,9 @@ void main() { expect(tester.testTextInput.log, isEmpty); }); - testWidgets('input from text selection menu', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); - await tester.pumpWidget(boilerplate(controller)); + testWidgetsWithLeakTracking('input from text selection menu', (WidgetTester tester) async { + controller.text = testText; + await tester.pumpWidget(boilerplate()); // Connect. await tester.showKeyboard(find.byType(EditableText)); @@ -9810,9 +9830,9 @@ void main() { tester.testTextInput.log.clear(); }); - testWidgets('input from controller', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); - await tester.pumpWidget(boilerplate(controller)); + testWidgetsWithLeakTracking('input from controller', (WidgetTester tester) async { + controller.text = testText; + await tester.pumpWidget(boilerplate()); // Connect. await tester.showKeyboard(find.byType(EditableText)); @@ -9827,8 +9847,7 @@ void main() { expect(updates, <TextEditingValue>[collapsedAtEnd('remoteremoteremote listener')]); }); - testWidgets('input from changing controller', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('input from changing controller', (WidgetTester tester) async { Widget build({ TextEditingController? textEditingController }) { return MediaQuery( data: const MediaQueryData(), @@ -9838,7 +9857,7 @@ void main() { showSelectionHandles: true, maxLines: 2, controller: textEditingController ?? controller, - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -9854,7 +9873,9 @@ void main() { // Connect. await tester.showKeyboard(find.byType(EditableText)); tester.testTextInput.log.clear(); - await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new text'))); + final TextEditingController controller1 = TextEditingController(text: 'new text'); + addTearDown(controller1.dispose); + await tester.pumpWidget(build(textEditingController: controller1)); List<TextEditingValue> updates = tester.testTextInput.log .where((MethodCall call) => call.method == 'TextInput.setEditingState') @@ -9864,7 +9885,9 @@ void main() { expect(updates, const <TextEditingValue>[TextEditingValue(text: 'new text')]); tester.testTextInput.log.clear(); - await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new new text'))); + final TextEditingController controller2 = TextEditingController(text: 'new new text'); + addTearDown(controller2.dispose); + await tester.pumpWidget(build(textEditingController: controller2)); updates = tester.testTextInput.log .where((MethodCall call) => call.method == 'TextInput.setEditingState') @@ -9875,14 +9898,13 @@ void main() { }); }); - testWidgets('input imm channel calls are ordered correctly', (WidgetTester tester) async { - const String testText = 'flutter is the best!'; - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('input imm channel calls are ordered correctly', (WidgetTester tester) async { + controller.text = 'flutter is the best!'; final EditableText et = EditableText( showSelectionHandles: true, maxLines: 2, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -9923,26 +9945,34 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'keyboard is requested after setEditingState after switching to a new text field', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/68571. + final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); + final FocusNode focusNode1 = FocusNode(); + addTearDown(focusNode1.dispose); final EditableText editableText1 = EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller1, + focusNode: focusNode1, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), keyboardType: TextInputType.text, ); + final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); + final FocusNode focusNode2 = FocusNode(); + addTearDown(focusNode2.dispose); final EditableText editableText2 = EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller2, + focusNode: focusNode2, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -9985,15 +10015,18 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Autofill does not request focus', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91354 . + final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); final FocusNode focusNode1 = FocusNode(); + addTearDown(focusNode1.dispose); final EditableText editableText1 = EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController(), + controller: controller1, focusNode: focusNode1, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, @@ -10001,11 +10034,14 @@ void main() { keyboardType: TextInputType.text, ); + final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); final FocusNode focusNode2 = FocusNode(); + addTearDown(focusNode2.dispose); final EditableText editableText2 = EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController(), + controller: controller2, focusNode: focusNode2, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, @@ -10036,15 +10072,14 @@ void main() { expect(focusNode2.hasFocus, isFalse); }); - testWidgets('setEditingState is not called when text changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setEditingState is not called when text changes', (WidgetTester tester) async { // We shouldn't get a message here because this change is owned by the platform side. - const String testText = 'flutter is the best!'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'flutter is the best!'; final EditableText et = EditableText( showSelectionHandles: true, maxLines: 2, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -10085,15 +10120,14 @@ void main() { expect(tester.testTextInput.editingState!['text'], 'flutter is the best!'); }); - testWidgets('setEditingState is called when text changes on controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setEditingState is called when text changes on controller', (WidgetTester tester) async { // We should get a message here because this change is owned by the framework side. - const String testText = 'flutter is the best!'; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = 'flutter is the best!'; final EditableText et = EditableText( showSelectionHandles: true, maxLines: 2, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -10135,7 +10169,7 @@ void main() { expect(tester.testTextInput.editingState!['text'], 'flutter is the best!...'); }); - testWidgets('Synchronous test of local and remote editing values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Synchronous test of local and remote editing values', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/65059 final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { @@ -10148,10 +10182,8 @@ void main() { } return newValue; }); - final TextEditingController controller = TextEditingController(); late StateSetter setState; - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node'); Widget builder() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -10266,7 +10298,7 @@ void main() { ); }); - testWidgets('Send text input state to engine when the input formatter rejects user input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Send text input state to engine when the input formatter rejects user input', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/67828 final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { @@ -10276,9 +10308,7 @@ void main() { final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { return collapsedAtEnd('Flutter is the best!'); }); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node'); Widget builder() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -10341,16 +10371,14 @@ void main() { ))); }); - testWidgets('Repeatedly receiving [TextEditingValue] will not trigger a keyboard request', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Repeatedly receiving [TextEditingValue] will not trigger a keyboard request', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66036 final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node'); Widget builder() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -10428,8 +10456,7 @@ void main() { }); group('TextEditingController', () { - testWidgets('TextEditingController.text set to empty string clears field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('TextEditingController.text set to empty string clears field', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: MediaQuery( @@ -10463,16 +10490,14 @@ void main() { expect(find.text('...'), findsNothing); }); - testWidgets('TextEditingController.clear() behavior test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextEditingController.clear() behavior test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66316 final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); return null; }); - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node'); Widget builder() { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -10538,8 +10563,9 @@ void main() { ); }); - testWidgets('TextEditingController.buildTextSpan receives build context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextEditingController.buildTextSpan receives build context', (WidgetTester tester) async { final _AccentColorTextEditingController controller = _AccentColorTextEditingController('a'); + addTearDown(controller.dispose); const Color color = Color.fromARGB(255, 1, 2, 3); final ThemeData lightTheme = ThemeData.light(); await tester.pumpWidget(MaterialApp( @@ -10548,7 +10574,7 @@ void main() { ), home: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -10560,9 +10586,8 @@ void main() { expect(textSpan.style!.color, color); }); - testWidgets('controller listener changes value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('controller listener changes value', (WidgetTester tester) async { const double maxValue = 5.5555; - final TextEditingController controller = TextEditingController(); controller.addListener(() { final double value = double.tryParse(controller.text.trim()) ?? .0; @@ -10596,8 +10621,8 @@ void main() { }); }); - testWidgets('autofocus:true on first frame does not throw', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('autofocus:true on first frame does not throw', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -10610,7 +10635,7 @@ void main() { controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -10627,7 +10652,7 @@ void main() { expect(exception, isNull); }); - testWidgets('updateEditingValue filters multiple calls from formatter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('updateEditingValue filters multiple calls from formatter', (WidgetTester tester) async { final MockTextFormatter formatter = MockTextFormatter(); await tester.pumpWidget( MediaQuery( @@ -10699,7 +10724,7 @@ void main() { expect(formatter.log, referenceLog); }); - testWidgets('formatter logic handles repeat filtering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('formatter logic handles repeat filtering', (WidgetTester tester) async { final MockTextFormatter formatter = MockTextFormatter(); await tester.pumpWidget( MediaQuery( @@ -10780,7 +10805,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/53612 - testWidgets('formatter logic handles initial repeat edge case', (WidgetTester tester) async { + testWidgetsWithLeakTracking('formatter logic handles initial repeat edge case', (WidgetTester tester) async { final MockTextFormatter formatter = MockTextFormatter(); await tester.pumpWidget( MediaQuery( @@ -10822,7 +10847,7 @@ void main() { expect(formatter.lastOldValue.text, 'test'); }); - testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -10879,13 +10904,13 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('Can access characters on editing string', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can access characters on editing string', (WidgetTester tester) async { late int charactersLength; final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, selectionControls: materialTextSelectionControls, @@ -10905,7 +10930,7 @@ void main() { expect(charactersLength, 1); }); - testWidgets('EditableText can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(MediaQuery( data: const MediaQueryData(), child: Directionality( @@ -10947,7 +10972,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('EditableText inherits DefaultTextHeightBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText inherits DefaultTextHeightBehavior', (WidgetTester tester) async { const TextHeightBehavior customTextHeightBehavior = TextHeightBehavior( applyHeightToFirstAscent: false, ); @@ -10975,7 +11000,7 @@ void main() { expect(renderObject.textHeightBehavior, equals(customTextHeightBehavior)); }); - testWidgets('EditableText defaultTextHeightBehavior is used over inherited widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText defaultTextHeightBehavior is used over inherited widget', (WidgetTester tester) async { const TextHeightBehavior inheritedTextHeightBehavior = TextHeightBehavior( applyHeightToFirstAscent: false, ); @@ -11013,7 +11038,9 @@ void main() { void expectToAssert(TextEditingValue value, bool shouldAssert) { dynamic initException; dynamic updateException; - controller = TextEditingController(); + + TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); try { controller = TextEditingController.fromValue(value); } catch (e) { @@ -11021,6 +11048,7 @@ void main() { } controller = TextEditingController(); + addTearDown(controller.dispose); try { controller.value = value; } catch (e) { @@ -11037,7 +11065,7 @@ void main() { expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: -1, end: 9)), false); }); - testWidgets('Preserves composing range if cursor moves within that range', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Preserves composing range if cursor moves within that range', (WidgetTester tester) async { final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, @@ -11059,7 +11087,7 @@ void main() { expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12)); }); - testWidgets('Clears composing range if cursor moves outside that range', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Clears composing range if cursor moves outside that range', (WidgetTester tester) async { final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, @@ -11100,7 +11128,7 @@ void main() { expect(state.currentTextEditingValue.composing, TextRange.empty); }); - testWidgets('Clears composing range if cursor moves outside that range - case two', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Clears composing range if cursor moves outside that range - case two', (WidgetTester tester) async { final Widget widget = MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, @@ -11182,7 +11210,7 @@ void main() { } // Regression test for https://github.com/flutter/flutter/issues/65374. - testWidgets('will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async { await setupWidget( tester, LengthLimitingTextInputFormatter( @@ -11208,7 +11236,7 @@ void main() { expect(state.currentTextEditingValue.composing, TextRange.empty); }); - testWidgets('handles composing text correctly, continued', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles composing text correctly, continued', (WidgetTester tester) async { await setupWidget( tester, LengthLimitingTextInputFormatter( @@ -11240,7 +11268,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/68086. - testWidgets('enforced composing truncated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('enforced composing truncated', (WidgetTester tester) async { await setupWidget( tester, LengthLimitingTextInputFormatter( @@ -11278,7 +11306,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/68086. - testWidgets('default truncate behaviors with different platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default truncate behaviors with different platforms', (WidgetTester tester) async { await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); @@ -11318,7 +11346,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/68086. - testWidgets("composing range removed if it's overflowed the truncated value's length", (WidgetTester tester) async { + testWidgetsWithLeakTracking("composing range removed if it's overflowed the truncated value's length", (WidgetTester tester) async { await setupWidget( tester, LengthLimitingTextInputFormatter( @@ -11347,7 +11375,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/68086. - testWidgets('composing range removed with different platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('composing range removed with different platforms', (WidgetTester tester) async { await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); @@ -11378,7 +11406,7 @@ void main() { } }); - testWidgets("composing range handled correctly when it's overflowed", (WidgetTester tester) async { + testWidgetsWithLeakTracking("composing range handled correctly when it's overflowed", (WidgetTester tester) async { const String string = '👨‍👩‍👦0123456'; await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); @@ -11399,7 +11427,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/68086. - testWidgets('typing in the middle with different platforms.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('typing in the middle with different platforms.', (WidgetTester tester) async { await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); @@ -11441,15 +11469,15 @@ void main() { group('callback errors', () { const String errorText = 'Test EditableText callback error'; - testWidgets('onSelectionChanged can throw errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSelectionChanged can throw errors', (WidgetTester tester) async { + controller.text = 'flutter is the best!'; + await tester.pumpWidget(MaterialApp( home: EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController( - text: 'flutter is the best!', - ), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -11468,15 +11496,15 @@ void main() { expect(error.toString(), contains(errorText)); }); - testWidgets('onChanged can throw errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onChanged can throw errors', (WidgetTester tester) async { + controller.text = 'flutter is the best!'; + await tester.pumpWidget(MaterialApp( home: EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController( - text: 'flutter is the best!', - ), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -11494,15 +11522,15 @@ void main() { expect(error.toString(), contains(errorText)); }); - testWidgets('onEditingComplete can throw errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onEditingComplete can throw errors', (WidgetTester tester) async { + controller.text = 'flutter is the best!'; + await tester.pumpWidget(MaterialApp( home: EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController( - text: 'flutter is the best!', - ), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -11525,15 +11553,15 @@ void main() { expect(error.toString(), contains(errorText)); }); - testWidgets('onSubmitted can throw errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSubmitted can throw errors', (WidgetTester tester) async { + controller.text = 'flutter is the best!'; + await tester.pumpWidget(MaterialApp( home: EditableText( showSelectionHandles: true, maxLines: 2, - controller: TextEditingController( - text: 'flutter is the best!', - ), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -11556,20 +11584,19 @@ void main() { expect(error.toString(), contains(errorText)); }); - testWidgets('input formatters can throw errors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('input formatters can throw errors', (WidgetTester tester) async { final TextInputFormatter badFormatter = TextInputFormatter.withFunction( (TextEditingValue oldValue, TextEditingValue newValue) => throw FlutterError(errorText), ); - final TextEditingController controller = TextEditingController( - text: 'flutter is the best!', - ); + controller.text = 'flutter is the best!'; + await tester.pumpWidget(MaterialApp( home: EditableText( showSelectionHandles: true, maxLines: 2, controller: controller, inputFormatters: <TextInputFormatter>[badFormatter], - focusNode: FocusNode(), + focusNode: focusNode, cursorColor: Colors.red, backgroundCursorColor: Colors.blue, style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), @@ -11591,8 +11618,10 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/72400. - testWidgets("delete doesn't cause crash when selection is -1,-1", (WidgetTester tester) async { + testWidgetsWithLeakTracking("delete doesn't cause crash when selection is -1,-1", (WidgetTester tester) async { final UnsettableController unsettableController = UnsettableController(); + addTearDown(unsettableController.dispose); + await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), @@ -11624,7 +11653,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('can change behavior by overriding text editing shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can change behavior by overriding text editing shortcuts', (WidgetTester tester) async { const Map<SingleActivator, Intent> testShortcuts = <SingleActivator, Intent>{ SingleActivator(LogicalKeyboardKey.arrowLeft): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true), SingleActivator(LogicalKeyboardKey.keyX, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true), @@ -11632,7 +11661,7 @@ void main() { SingleActivator(LogicalKeyboardKey.keyV, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true), SingleActivator(LogicalKeyboardKey.keyA, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true), }; - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -11689,8 +11718,8 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, skip: kIsWeb); // [intended] - testWidgets('navigating by word', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'word word word'); + testWidgetsWithLeakTracking('navigating by word', (WidgetTester tester) async { + controller.text = 'word word word'; // word wo|rd| word controller.selection = const TextSelection( baseOffset: 7, @@ -11826,9 +11855,8 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('navigating multiline text', (WidgetTester tester) async { - const String multilineText = 'word word word\nword word\nword'; // 15 + 10 + 4; - final TextEditingController controller = TextEditingController(text: multilineText); + testWidgetsWithLeakTracking('navigating multiline text', (WidgetTester tester) async { + controller.text = 'word word word\nword word\nword'; // 15 + 10 + 4; // wo|rd wo|rd controller.selection = const TextSelection( baseOffset: 17, @@ -11985,9 +12013,8 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets("Mac's expand by line behavior on multiple lines", (WidgetTester tester) async { - const String multilineText = 'word word word\nword word\nword'; // 15 + 10 + 4; - final TextEditingController controller = TextEditingController(text: multilineText); + testWidgetsWithLeakTracking("Mac's expand by line behavior on multiple lines", (WidgetTester tester) async { + controller.text = 'word word word\nword word\nword'; // 15 + 10 + 4; // word word word // wo|rd word // w|ord @@ -12087,9 +12114,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }) ); - testWidgets("Mac's expand extent position", (WidgetTester tester) async { - const String testText = 'Now is the time for all good people to come to the aid of their country'; - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking("Mac's expand extent position", (WidgetTester tester) async { + controller.text = 'Now is the time for all good people to come to the aid of their country'; // Start the selection in the middle somewhere. controller.selection = const TextSelection.collapsed(offset: 10); await tester.pumpWidget(MaterialApp( @@ -12321,8 +12347,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }) ); - testWidgets('expanding selection to start/end single line', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'word word word'); + testWidgetsWithLeakTracking('expanding selection to start/end single line', (WidgetTester tester) async { + controller.text = 'word word word'; // word wo|rd| word controller.selection = const TextSelection( baseOffset: 7, @@ -12408,8 +12434,8 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }) ); - testWidgets('can change text editing behavior by overriding actions', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: testText); + testWidgetsWithLeakTracking('can change text editing behavior by overriding actions', (WidgetTester tester) async { + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -12454,10 +12480,8 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, skip: kIsWeb); // [intended] - testWidgets('ignore key event from web platform', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'test\ntest', - ); + testWidgetsWithLeakTracking('ignore key event from web platform', (WidgetTester tester) async { + controller.text = 'test\ntest'; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -12510,7 +12534,7 @@ void main() { } }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async { late StateSetter setState; bool enableInteractiveSelection = true; await tester.pumpWidget( @@ -12576,13 +12600,14 @@ void main() { // On web, using keyboard for selection is handled by the browser. }, skip: kIsWeb); // [intended] - testWidgets('EditableText does not leak animation controllers', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('EditableText does not leak animation controllers', (WidgetTester tester) async { + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( autofocus: true, - controller: TextEditingController(text: 'A'), + controller: controller, focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, @@ -12612,12 +12637,12 @@ void main() { expect(tester.hasRunningAnimations, isFalse); }); - testWidgets('Floating cursor affinity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating cursor affinity', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; - final FocusNode focusNode = FocusNode(); final GlobalKey key = GlobalKey(); // Set it up so that there will be word-wrap. - final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'); + controller.text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'; + await tester.pumpWidget( MaterialApp( home: Center( @@ -12674,16 +12699,20 @@ void main() { )); EditableText.debugDeterministicCursor = false; - }); + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 2}, + )); -testWidgets('Floating cursor ending with selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating cursor ending with selection', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; - final FocusNode focusNode = FocusNode(); final GlobalKey key = GlobalKey(); SelectionChangedCause? lastSelectionChangedCause; - - final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n1234567890'); + controller.text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n1234567890'; controller.selection = const TextSelection.collapsed(offset: 0); + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -12855,8 +12884,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async lastSelectionChangedCause = null; EditableText.debugDeterministicCursor = false; - }); - + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134386 + notDisposedAllowList: <String, int?> {'LeaderLayer': 8}, + )); group('Selection changed scroll into view', () { final String text = List<int>.generate(64, (int index) => index).join('\n'); @@ -12864,6 +12897,11 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async final ScrollController scrollController = ScrollController(); late double maxScrollExtent; + tearDownAll(() { + controller.dispose(); + scrollController.dispose(); + }); + Future<void> resetSelectionAndScrollOffset(WidgetTester tester, {required bool setMaxScrollExtent}) async { controller.value = controller.value.copyWith( text: text, @@ -12877,7 +12915,6 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(scrollController.offset, targetOffset); } - Future<TextSelectionDelegate> pumpLongScrollableText(WidgetTester tester) async { final GlobalKey<EditableTextState> key = GlobalKey<EditableTextState>(); @@ -12908,7 +12945,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async return key.currentState!; } - testWidgets('SelectAll toolbar action will not set max scroll on designated platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectAll toolbar action will not set max scroll on designated platforms', (WidgetTester tester) async { final TextSelectionDelegate textSelectionDelegate = await pumpLongScrollableText(tester); await resetSelectionAndScrollOffset(tester, setMaxScrollExtent: false); @@ -12917,7 +12954,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(scrollController.offset, 0.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Selection will be scrolled into view with SelectionChangedCause', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selection will be scrolled into view with SelectionChangedCause', (WidgetTester tester) async { final TextSelectionDelegate textSelectionDelegate = await pumpLongScrollableText(tester); // Cut @@ -12966,13 +13003,11 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }); - testWidgets('Should not scroll on paste if caret already visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Should not scroll on paste if caret already visible', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/96658. final ScrollController scrollController = ScrollController(); - final TextEditingController controller = TextEditingController( - text: 'Lorem ipsum please paste here: \n${".\n" * 50}', - ); - final FocusNode focusNode = FocusNode(); + addTearDown(scrollController.dispose); + controller.text = 'Lorem ipsum please paste here: \n${".\n" * 50}'; await tester.pumpWidget( MaterialApp( @@ -13010,13 +13045,14 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(scrollController.offset, 0.0); }); - testWidgets('Autofill enabled by default', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Autofill enabled by default', (WidgetTester tester) async { + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( autofocus: true, - controller: TextEditingController(text: 'A'), + controller: controller, focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, @@ -13033,13 +13069,14 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets('Autofill can be disabled', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('Autofill can be disabled', (WidgetTester tester) async { + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( autofocus: true, - controller: TextEditingController(text: 'A'), + controller: controller, focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, @@ -13073,11 +13110,11 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async Future<void> sendUndo(WidgetTester tester) => sendUndoRedo(tester); Future<void> sendRedo(WidgetTester tester) => sendUndoRedo(tester, true); - Widget boilerplate(TextEditingController controller, [FocusNode? focusNode]) { + Widget boilerplate() { return MaterialApp( home: EditableText( controller: controller, - focusNode: focusNode ?? FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -13117,9 +13154,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async selection: TextSelection.collapsed(offset: textAC.length), ); - testWidgets('Should have no effect on an empty and non-focused field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - await tester.pumpWidget(boilerplate(controller)); + testWidgetsWithLeakTracking('Should have no effect on an empty and non-focused field', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); expect(controller.value, TextEditingValue.empty); // Undo/redo have no effect on an empty field that has never been edited. @@ -13133,10 +13169,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('Should have no effect on an empty and focused field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + testWidgetsWithLeakTracking('Should have no effect on an empty and focused field', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); await waitForThrottling(tester); expect(controller.value, TextEditingValue.empty); @@ -13144,31 +13178,29 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // state saved in text editing history. focusNode.requestFocus(); await tester.pump(); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); await waitForThrottling(tester); // Undo/redo should have no effect. The field is focused and the value has // changed, but the text remains empty. await sendUndo(tester); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); await sendRedo(tester); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('Can undo/redo a single insertion', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + testWidgetsWithLeakTracking('Can undo/redo a single insertion', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); // Focus the field and wait for throttling delay to get the initial // state saved in text editing history. focusNode.requestFocus(); await tester.pump(); await waitForThrottling(tester); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); // First insertion. await tester.enterText(find.byType(EditableText), textA); @@ -13198,17 +13230,15 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('Can undo/redo multiple insertions', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + testWidgetsWithLeakTracking('Can undo/redo multiple insertions', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); // Focus the field and wait for throttling delay to get the initial // state saved in text editing history. focusNode.requestFocus(); await tester.pump(); await waitForThrottling(tester); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); // First insertion. await tester.enterText(find.byType(EditableText), textA); @@ -13242,17 +13272,15 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // Regression test for https://github.com/flutter/flutter/issues/120794. // This is only reproducible on Android platform because it is the only // platform where composing changes are saved in the editing history. - testWidgets('Can undo as intented when adding a delay between undos', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + testWidgetsWithLeakTracking('Can undo as intented when adding a delay between undos', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); // Focus the field and wait for throttling delay to get the initial // state saved in text editing history. focusNode.requestFocus(); await tester.pump(); await waitForThrottling(tester); - expect(controller.value, emptyTextCollapsed); + expect(controller.value, emptyTextCollapsed); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); @@ -13298,11 +13326,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] // Regression test for https://github.com/flutter/flutter/issues/120194. - testWidgets('Cursor does not jump after undo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor does not jump after undo', (WidgetTester tester) async { // Initialize the controller with a non empty text. - final TextEditingController controller = TextEditingController(text: textA); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + controller.text = textA; + await tester.pumpWidget(boilerplate()); // Focus the field and wait for throttling delay to get the initial // state saved in text editing history. @@ -13323,11 +13350,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('Initial value is recorded when an undo is received just after getting the focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial value is recorded when an undo is received just after getting the focus', (WidgetTester tester) async { // Initialize the controller with a non empty text. - final TextEditingController controller = TextEditingController(text: textA); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + controller.text = textA; + await tester.pumpWidget(boilerplate()); // Focus the field and do not wait for throttling delay before calling undo. focusNode.requestFocus(); @@ -13349,10 +13375,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('Can make changes in the middle of the history', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - await tester.pumpWidget(boilerplate(controller, focusNode)); + testWidgetsWithLeakTracking('Can make changes in the middle of the history', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); // Focus the field and wait for throttling delay to get the initial // state saved in text editing history. @@ -13403,9 +13427,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('inside EditableText, duplicate changes', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('inside EditableText, duplicate changes', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -13534,14 +13556,13 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('inside EditableText, autofocus', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); + testWidgetsWithLeakTracking('inside EditableText, autofocus', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( autofocus: true, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: textStyle, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -13607,9 +13628,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('does not save composing changes (except Android)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('does not save composing changes (except Android)', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -13768,9 +13787,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.android }), skip: kIsWeb); // [intended] - testWidgets('does save composing changes on Android', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('does save composing changes on Android', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -14002,9 +14019,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // On web, these keyboard shortcuts are handled by the browser. }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] - testWidgets('saves right up to composing change even when throttled', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); + testWidgetsWithLeakTracking('saves right up to composing change even when throttled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( @@ -14204,9 +14219,11 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] }); - testWidgets('pasting with the keyboard collapses the selection and places it after the pasted content', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pasting with the keyboard collapses the selection and places it after the pasted content', (WidgetTester tester) async { Future<void> testPasteSelection(WidgetTester tester, _VoidFutureCallback paste) async { final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: EditableText( @@ -14339,7 +14356,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, skip: kIsWeb); // [intended] // Regression test for https://github.com/flutter/flutter/issues/98322. - testWidgets('EditableText consumes ActivateIntent and ButtonActivateIntent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText consumes ActivateIntent and ButtonActivateIntent', (WidgetTester tester) async { bool receivedIntent = false; await tester.pumpWidget( MaterialApp( @@ -14381,8 +14398,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }); // Regression test for https://github.com/flutter/flutter/issues/100585. - testWidgets('can paste and remove field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'text'); + testWidgetsWithLeakTracking('can paste and remove field', (WidgetTester tester) async { + controller.text = 'text'; late StateSetter setState; bool showField = true; final _CustomTextSelectionControls controls = _CustomTextSelectionControls( @@ -14431,8 +14448,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, skip: kIsWeb); // [intended] // Regression test for https://github.com/flutter/flutter/issues/100585. - testWidgets('can cut and remove field', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'text'); + testWidgetsWithLeakTracking('can cut and remove field', (WidgetTester tester) async { + controller.text = 'text'; late StateSetter setState; bool showField = true; final _CustomTextSelectionControls controls = _CustomTextSelectionControls( @@ -14482,10 +14499,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, skip: kIsWeb); // [intended] group('Mac document shortcuts', () { - testWidgets('ctrl-A/E', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ctrl-A/E', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -14501,7 +14518,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14563,10 +14580,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('ctrl-F/B', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ctrl-F/B', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -14582,7 +14599,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14629,10 +14646,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('ctrl-N/P', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ctrl-N/P', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -14648,7 +14665,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14708,11 +14725,11 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async await tester.pump(); } - testWidgets('with normal characters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with normal characters', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController(text: testText); + controller.text = testText; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -14728,7 +14745,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14803,15 +14820,13 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('with extended grapheme clusters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('with extended grapheme clusters', (WidgetTester tester) async { final String targetPlatformString = defaultTargetPlatform.toString(); final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase(); - final TextEditingController controller = TextEditingController( - // One extended grapheme cluster of length 8 and one surrogate pair of - // length 2. - text: '👨‍👩‍👦😆', - ); + // One extended grapheme cluster of length 8 and one surrogate pair of + // length 2. + controller.text = '👨‍👩‍👦😆'; controller.selection = const TextSelection( baseOffset: 0, extentOffset: 0, @@ -14827,7 +14842,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14887,7 +14902,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets('macOS selectors work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('macOS selectors work', (WidgetTester tester) async { controller.text = 'test\nline2'; controller.selection = TextSelection.collapsed(offset: controller.text.length); @@ -14904,7 +14919,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14945,9 +14960,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }); }); - testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); - final TextEditingController controller = TextEditingController(text: ''); await tester.pumpWidget(MaterialApp( home: Align( alignment: Alignment.topLeft, @@ -14958,7 +14972,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.subtitle1!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -14996,14 +15010,16 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); group('Spell check', () { - testWidgets( + testWidgetsWithLeakTracking( 'Spell check configured properly when spell check disabled by default', (WidgetTester tester) async { + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15018,14 +15034,16 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(state.spellCheckEnabled, isFalse); }); - testWidgets( + testWidgetsWithLeakTracking( 'Spell check configured properly when spell check disabled manually', (WidgetTester tester) async { + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15041,14 +15059,16 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(state.spellCheckEnabled, isFalse); }); - testWidgets( + testWidgetsWithLeakTracking( 'Error thrown when spell check configuration defined without specifying misspelled text style', (WidgetTester tester) async { + controller.text = 'A'; + expect( () { EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15061,17 +15081,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Spell check configured properly when spell check enabled without specified spell check service and native spell check service defined', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; + controller.text = 'A'; await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15095,16 +15116,17 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async tester.binding.platformDispatcher.clearNativeSpellCheckServiceDefined(); }); - testWidgets( + testWidgetsWithLeakTracking( 'Spell check configured properly with specified spell check service', (WidgetTester tester) async { final FakeSpellCheckService fakeSpellCheckService = FakeSpellCheckService(); + controller.text = 'A'; await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15127,17 +15149,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Spell check disabled when spell check configuration specified but no default spell check service available', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = false; + controller.text = 'A'; await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15158,16 +15181,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async tester.binding.platformDispatcher.clearNativeSpellCheckServiceDefined(); }); - testWidgets( + testWidgetsWithLeakTracking( 'findSuggestionSpanAtCursorIndex finds correct span with cursor in middle of a word', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15201,16 +15226,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(suggestionSpan, equals(expectedSpan)); }); - testWidgets( + testWidgetsWithLeakTracking( 'findSuggestionSpanAtCursorIndex finds correct span with cursor on edge of a word', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15243,16 +15270,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(suggestionSpan, equals(expectedSpan)); }); - testWidgets( + testWidgetsWithLeakTracking( 'findSuggestionSpanAtCursorIndex finds no span when cursor out of range of spans', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15285,16 +15314,18 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(suggestionSpan, isNull); }); - testWidgets( + testWidgetsWithLeakTracking( 'findSuggestionSpanAtCursorIndex finds no span when word correctly spelled', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; + controller.text = 'A'; + await tester.pumpWidget( MaterialApp( home: EditableText( - controller: TextEditingController(text: 'A'), - focusNode: FocusNode(), + controller: controller, + focusNode: focusNode, style: const TextStyle(), cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15327,7 +15358,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(suggestionSpan, isNull); }); - testWidgets('can show spell check suggestions toolbar when there are spell check results', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show spell check suggestions toolbar when there are spell check results', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; const TextEditingValue value = TextEditingValue( @@ -15388,7 +15419,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(find.text('DELETE'), matcher); }); - testWidgets('can show spell check suggestions toolbar when there are no spell check results on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show spell check suggestions toolbar when there are no spell check results on iOS', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; const TextEditingValue value = TextEditingValue( @@ -15450,7 +15481,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; const TextEditingValue value = TextEditingValue( @@ -15511,7 +15542,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async } }); - testWidgets('material spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async { + testWidgetsWithLeakTracking('material spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; const TextEditingValue value = TextEditingValue( @@ -15576,7 +15607,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async } }); - testWidgets('replacing puts cursor at the end of the word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('replacing puts cursor at the end of the word', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; controller.value = const TextEditingValue( @@ -15688,7 +15719,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('tapping on a misspelled word hides the handles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tapping on a misspelled word hides the handles', (WidgetTester tester) async { tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = true; controller.value = const TextEditingValue( @@ -15751,12 +15782,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }); group('magnifier', () { - testWidgets('should build nothing by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should build nothing by default', (WidgetTester tester) async { final EditableText editableText = EditableText( controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15772,27 +15803,29 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); final BuildContext context = tester.firstElement(find.byType(EditableText)); + final ValueNotifier<MagnifierInfo> notifier = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty); + addTearDown(notifier.dispose); expect( - editableText.magnifierConfiguration.magnifierBuilder( - context, - MagnifierController(), - ValueNotifier<MagnifierInfo>(MagnifierInfo.empty) - ), - isNull, + editableText.magnifierConfiguration.magnifierBuilder( + context, + MagnifierController(), + notifier, + ), + isNull, ); }); }); // Regression test for: https://github.com/flutter/flutter/issues/117418. - testWidgets('can handle the partial selection of a multi-code-unit glyph', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can handle the partial selection of a multi-code-unit glyph', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( controller: controller, showSelectionHandles: true, autofocus: true, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15828,12 +15861,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(tester.takeException(), null); }); - testWidgets('does not crash when didChangeMetrics is called after unmounting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not crash when didChangeMetrics is called after unmounting', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, @@ -15850,13 +15883,9 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async state.didChangeMetrics(); }); - testWidgets('_CompositionCallback widget does not skip frames', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_CompositionCallback widget does not skip frames', (WidgetTester tester) async { EditableText.debugDeterministicCursor = true; - final FocusNode focusNode = FocusNode(); - final TextEditingController controller = TextEditingController.fromValue( - const TextEditingValue(selection: TextSelection.collapsed(offset: 0)), - ); - + controller.value = const TextEditingValue(selection: TextSelection.collapsed(offset: 0)); Offset offset = Offset.zero; late StateSetter setState; @@ -15911,13 +15940,17 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }); group('selection behavior when receiving focus', () { - testWidgets('tabbing between fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tabbing between fields', (WidgetTester tester) async { final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); controller1.text = 'Text1'; controller2.text = 'Text2\nLine2'; final FocusNode focusNode1 = FocusNode(); + addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(); + addTearDown(focusNode2.dispose); await tester.pumpWidget( MaterialApp( @@ -16047,11 +16080,9 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets('Selection is updated when the field has focus and the new selection is invalid', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selection is updated when the field has focus and the new selection is invalid', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/120631. - final TextEditingController controller = TextEditingController(); controller.text = 'Text'; - final FocusNode focusNode = FocusNode(); await tester.pumpWidget( MaterialApp( @@ -16106,11 +16137,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets('when having focus stolen between frames on web', (WidgetTester tester) async { - final TextEditingController controller1 = TextEditingController(); - controller1.text = 'Text1'; + testWidgetsWithLeakTracking('when having focus stolen between frames on web', (WidgetTester tester) async { + controller.text = 'Text1'; final FocusNode focusNode1 = FocusNode(); + addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(); + addTearDown(focusNode2.dispose); await tester.pumpWidget( MaterialApp( @@ -16118,8 +16150,8 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ EditableText( - key: ValueKey<String>(controller1.text), - controller: controller1, + key: ValueKey<String>(controller.text), + controller: controller, focusNode: focusNode1, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, @@ -16139,7 +16171,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(focusNode1.hasFocus, isFalse); expect(focusNode2.hasFocus, isFalse); expect( - controller1.selection, + controller.selection, const TextSelection.collapsed(offset: -1), ); @@ -16148,7 +16180,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async // Set the text editing value in order to trigger an internal call to // requestFocus. state.userUpdateTextEditingValue( - controller1.value, + controller.value, SelectionChangedCause.keyboard, ); // Focus takes a frame to update, so it hasn't changed yet. @@ -16169,10 +16201,10 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(focusNode1.hasFocus, isTrue); expect(focusNode2.hasFocus, isFalse); expect( - controller1.selection, + controller.selection, TextSelection( baseOffset: 0, - extentOffset: controller1.text.length, + extentOffset: controller.text.length, ), ); }, @@ -16180,7 +16212,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); - testWidgets('EditableText respects MediaQuery.boldText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EditableText respects MediaQuery.boldText', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( @@ -16201,12 +16233,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(state.buildTextSpan().style!.fontWeight, FontWeight.bold); }); - testWidgets('code points are treated as single characters in obscure mode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('code points are treated as single characters in obscure mode', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -16337,12 +16369,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('when manually placing the cursor in the middle of a code point', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when manually placing the cursor in the middle of a code point', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -16421,12 +16453,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('when inserting a malformed string', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when inserting a malformed string', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -16483,12 +16515,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('when inserting a malformed string that is a sequence of dangling high surrogates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when inserting a malformed string that is a sequence of dangling high surrogates', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -16543,12 +16575,12 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async skip: kIsWeb, // [intended] ); - testWidgets('when inserting a malformed string that is a sequence of dangling low surrogates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when inserting a malformed string that is a sequence of dangling low surrogates', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: EditableText( backgroundCursorColor: Colors.grey, - controller: TextEditingController(), + controller: controller, focusNode: focusNode, obscureText: true, toolbarOptions: const ToolbarOptions( @@ -16602,6 +16634,82 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async }, skip: kIsWeb, // [intended] ); + + group('hasStrings', () { + late int calls; + setUp(() { + calls = 0; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) { + if (methodCall.method == 'Clipboard.hasStrings') { + calls += 1; + } + return Future<void>.value(); + }); + }); + tearDown(() { + TestWidgetsFlutterBinding.ensureInitialized() + .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); + }); + + testWidgetsWithLeakTracking('web avoids the paste permissions prompt by not calling hasStrings', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + expect(calls, equals(kIsWeb ? 0 : 1)); + + // Long-press to bring up the context menu. + final Finder textFinder = find.byType(EditableText); + await tester.longPress(textFinder); + tester.state<EditableTextState>(textFinder).showToolbar(); + await tester.pumpAndSettle(); + + expect(calls, equals(kIsWeb ? 0 : 2)); + }); + }); + + testWidgetsWithLeakTracking('Cursor color with an opacity is respected', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + const double opacity = 0.55; + controller.text = 'blah blah'; + + await tester.pumpWidget( + MaterialApp( + home: EditableText( + key: key, + cursorColor: cursorColor.withOpacity(opacity), + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + ), + ), + ); + + // Tap to show the cursor. + await tester.tap(find.byKey(key)); + await tester.pumpAndSettle(); + + final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); + expect(state.renderEditable.cursorColor, cursorColor.withOpacity(opacity)); + }); } class UnsettableController extends TextEditingController { @@ -16885,6 +16993,16 @@ class TransformedEditableText extends StatefulWidget { class _TransformedEditableTextState extends State<TransformedEditableText> { bool _isTransformed = false; + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MediaQuery( @@ -16897,8 +17015,8 @@ class _TransformedEditableTextState extends State<TransformedEditableText> { Transform.translate( offset: _isTransformed ? widget.offset : Offset.zero, child: EditableText( - controller: TextEditingController(), - focusNode: FocusNode(), + controller: _controller, + focusNode: _focusNode, style: Typography.material2018().black.titleMedium!, cursorColor: Colors.blue, backgroundCursorColor: Colors.grey, diff --git a/packages/flutter/test/widgets/ensure_visible_test.dart b/packages/flutter/test/widgets/ensure_visible_test.dart index 16f493804cadb..97898ca632f71 100644 --- a/packages/flutter/test/widgets/ensure_visible_test.dart +++ b/packages/flutter/test/widgets/ensure_visible_test.dart @@ -7,6 +7,9 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +import 'two_dimensional_utils.dart'; Finder findKey(int i) => find.byKey(ValueKey<int>(i), skipOffstage: false); @@ -68,7 +71,7 @@ Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWr void main() { group('SingleChildScrollView', () { - testWidgets('SingleChildScrollView ensureVisible Axis.vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical)); @@ -95,7 +98,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); - testWidgets('SingleChildScrollView ensureVisible Axis.horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal)); @@ -122,7 +125,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); - testWidgets('SingleChildScrollView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical, reverse: true)); @@ -190,7 +193,7 @@ void main() { expect(tester.getBottomLeft(findKey(6)).dy, equals(500.0)); }); - testWidgets('SingleChildScrollView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal, reverse: true)); @@ -263,7 +266,7 @@ void main() { expect(tester.getBottomLeft(findKey(6)).dx, equals(500.0)); }); - testWidgets('SingleChildScrollView ensureVisible rotated child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView ensureVisible rotated child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget( @@ -310,7 +313,7 @@ void main() { expect(tester.getTopLeft(findKey(0)).dy, moreOrLessEquals(500.0, epsilon: 0.1)); }); - testWidgets('Nested SingleChildScrollView ensureVisible behavior test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested SingleChildScrollView ensureVisible behavior test', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/65100 Finder findKey(String coordinate) => find.byKey(ValueKey<String>(coordinate)); BuildContext findContext(String coordinate) => tester.element(findKey(coordinate)); @@ -388,7 +391,7 @@ void main() { }); group('ListView', () { - testWidgets('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -424,7 +427,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); - testWidgets('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -460,7 +463,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); - testWidgets('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -536,7 +539,7 @@ void main() { expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); }); - testWidgets('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -617,7 +620,7 @@ void main() { expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); }); - testWidgets('ListView ensureVisible negative child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible negative child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -675,7 +678,7 @@ void main() { expect(getOffset(), equals(-400.0)); }); - testWidgets('ListView ensureVisible rotated child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible rotated child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -728,7 +731,7 @@ void main() { }); group('ListView shrinkWrap', () { - testWidgets('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -764,7 +767,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); - testWidgets('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -800,7 +803,7 @@ void main() { expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); - testWidgets('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -876,7 +879,7 @@ void main() { expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); }); - testWidgets('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -959,7 +962,7 @@ void main() { }); group('Scrollable with center', () { - testWidgets('ensureVisible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ensureVisible', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); @@ -1050,4 +1053,279 @@ void main() { expect(tester.getTopLeft(findKey(-3)).dy, equals(100.0)); }); }); + + group('TwoDimensionalViewport ensureVisible', () { + Finder findKey(ChildVicinity vicinity) { + return find.byKey(ValueKey<ChildVicinity>(vicinity)); + } + + BuildContext findContext(WidgetTester tester, ChildVicinity vicinity) { + return tester.element(findKey(vicinity)); + } + + testWidgets('Axis.vertical', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 0)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(0.0), + ); + // (0, 3) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(600.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 3)), + ); + await tester.pump(); + // Now in view at top edge of viewport + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(0.0), + ); + + // If already visible, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 3)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(0.0), + ); + }); + + testWidgets('Axis.horizontal', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 1, yIndex: 0)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 1, yIndex: 0))).dx, + equals(0.0), + ); + // (5, 0) is now in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(800.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 0)), + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + await tester.pump(); + // Now in view at trailing edge of viewport + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(600.0), + ); + + // If already in position, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 0)), + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(600.0), + ); + }); + + testWidgets('both axes', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 1, yIndex: 1)), + ); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + // (5, 4) is in the cache extent, and will be brought into view next + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(800.0, 600.0, 1000.0, 800.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 4)), + alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd + ); + await tester.pump(); + // Now in view at bottom trailing corner of viewport + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + + // If already visible, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 4)), + alignment: 1.0, + ); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + }); + + testWidgets('Axis.vertical reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + verticalDetails: const ScrollableDetails.vertical(reverse: true), + useCacheExtent: true, + )); + + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(400.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 0)), + ); + await tester.pump(); + // Already visible so no change. + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(400.0), + ); + // (0, 3) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(-200.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 3)), + ); + await tester.pump(); + // Now in view at bottom edge of viewport since we are reversed + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(400.0), + ); + + // If already visible, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 3)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(400.0), + ); + }); + + testWidgets('Axis.horizontal reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + horizontalDetails: const ScrollableDetails.horizontal(reverse: true), + useCacheExtent: true, + )); + + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, + equals(600.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 0, yIndex: 0)), + ); + await tester.pump(); + // Already visible so no change. + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, + equals(600.0), + ); + // (4, 0) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(-200.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 4, yIndex: 0)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(200.0), + ); + + // If already visible, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 4, yIndex: 0)), + ); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(200.0), + ); + }); + + testWidgets('both axes reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + verticalDetails: const ScrollableDetails.vertical(reverse: true), + horizontalDetails: const ScrollableDetails.horizontal(reverse: true), + useCacheExtent: true, + )); + + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 1, yIndex: 1)), + ); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + // (5, 4) is in the cache extent, and will be brought into view next + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(-200.0, -200.0, 0.0, 0.0), + ); + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 4)), + alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd + ); + await tester.pump(); + // Now in view at trailing corner of viewport + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + + // If already visible, no change + Scrollable.ensureVisible(findContext( + tester, + const ChildVicinity(xIndex: 5, yIndex: 4)), + alignment: 1.0, + ); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + }); + }); } diff --git a/packages/flutter/test/widgets/error_widget_builder_test.dart b/packages/flutter/test/widgets/error_widget_builder_test.dart index 6e9fa6eaaa13b..c5c075897aded 100644 --- a/packages/flutter/test/widgets/error_widget_builder_test.dart +++ b/packages/flutter/test/widgets/error_widget_builder_test.dart @@ -4,11 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ErrorWidget.builder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ErrorWidget.builder', (WidgetTester tester) async { final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder; ErrorWidget.builder = (FlutterErrorDetails details) { return const Text('oopsie!', textDirection: TextDirection.ltr); @@ -27,7 +26,7 @@ void main() { ErrorWidget.builder = oldBuilder; }); - testWidgets('ErrorWidget.builder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ErrorWidget.builder', (WidgetTester tester) async { final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder; ErrorWidget.builder = (FlutterErrorDetails details) { return ErrorWidget(''); diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart index a10a77bd2498c..e12f2ecb76784 100644 --- a/packages/flutter/test/widgets/fade_in_image_test.dart +++ b/packages/flutter/test/widgets/fade_in_image_test.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; import '../painting/image_test_utils.dart'; @@ -58,7 +59,7 @@ class LoadTestImageProvider extends ImageProvider<Object> { } @override - ImageStreamCompleter load(Object key, DecoderCallback decode) { + ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) { throw UnimplementedError(); } } @@ -90,14 +91,26 @@ FadeInImageParts findFadeInImage(WidgetTester tester) { } } -Future<void> main() async { +void main() { // These must run outside test zone to complete - final ui.Image targetImage = await createTestImage(); - final ui.Image placeholderImage = await createTestImage(); - final ui.Image replacementImage = await createTestImage(); + late final ui.Image targetImage; + late final ui.Image placeholderImage; + late final ui.Image replacementImage; + + setUpAll(() async { + targetImage = await createTestImage(); + placeholderImage = await createTestImage(); + replacementImage = await createTestImage(); + }); + + tearDownAll(() { + targetImage.dispose(); + placeholderImage.dispose(); + replacementImage.dispose(); + }); group('FadeInImage', () { - testWidgets('animates an uncached image', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animates an uncached image', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -147,7 +160,7 @@ Future<void> main() async { expect(findFadeInImage(tester).target.opacity, 1); }); - testWidgets("FadeInImage's image obeys gapless playback", (WidgetTester tester) async { + testWidgetsWithLeakTracking("FadeInImage's image obeys gapless playback", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); final TestImageProvider secondImageProvider = TestImageProvider(replacementImage); @@ -188,7 +201,7 @@ Future<void> main() async { }); // Regression test for https://github.com/flutter/flutter/issues/111011 - testWidgets("FadeInImage's image obeys gapless playback when first image is cached but second isn't", + testWidgetsWithLeakTracking("FadeInImage's image obeys gapless playback when first image is cached but second isn't", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -225,7 +238,7 @@ Future<void> main() async { expect(parts.target.opacity, 1); }); - testWidgets("FadeInImage's placeholder obeys gapless playback", (WidgetTester tester) async { + testWidgetsWithLeakTracking("FadeInImage's placeholder obeys gapless playback", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -261,7 +274,7 @@ Future<void> main() async { expect(parts.placeholder!.opacity, 1); }); - testWidgets('shows a cached image immediately when skipFadeOnSynchronousLoad=true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shows a cached image immediately when skipFadeOnSynchronousLoad=true', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); imageProvider.resolve(ImageConfiguration.empty); @@ -277,7 +290,7 @@ Future<void> main() async { expect(findFadeInImage(tester).target.opacity, 1); }); - testWidgets('handles updating the placeholder image', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles updating the placeholder image', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -309,7 +322,7 @@ Future<void> main() async { expect(findFadeInImage(tester).state, same(state)); }); - testWidgets('does not keep the placeholder in the tree if it is invisible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not keep the placeholder in the tree if it is invisible', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -331,7 +344,7 @@ Future<void> main() async { expect(find.byType(Image), findsOneWidget); }); - testWidgets("doesn't interrupt in-progress animation when animation values are updated", (WidgetTester tester) async { + testWidgetsWithLeakTracking("doesn't interrupt in-progress animation when animation values are updated", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -434,7 +447,7 @@ Future<void> main() async { }); group('semantics', () { - testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async { + testWidgetsWithLeakTracking('only one Semantics node appears within FadeInImage', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -446,7 +459,7 @@ Future<void> main() async { expect(find.byType(Semantics), findsOneWidget); }); - testWidgets('is excluded if excludeFromSemantics is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is excluded if excludeFromSemantics is true', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -462,7 +475,7 @@ Future<void> main() async { group('label', () { const String imageSemanticText = 'Test image semantic label'; - testWidgets('defaults to image label if placeholder label is unspecified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('defaults to image label if placeholder label is unspecified', (WidgetTester tester) async { Semantics semanticsWidget() => tester.widget(find.byType(Semantics)); final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); @@ -489,7 +502,7 @@ Future<void> main() async { expect(semanticsWidget().properties.label, imageSemanticText); }); - testWidgets('is empty without any specified semantics labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is empty without any specified semantics labels', (WidgetTester tester) async { Semantics semanticsWidget() => tester.widget(find.byType(Semantics)); final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); @@ -515,7 +528,7 @@ Future<void> main() async { }); group("placeholder's BoxFit", () { - testWidgets("should be the image's BoxFit when not set", (WidgetTester tester) async { + testWidgetsWithLeakTracking("should be the image's BoxFit when not set", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -529,7 +542,7 @@ Future<void> main() async { expect(findFadeInImage(tester).placeholder!.fit, equals(BoxFit.cover)); }); - testWidgets('should be the given value when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should be the given value when set', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -546,7 +559,7 @@ Future<void> main() async { }); group("placeholder's FilterQuality", () { - testWidgets("should be the image's FilterQuality when not set", (WidgetTester tester) async { + testWidgetsWithLeakTracking("should be the image's FilterQuality when not set", (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); @@ -560,7 +573,7 @@ Future<void> main() async { expect(findFadeInImage(tester).placeholder!.filterQuality, equals(FilterQuality.medium)); }); - testWidgets('should be the given value when set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should be the given value when set', (WidgetTester tester) async { final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider imageProvider = TestImageProvider(targetImage); diff --git a/packages/flutter/test/widgets/fade_transition_test.dart b/packages/flutter/test/widgets/fade_transition_test.dart index edcfce8630e62..e35013867e8e5 100644 --- a/packages/flutter/test/widgets/fade_transition_test.dart +++ b/packages/flutter/test/widgets/fade_transition_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('FadeTransition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FadeTransition', (WidgetTester tester) async { final DebugPrintCallback oldPrint = debugPrint; final List<String> log = <String>[]; debugPrint = (String? message, { int? wrapWidth }) { diff --git a/packages/flutter/test/widgets/fast_reassemble_test.dart b/packages/flutter/test/widgets/fast_reassemble_test.dart deleted file mode 100644 index 1269992c113ce..0000000000000 --- a/packages/flutter/test/widgets/fast_reassemble_test.dart +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('reassemble with a className only marks subtrees from the first matching element as dirty', (WidgetTester tester) async { - await tester.pumpWidget( - const Foo(Bar(Fizz(SizedBox()))) - ); - - expect(Foo.count, 0); - expect(Bar.count, 0); - expect(Fizz.count, 0); - - DebugReassembleConfig config = DebugReassembleConfig(widgetName: 'Bar'); - WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config); - - expect(Foo.count, 0); - expect(Bar.count, 1); - expect(Fizz.count, 1); - - config = DebugReassembleConfig(widgetName: 'Fizz'); - WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config); - - expect(Foo.count, 0); - expect(Bar.count, 1); - expect(Fizz.count, 2); - - config = DebugReassembleConfig(widgetName: 'NoMatch'); - WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config); - - expect(Foo.count, 0); - expect(Bar.count, 1); - expect(Fizz.count, 2); - - config = DebugReassembleConfig(); - WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config); - - expect(Foo.count, 1); - expect(Bar.count, 2); - expect(Fizz.count, 3); - - WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, null); - - expect(Foo.count, 2); - expect(Bar.count, 3); - expect(Fizz.count, 4); - }); -} - -class Foo extends StatefulWidget { - const Foo(this.child, {super.key}); - - final Widget child; - static int count = 0; - - @override - State<Foo> createState() => _FooState(); -} - -class _FooState extends State<Foo> { - @override - void reassemble() { - Foo.count += 1; - super.reassemble(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - - -class Bar extends StatefulWidget { - const Bar(this.child, {super.key}); - - final Widget child; - static int count = 0; - - @override - State<Bar> createState() => _BarState(); -} - -class _BarState extends State<Bar> { - @override - void reassemble() { - Bar.count += 1; - super.reassemble(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class Fizz extends StatefulWidget { - const Fizz(this.child, {super.key}); - - final Widget child; - static int count = 0; - - @override - State<Fizz> createState() => _FizzState(); -} - -class _FizzState extends State<Fizz> { - @override - void reassemble() { - Fizz.count += 1; - super.reassemble(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/packages/flutter/test/widgets/fitted_box_test.dart b/packages/flutter/test/widgets/fitted_box_test.dart index bc5d30cb42fc1..bcc8aee897c5f 100644 --- a/packages/flutter/test/widgets/fitted_box_test.dart +++ b/packages/flutter/test/widgets/fitted_box_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can size according to aspect ratio', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can size according to aspect ratio', (WidgetTester tester) async { final Key outside = UniqueKey(); final Key inside = UniqueKey(); @@ -42,7 +43,7 @@ void main() { expect(insidePoint, equals(outsidePoint)); }); - testWidgets('Can contain child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can contain child', (WidgetTester tester) async { final Key outside = UniqueKey(); final Key inside = UniqueKey(); @@ -77,7 +78,7 @@ void main() { expect(insidePoint, equals(outsidePoint)); }); - testWidgets('Child can cover', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Child can cover', (WidgetTester tester) async { final Key outside = UniqueKey(); final Key inside = UniqueKey(); @@ -113,7 +114,7 @@ void main() { expect(insidePoint, equals(outsidePoint)); }); - testWidgets('FittedBox with no child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox with no child', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Center( @@ -129,7 +130,7 @@ void main() { expect(box.size.height, 0.0); }); - testWidgets('Child can be aligned multiple ways in a row', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Child can be aligned multiple ways in a row', (WidgetTester tester) async { final Key outside = UniqueKey(); final Key inside = UniqueKey(); @@ -339,7 +340,7 @@ void main() { } }); - testWidgets('FittedBox layers - contain', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox layers - contain', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox( @@ -360,7 +361,7 @@ void main() { expect(getLayers(), <Type>[TransformLayer, TransformLayer, OffsetLayer]); }); - testWidgets('FittedBox layers - cover - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox layers - cover - horizontal', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox( @@ -383,7 +384,7 @@ void main() { expect(getLayers(), <Type>[TransformLayer, ClipRectLayer, TransformLayer, OffsetLayer]); }); - testWidgets('FittedBox layers - cover - vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox layers - cover - vertical', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox( @@ -406,7 +407,7 @@ void main() { expect(getLayers(), <Type>[TransformLayer, ClipRectLayer, TransformLayer, OffsetLayer]); }); - testWidgets('FittedBox layers - none - clip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox layers - none - clip', (WidgetTester tester) async { final List<double> values = <double>[10.0, 50.0, 100.0]; for (final double a in values) { for (final double b in values) { @@ -442,7 +443,7 @@ void main() { } }); - testWidgets('Big child into small fitted box - hit testing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Big child into small fitted box - hit testing', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); bool pointerDown = false; await tester.pumpWidget( @@ -474,7 +475,7 @@ void main() { expect(pointerDown, isTrue); }); - testWidgets('Can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(FittedBox(fit: BoxFit.none, child: Container())); final RenderFittedBox renderObject = tester.allRenderObjects.whereType<RenderFittedBox>().first; expect(renderObject.clipBehavior, equals(Clip.none)); @@ -483,7 +484,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('BoxFit.scaleDown matches size of child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxFit.scaleDown matches size of child', (WidgetTester tester) async { final Key outside = UniqueKey(); final Key inside = UniqueKey(); @@ -544,7 +545,7 @@ void main() { expect(insidePoint - outsidePoint, equals(Offset.zero)); }); - testWidgets('Switching to and from BoxFit.scaleDown causes relayout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Switching to and from BoxFit.scaleDown causes relayout', (WidgetTester tester) async { final Key outside = UniqueKey(); final Widget scaleDownWidget = Center( @@ -588,7 +589,7 @@ void main() { expect(outsideBox.size.height, 50.0); }); - testWidgets('FittedBox without child does not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FittedBox without child does not throw', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox( diff --git a/packages/flutter/test/widgets/flex_test.dart b/packages/flutter/test/widgets/flex_test.dart index 1a29b997e29aa..ea48423f574f8 100644 --- a/packages/flutter/test/widgets/flex_test.dart +++ b/packages/flutter/test/widgets/flex_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can hit test flex children of stacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hit test flex children of stacks', (WidgetTester tester) async { bool didReceiveTap = false; await tester.pumpWidget( Directionality( @@ -47,7 +48,7 @@ void main() { expect(didReceiveTap, isTrue); }); - testWidgets('Flexible defaults to loose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flexible defaults to loose', (WidgetTester tester) async { await tester.pumpWidget( const Row( textDirection: TextDirection.ltr, @@ -61,7 +62,7 @@ void main() { expect(box.size.width, 100.0); }); - testWidgets("Doesn't overflow because of floating point accumulated error", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Doesn't overflow because of floating point accumulated error", (WidgetTester tester) async { // both of these cases have failed in the past due to floating point issues await tester.pumpWidget( const Center( @@ -99,7 +100,7 @@ void main() { ); }); - testWidgets('Error information is printed correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Error information is printed correctly', (WidgetTester tester) async { // We run this twice, the first time without an error, so that the second time // we only get a single exception. Otherwise we'd get two, the one we want and // an extra one when we discover we never computed a size. @@ -133,7 +134,7 @@ void main() { expect(message, contains('\nSee also:')); }); - testWidgets('Can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(const Flex(direction: Axis.vertical)); final RenderFlex renderObject = tester.allRenderObjects.whereType<RenderFlex>().first; expect(renderObject.clipBehavior, equals(Clip.none)); diff --git a/packages/flutter/test/widgets/flow_test.dart b/packages/flutter/test/widgets/flow_test.dart index 0746896d9f31d..2a517963e20af 100644 --- a/packages/flutter/test/widgets/flow_test.dart +++ b/packages/flutter/test/widgets/flow_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestFlowDelegate extends FlowDelegate { TestFlowDelegate({required this.startOffset}) : super(repaint: startOffset); @@ -61,7 +62,7 @@ class DuplicatePainterOpacityFlowDelegate extends OpacityFlowDelegate { } void main() { - testWidgets('Flow control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flow control test', (WidgetTester tester) async { final AnimationController startOffset = AnimationController.unbounded( vsync: tester, ); @@ -115,7 +116,7 @@ void main() { expect(log, equals(<int>[0])); }); - testWidgets('paintChild gets called twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('paintChild gets called twice', (WidgetTester tester) async { await tester.pumpWidget( Flow( delegate: DuplicatePainterOpacityFlowDelegate(1.0), @@ -137,7 +138,7 @@ void main() { )); }); - testWidgets('Flow opacity layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flow opacity layer', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow( @@ -157,7 +158,7 @@ void main() { expect(layer!.firstChild, isA<TransformLayer>()); }); - testWidgets('Flow can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flow can set and update clipBehavior', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow( @@ -186,7 +187,7 @@ void main() { } }); - testWidgets('Flow.unwrapped can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flow.unwrapped can set and update clipBehavior', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow.unwrapped( diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart index 1589bac4e9d2c..8e3cfb9974bbb 100644 --- a/packages/flutter/test/widgets/focus_manager_test.dart +++ b/packages/flutter/test/widgets/focus_manager_test.dart @@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final GlobalKey widgetKey = GlobalKey(); @@ -20,13 +21,16 @@ void main() { } group(FocusNode, () { - testWidgets('Can add children.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can add children.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusNode parent = FocusNode(); + addTearDown(parent.dispose); final FocusAttachment parentAttachment = parent.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); parentAttachment.reparent(parent: tester.binding.focusManager.rootScope); child1Attachment.reparent(parent: parent); @@ -40,13 +44,16 @@ void main() { expect(parent.children.last, equals(child2)); }); - testWidgets('Can remove children.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can remove children.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusNode parent = FocusNode(); + addTearDown(parent.dispose); final FocusAttachment parentAttachment = parent.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); parentAttachment.reparent(parent: tester.binding.focusManager.rootScope); child1Attachment.reparent(parent: parent); @@ -66,9 +73,12 @@ void main() { expect(parent.children, isEmpty); }); - testWidgets('Geometry is transformed properly.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Geometry is transformed properly.', (WidgetTester tester) async { final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1'); + addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2'); + addTearDown(focusNode2.dispose); + await tester.pumpWidget( Padding( padding: const EdgeInsets.all(8.0), @@ -103,17 +113,22 @@ void main() { expect(focusNode2.offset, equals(const Offset(443.0, 194.5))); }); - testWidgets('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('descendantsAreFocusable disables focus for descendants.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: scope); @@ -151,17 +166,22 @@ void main() { expect(scope.traversalDescendants.contains(child2), isFalse); }); - testWidgets('descendantsAreTraversable disables traversal for descendants.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('descendantsAreTraversable disables traversal for descendants.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -184,17 +204,22 @@ void main() { expect(scope.traversalDescendants, equals(<FocusNode>[])); }); - testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async { + testWidgetsWithLeakTracking("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: scope); @@ -215,11 +240,11 @@ void main() { expect(scope.traversalChildren.contains(parent2), isFalse); }); - testWidgets('implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - FocusNode( - debugLabel: 'Label', - ).debugFillProperties(builder); + final FocusNode focusNode = FocusNode(debugLabel: 'Label'); + addTearDown(focusNode.dispose); + focusNode.debugFillProperties(builder); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); expect(description, <String>[ 'context: null', @@ -231,8 +256,13 @@ void main() { ]); }); - testWidgets('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(debugLabel: 'Test Node 3'); + testWidgetsWithLeakTracking('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1'); + addTearDown(focusNode1.dispose); + final FocusNode focusNode2 = FocusNode(debugLabel: 'Test Node 2'); + addTearDown(focusNode2.dispose); + final FocusNode focusNode3 = FocusNode(debugLabel: 'Test Node 3'); + addTearDown(focusNode3.dispose); List<List<KeyEventResult>> results = <List<KeyEventResult>>[ <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored], <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored], @@ -242,7 +272,7 @@ void main() { await tester.pumpWidget( Focus( - focusNode: FocusNode(debugLabel: 'Test Node 1'), + focusNode: focusNode1, onKeyEvent: (_, KeyEvent event) { logs.add(0); return results[0][0]; @@ -252,7 +282,7 @@ void main() { return results[0][1]; }, child: Focus( - focusNode: FocusNode(debugLabel: 'Test Node 2'), + focusNode: focusNode2, onKeyEvent: (_, KeyEvent event) { logs.add(10); return results[1][0]; @@ -262,7 +292,7 @@ void main() { return results[1][1]; }, child: Focus( - focusNode: focusNode, + focusNode: focusNode3, onKeyEvent: (_, KeyEvent event) { logs.add(20); return results[2][0]; @@ -276,7 +306,7 @@ void main() { ), ), ); - focusNode.requestFocus(); + focusNode3.requestFocus(); await tester.pump(); // All ignored. @@ -327,15 +357,19 @@ void main() { group(FocusScopeNode, () { - testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); scope.attach(context); final FocusScopeNode parent = FocusScopeNode(debugLabel: 'Parent'); + addTearDown(parent.dispose); parent.attach(context); final FocusScopeNode child1 = FocusScopeNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusScopeNode child2 = FocusScopeNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); child2.attach(context); scope.setFirstFocus(parent); parent.setFirstFocus(child1); @@ -352,15 +386,19 @@ void main() { expect(scope.focusedChild, equals(parent)); }); - testWidgets('Removing a node removes it from scope.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Removing a node removes it from scope.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent = FocusNode(); + addTearDown(parent.dispose); final FocusAttachment parentAttachment = parent.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parentAttachment.reparent(parent: scope); @@ -377,15 +415,19 @@ void main() { expect(scope.focusedChild, isNull); }); - testWidgets('Can add children to scope and focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can add children to scope and focus', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent = FocusNode(); + addTearDown(parent.dispose); final FocusAttachment parentAttachment = parent.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parentAttachment.reparent(parent: scope); @@ -417,11 +459,13 @@ void main() { expect(child2.hasPrimaryFocus, isTrue); }); - testWidgets('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Requesting focus before adding to tree results in a request after adding', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode child = FocusNode(); + addTearDown(child.dispose); child.requestFocus(); expect(child.hasPrimaryFocus, isFalse); // not attached yet. @@ -437,15 +481,19 @@ void main() { expect(child.hasPrimaryFocus, isTrue); // now attached and parented, so focus finally happened. }); - testWidgets('Autofocus works.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Autofocus works.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent = FocusNode(debugLabel: 'Parent'); + addTearDown(parent.dispose); final FocusAttachment parentAttachment = parent.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parentAttachment.reparent(parent: scope); @@ -474,15 +522,19 @@ void main() { expect(child2.hasPrimaryFocus, isFalse); }); - testWidgets('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adding a focusedChild to a scope sets scope as focusedChild in parent scope', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: scope1); @@ -506,17 +558,22 @@ void main() { expect(child2.hasPrimaryFocus, isFalse); }); - testWidgets('Can move node with focus without losing focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can move node with focus without losing focus', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: scope); @@ -543,17 +600,22 @@ void main() { expect(parent2.children.first, equals(child1)); }); - testWidgets('canRequestFocus affects children.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('canRequestFocus affects children.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: scope); @@ -583,17 +645,22 @@ void main() { expect(parent1.traversalChildren.contains(child2), isFalse); }); - testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("skipTraversal doesn't affect children.", (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope'); + addTearDown(scope.dispose); final FocusAttachment scopeAttachment = scope.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: scope); @@ -618,23 +685,31 @@ void main() { expect(scope.traversalDescendants.contains(child2), isTrue); }); - testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can move node between scopes and lose scope focus', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -656,23 +731,31 @@ void main() { expect(parent2.children.contains(child1), isTrue); }); - testWidgets('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ancestors and descendants are computed and recomputed properly', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -692,23 +775,31 @@ void main() { expect(tester.binding.focusManager.rootScope.descendants, equals(<FocusNode>[child1, child3, child4, parent2, scope2, child2, parent1, scope1])); }); - testWidgets('Can move focus between scopes and keep focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can move focus between scopes and keep focus', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -750,23 +841,31 @@ void main() { expect(scope2.focusedChild, equals(child4)); }); - testWidgets('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unfocus with disposition previouslyFocusedChild works properly', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -831,23 +930,31 @@ void main() { expect(child3.hasPrimaryFocus, isTrue); }); - testWidgets('Unfocus with disposition scope works properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unfocus with disposition scope works properly', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -916,23 +1023,31 @@ void main() { expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue); }); - testWidgets('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unfocus works properly when some nodes are unfocusable', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -982,23 +1097,31 @@ void main() { expect(child2.hasPrimaryFocus, isFalse); }); - testWidgets('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Requesting focus on a scope works properly when some focusedChild nodes are unfocusable', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'child3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'child4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -1036,7 +1159,7 @@ void main() { expect(child4.hasPrimaryFocus, isTrue); }); - testWidgets('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Key handling bubbles up and terminates when handled.', (WidgetTester tester) async { final Set<FocusNode> receivedAnEvent = <FocusNode>{}; final Set<FocusNode> shouldHandle = <FocusNode>{}; KeyEventResult handleEvent(FocusNode node, RawKeyEvent event) { @@ -1054,20 +1177,28 @@ void main() { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1'); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context, onKey: handleEvent); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'Scope 2'); + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context, onKey: handleEvent); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1', onKey: handleEvent); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2', onKey: handleEvent); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context, onKey: handleEvent); final FocusNode child2 = FocusNode(debugLabel: 'Child 2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context, onKey: handleEvent); final FocusNode child3 = FocusNode(debugLabel: 'Child 3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context, onKey: handleEvent); final FocusNode child4 = FocusNode(debugLabel: 'Child 4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context, onKey: handleEvent); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -1100,7 +1231,7 @@ void main() { expect(receivedAnEvent, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Initial highlight mode guesses correctly.', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic; switch (defaultTargetPlatform) { case TargetPlatform.fuchsia: @@ -1114,7 +1245,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async { expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch)); RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); @@ -1122,7 +1253,7 @@ void main() { expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); }, variant: TargetPlatformVariant.mobile()); - testWidgets('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async { expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); RendererBinding.instance.initMouseTracker(); // Clear out the mouse state. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0); @@ -1130,12 +1261,12 @@ void main() { expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async { await tester.sendKeyEvent(LogicalKeyboardKey.enter); expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional)); }, variant: TargetPlatformVariant.all()); - testWidgets('Events change focus highlight mode.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Events change focus highlight mode.', (WidgetTester tester) async { await setupWidget(tester); int callCount = 0; FocusHighlightMode? lastMode; @@ -1176,11 +1307,11 @@ void main() { expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch)); }); - testWidgets('implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - FocusScopeNode( - debugLabel: 'Scope Label', - ).debugFillProperties(builder); + final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope Label'); + addTearDown(scope.dispose); + scope.debugFillProperties(builder); final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList(); expect(description, <String>[ 'context: null', @@ -1192,23 +1323,31 @@ void main() { ]); }); - testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugDescribeFocusTree produces correct output', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'Scope 1'); + addTearDown(scope1.dispose); final FocusAttachment scope1Attachment = scope1.attach(context); final FocusScopeNode scope2 = FocusScopeNode(); // No label, Just to test that it works. + addTearDown(scope2.dispose); final FocusAttachment scope2Attachment = scope2.attach(context); final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'Child 1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(); // No label, Just to test that it works. + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); final FocusNode child3 = FocusNode(debugLabel: 'Child 3'); + addTearDown(child3.dispose); final FocusAttachment child3Attachment = child3.attach(context); final FocusNode child4 = FocusNode(debugLabel: 'Child 4'); + addTearDown(child4.dispose); final FocusAttachment child4Attachment = child4.attach(context); scope1Attachment.reparent(parent: tester.binding.focusManager.rootScope); scope2Attachment.reparent(parent: tester.binding.focusManager.rootScope); @@ -1227,8 +1366,9 @@ void main() { 'FocusManager#00000\n' ' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n' ' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← [root]\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ [root]\n' ' │\n' ' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n' ' │ IN FOCUS PATH\n' @@ -1267,11 +1407,13 @@ void main() { }); group('Autofocus', () { - testWidgets( + testWidgetsWithLeakTracking( 'works when the previous focused node is detached', (WidgetTester tester) async { final FocusNode node1 = FocusNode(); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(); + addTearDown(node2.dispose); await tester.pumpWidget( FocusScope( @@ -1292,11 +1434,13 @@ void main() { expect(node2.hasPrimaryFocus, isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'node detached before autofocus is applied', (WidgetTester tester) async { final FocusScopeNode scopeNode = FocusScopeNode(); + addTearDown(scopeNode.dispose); final FocusNode node1 = FocusNode(); + addTearDown(node1.dispose); await tester.pumpWidget( FocusScope( @@ -1320,10 +1464,13 @@ void main() { expect(scopeNode.hasPrimaryFocus, isTrue); }); - testWidgets('autofocus the first candidate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('autofocus the first candidate', (WidgetTester tester) async { final FocusNode node1 = FocusNode(); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(); + addTearDown(node2.dispose); final FocusNode node3 = FocusNode(); + addTearDown(node3.dispose); await tester.pumpWidget( Directionality( @@ -1353,10 +1500,13 @@ void main() { expect(node1.hasPrimaryFocus, isTrue); }); - testWidgets('Autofocus works with global key reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Autofocus works with global key reparenting', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); + addTearDown(scope1.dispose); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -1407,15 +1557,19 @@ void main() { }); }); - testWidgets("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Doesn't lose focused child when reparenting if the nearestScope doesn't change.", (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); child1Attachment.reparent(parent: parent1); @@ -1433,7 +1587,7 @@ void main() { expect(parent1.focusedChild, equals(child2)); }); - testWidgets('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ancestors get notified exactly as often as needed if focused child changes focus.', (WidgetTester tester) async { bool topFocus = false; bool parent1Focus = false; bool parent2Focus = false; @@ -1458,14 +1612,19 @@ void main() { } final BuildContext context = await setupWidget(tester); final FocusScopeNode top = FocusScopeNode(debugLabel: 'top'); + addTearDown(top.dispose); final FocusAttachment topAttachment = top.attach(context); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusScopeNode parent2 = FocusScopeNode(debugLabel: 'parent2'); + addTearDown(parent2.dispose); final FocusAttachment parent2Attachment = parent2.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); topAttachment.reparent(parent: tester.binding.focusManager.rootScope); parent1Attachment.reparent(parent: top); @@ -1564,13 +1723,16 @@ void main() { expect(child2Notify, equals(0)); }); - testWidgets('Focus changes notify listeners.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus changes notify listeners.', (WidgetTester tester) async { final BuildContext context = await setupWidget(tester); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); final FocusNode child2 = FocusNode(debugLabel: 'child2'); + addTearDown(child2.dispose); final FocusAttachment child2Attachment = child2.attach(context); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); child1Attachment.reparent(parent: parent1); @@ -1608,9 +1770,26 @@ void main() { tester.binding.focusManager.removeListener(handleFocusChange); }); - testWidgets('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async { + test('$FocusManager dispatches object creation in constructor', () async { + await expectLater( + await memoryEvents(() => FocusManager().dispose(), FocusManager), + areCreateAndDispose, + ); + }); + + test('$FocusNode dispatches object creation in constructor', () async { + await expectLater( + await memoryEvents(() => FocusNode().dispose(), FocusNode), + areCreateAndDispose, + ); + }); + + testWidgetsWithLeakTracking('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async { final FocusNode nodeA = FocusNode(debugLabel: 'a'); + addTearDown(nodeA.dispose); final FocusNode nodeB = FocusNode(debugLabel: 'b'); + addTearDown(nodeB.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -1654,7 +1833,7 @@ void main() { tester.binding.focusManager.removeListener(handleFocusChange); }); - testWidgets('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugFocusChanges causes logging of focus changes', (WidgetTester tester) async { final bool oldDebugFocusChanges = debugFocusChanges; final DebugPrintCallback oldDebugPrint = debugPrint; final StringBuffer messages = StringBuffer(); @@ -1665,8 +1844,10 @@ void main() { try { final BuildContext context = await setupWidget(tester); final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1'); + addTearDown(parent1.dispose); final FocusAttachment parent1Attachment = parent1.attach(context); final FocusNode child1 = FocusNode(debugLabel: 'child1'); + addTearDown(child1.dispose); final FocusAttachment child1Attachment = child1.attach(context); parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope); child1Attachment.reparent(parent: parent1); @@ -1699,7 +1880,7 @@ void main() { expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1'))); }); - testWidgets("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async { + testWidgetsWithLeakTracking("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async { final bool oldDebugFocusChanges = debugFocusChanges; final DebugPrintCallback oldDebugPrint = debugPrint; final StringBuffer messages = StringBuffer(); diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart index b11590fe8fb39..2f09b957badae 100644 --- a/packages/flutter/test/widgets/focus_scope_test.dart +++ b/packages/flutter/test/widgets/focus_scope_test.dart @@ -6,12 +6,13 @@ import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group('FocusScope', () { - testWidgets('Can focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( @@ -27,7 +28,7 @@ void main() { expect(find.text('A FOCUSED'), findsOneWidget); }); - testWidgets('Can unfocus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can unfocus', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( @@ -62,7 +63,7 @@ void main() { expect(find.text('B FOCUSED'), findsOneWidget); }); - testWidgets('Autofocus works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Autofocus works', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); await tester.pumpWidget( @@ -82,7 +83,7 @@ void main() { expect(find.text('B FOCUSED'), findsOneWidget); }); - testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); @@ -129,9 +130,11 @@ void main() { // This moves a focus node first into a focus scope that is added to its // parent, and then out of that focus scope again. - testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can move focus in and out of FocusScope', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); + addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); + addTearDown(childFocusScope.dispose); final GlobalKey<TestFocusState> key = GlobalKey(); // Initially create the focus inside of the parent FocusScope. @@ -274,10 +277,13 @@ void main() { childAttachment.detach(); }); - testWidgets('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async { final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); + addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1'); + addTearDown(childFocusScope1.dispose); final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2'); + addTearDown(childFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(debugLabel: 'Key A'); final GlobalKey<TestFocusState> keyB = GlobalKey(debugLabel: 'Key B'); final GlobalKey<TestFocusState> keyC = GlobalKey(debugLabel: 'Key C'); @@ -376,7 +382,7 @@ void main() { expect(childFocusScope2.isFirstFocus, isFalse); }); - testWidgets('Removing focused widget moves focus to next widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Removing focused widget moves focus to next widget', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); @@ -420,10 +426,12 @@ void main() { expect(find.text('b'), findsOneWidget); }); - testWidgets('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node'); + addTearDown(parentFocusScope.dispose); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node'); + addTearDown(childFocusScope.dispose); await tester.pumpWidget( FocusScope( @@ -466,11 +474,15 @@ void main() { expect(find.text('A FOCUSED'), findsOneWidget); }); - testWidgets('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async { final FocusNode topNode = FocusNode(debugLabel: 'Top'); + addTearDown(topNode.dispose); final FocusNode parentNode = FocusNode(debugLabel: 'Parent'); + addTearDown(parentNode.dispose); final FocusNode childNode = FocusNode(debugLabel: 'Child'); + addTearDown(childNode.dispose); final FocusNode insertedNode = FocusNode(debugLabel: 'Inserted'); + addTearDown(insertedNode.dispose); await tester.pumpWidget( FocusScope( @@ -532,11 +544,15 @@ void main() { expect(insertedNode.hasFocus, isFalse); }); - testWidgets('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async { final FocusScopeNode topNode = FocusScopeNode(debugLabel: 'Top'); + addTearDown(topNode.dispose); final FocusScopeNode parentNode = FocusScopeNode(debugLabel: 'Parent'); + addTearDown(parentNode.dispose); final FocusScopeNode childNode = FocusScopeNode(debugLabel: 'Child'); + addTearDown(childNode.dispose); final FocusScopeNode insertedNode = FocusScopeNode(debugLabel: 'Inserted'); + addTearDown(insertedNode.dispose); await tester.pumpWidget( FocusScope.withExternalFocusNode( @@ -599,10 +615,11 @@ void main() { }); // Arguably, this isn't correct behavior, but it is what happens now. - testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); + addTearDown(parentFocusScope.dispose); await tester.pumpWidget( FocusScope( @@ -656,12 +673,13 @@ void main() { expect(find.text('b'), findsOneWidget); }); - testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope'); + addTearDown(parentFocusScope.dispose); // This checks both FocusScopes that have their own nodes, as well as those // that use external nodes. @@ -719,13 +737,15 @@ void main() { }); // By "pinned", it means kept in the tree by a GlobalKey. - testWidgets("Removing pinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Removing pinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); + addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); + addTearDown(parentFocusScope2.dispose); await tester.pumpWidget( FocusTraversalGroup( @@ -805,11 +825,13 @@ void main() { expect(find.text('B FOCUSED'), findsOneWidget); }); - testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1'); + addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); + addTearDown(parentFocusScope2.dispose); await tester.pumpWidget( FocusTraversalGroup( @@ -885,9 +907,11 @@ void main() { expect(find.text('B FOCUSED'), findsOneWidget); }); - testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving widget from one scope to another retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(); + addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(); + addTearDown(parentFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); @@ -966,9 +990,11 @@ void main() { expect(find.text('b'), findsOneWidget); }); - testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving FocusScopeNodes retains focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1'); + addTearDown(parentFocusScope1.dispose); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2'); + addTearDown(parentFocusScope2.dispose); final GlobalKey<TestFocusState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey(); @@ -1052,7 +1078,7 @@ void main() { expect(find.text('b'), findsOneWidget); }); - testWidgets('Can focus root node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can focus root node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); await tester.pumpWidget( Focus( @@ -1071,8 +1097,9 @@ void main() { expect(rootNode, equals(firstElement.owner!.focusManager.rootScope)); }); - testWidgets('Can autofocus a node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can autofocus a node.', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); await tester.pumpWidget( Focus( focusNode: focusNode, @@ -1095,9 +1122,11 @@ void main() { expect(focusNode.hasPrimaryFocus, isTrue); }); - testWidgets("Won't autofocus a node if one is already focused.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Won't autofocus a node if one is already focused.", (WidgetTester tester) async { final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A'); + addTearDown(focusNodeA.dispose); final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B'); + addTearDown(focusNodeB.dispose); await tester.pumpWidget( Column( children: <Widget>[ @@ -1134,9 +1163,10 @@ void main() { expect(focusNodeA.hasPrimaryFocus, isTrue); }); - testWidgets("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { + testWidgetsWithLeakTracking("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusScopeNode focusScopeNode = FocusScopeNode(); + addTearDown(focusScopeNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; @@ -1205,7 +1235,7 @@ void main() { }); group('Focus', () { - testWidgets('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -1213,6 +1243,7 @@ void main() { final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key6 = GlobalKey(debugLabel: '6'); final FocusScopeNode scopeNode = FocusScopeNode(); + addTearDown(scopeNode.dispose); await tester.pumpWidget( FocusScope( key: key1, @@ -1252,7 +1283,7 @@ void main() { expect(Focus.of(element5).parent!.parent, equals(root)); expect(Focus.of(element6).parent!.parent!.parent, equals(root)); }); - testWidgets('Can traverse Focus children.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can traverse Focus children.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -1326,7 +1357,7 @@ void main() { expect(keys, equals(<Key>[key7, key8])); }); - testWidgets('Can set focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); late bool gotFocus; await tester.pumpWidget( @@ -1346,7 +1377,7 @@ void main() { expect(node.hasFocus, isTrue); }); - testWidgets('Focus is ignored when set to not focusable.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus is ignored when set to not focusable.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); bool? gotFocus; await tester.pumpWidget( @@ -1367,7 +1398,7 @@ void main() { expect(node.hasFocus, isFalse); }); - testWidgets('Focus is lost when set to not focusable.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus is lost when set to not focusable.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); bool? gotFocus; await tester.pumpWidget( @@ -1407,10 +1438,11 @@ void main() { expect(node.hasFocus, isFalse); }); - testWidgets('Child of unfocusable Focus can get focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Child of unfocusable Focus can get focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( Focus( @@ -1439,7 +1471,7 @@ void main() { expect(unfocusableNode.hasFocus, isTrue); }); - testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); late bool gotFocus; await tester.pumpWidget( @@ -1465,7 +1497,7 @@ void main() { expect(FocusManager.instance.rootScope.descendants, isEmpty); }); - testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus widgets set Semantics information about focus', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); await tester.pumpWidget( @@ -1494,7 +1526,7 @@ void main() { expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse); }); - testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async { final GlobalKey<TestFocusState> key = GlobalKey(); final TestFocus testFocus = TestFocus(key: key); @@ -1511,7 +1543,7 @@ void main() { expect(key.currentState!.focusNode.canRequestFocus, isFalse); }); - testWidgets('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async { final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1'); final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2'); final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1'); @@ -1620,11 +1652,15 @@ void main() { expect(Focus.of(container1.currentContext!).hasFocus, isTrue); }); - testWidgets('skipTraversal works as expected.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('skipTraversal works as expected.', (WidgetTester tester) async { final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1'); + addTearDown(scope1.dispose); final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2'); + addTearDown(scope2.dispose); final FocusNode focus1 = FocusNode(debugLabel: 'focus1'); + addTearDown(focus1.dispose); final FocusNode focus2 = FocusNode(debugLabel: 'focus2'); + addTearDown(focus2.dispose); Future<void> pumpTest({ bool traverseScope1 = false, @@ -1674,10 +1710,11 @@ void main() { expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2])); }); - testWidgets('descendantsAreFocusable works as expected.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('descendantsAreFocusable works as expected.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( Focus( @@ -1713,11 +1750,15 @@ void main() { expect(unfocusableNode.hasFocus, isFalse); }); - testWidgets('descendantsAreTraversable works as expected.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('descendantsAreTraversable works as expected.', (WidgetTester tester) async { final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope'); + addTearDown(scopeNode.dispose); final FocusNode node1 = FocusNode(debugLabel: 'node 1'); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(debugLabel: 'node 2'); + addTearDown(node2.dispose); final FocusNode node3 = FocusNode(debugLabel: 'node 3'); + addTearDown(node3.dispose); await tester.pumpWidget( FocusScope( @@ -1746,7 +1787,7 @@ void main() { expect(node2.traversalDescendants, equals(<FocusNode>[])); }); - testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Focus(includeSemantics: false, child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); @@ -1754,9 +1795,10 @@ void main() { semantics.dispose(); }); - testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; @@ -1803,9 +1845,10 @@ void main() { expect(keyEventHandled, isTrue); }); - testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) { keyEventHandled = true; @@ -1852,9 +1895,10 @@ void main() { expect(keyEventHandled, isTrue); }); - testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? keyEventHandled; KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) { keyEventHandled = true; @@ -1921,7 +1965,7 @@ void main() { expect(keyEventHandled, isTrue); }); - testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async { await tester.pumpWidget( Focus( child: Container(), @@ -1931,10 +1975,11 @@ void main() { }); group('ExcludeFocus', () { - testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? gotFocus; await tester.pumpWidget( ExcludeFocus( @@ -1970,10 +2015,13 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/61700 - testWidgets("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async { final FocusNode parentFocusNode = FocusNode(debugLabel: 'group'); + addTearDown(parentFocusNode.dispose); final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1'); + addTearDown(focusNode1.dispose); final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2'); + addTearDown(focusNode2.dispose); await tester.pumpWidget( ExcludeFocus( excluding: false, @@ -2039,7 +2087,7 @@ void main() { expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue); }); - testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(ExcludeFocus(child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); @@ -2048,8 +2096,9 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/92693 - testWidgets('Setting parent FocusScope.canRequestFocus to false, does not set descendant Focus._internalNode._canRequestFocus to false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting parent FocusScope.canRequestFocus to false, does not set descendant Focus._internalNode._canRequestFocus to false', (WidgetTester tester) async { final FocusNode childFocusNode = FocusNode(debugLabel: 'node 1'); + addTearDown(childFocusNode.dispose); Widget buildFocusTree({required bool parentCanRequestFocus}) { return FocusScope( diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart index e03f77628bf9b..bf447bb661ad0 100644 --- a/packages/flutter/test/widgets/focus_traversal_test.dart +++ b/packages/flutter/test/widgets/focus_traversal_test.dart @@ -8,12 +8,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group(WidgetOrderTraversalPolicy, () { - testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -52,7 +53,7 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Find the initial focus if there is none yet and traversing backwards.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus if there is none yet and traversing backwards.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -95,7 +96,114 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Move focus to next node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('focus traversal should work case 1', (WidgetTester tester) async { + final FocusNode outer1 = FocusNode(debugLabel: 'outer1', skipTraversal: true); + final FocusNode outer2 = FocusNode(debugLabel: 'outer2', skipTraversal: true); + final FocusNode inner1 = FocusNode(debugLabel: 'inner1', ); + final FocusNode inner2 = FocusNode(debugLabel: 'inner2', ); + addTearDown(() { + outer1.dispose(); + outer2.dispose(); + inner1.dispose(); + inner2.dispose(); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: FocusTraversalGroup( + child: Row( + children: <Widget>[ + FocusScope( + child: Focus( + focusNode: outer1, + child: Focus( + focusNode: inner1, + child: const SizedBox(width: 10, height: 10), + ), + ), + ), + FocusScope( + child: Focus( + focusNode: outer2, + // Add a padding to ensure both Focus widgets have different + // sizes. + child: Padding( + padding: const EdgeInsets.all(5), + child: Focus( + focusNode: inner2, + child: const SizedBox(width: 10, height: 10), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + expect(FocusManager.instance.primaryFocus, isNull); + inner1.requestFocus(); + await tester.pump(); + expect(FocusManager.instance.primaryFocus, inner1); + outer2.nextFocus(); + await tester.pump(); + expect(FocusManager.instance.primaryFocus, inner2); + }); + + testWidgetsWithLeakTracking('focus traversal should work case 2', (WidgetTester tester) async { + final FocusNode outer1 = FocusNode(debugLabel: 'outer1', skipTraversal: true); + final FocusNode outer2 = FocusNode(debugLabel: 'outer2', skipTraversal: true); + final FocusNode inner1 = FocusNode(debugLabel: 'inner1', ); + final FocusNode inner2 = FocusNode(debugLabel: 'inner2', ); + addTearDown(() { + outer1.dispose(); + outer2.dispose(); + inner1.dispose(); + inner2.dispose(); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: FocusTraversalGroup( + child: Row( + children: <Widget>[ + FocusScope( + child: Focus( + focusNode: outer1, + child: Focus( + focusNode: inner1, + child: const SizedBox(width: 10, height: 10), + ), + ), + ), + FocusScope( + child: Focus( + focusNode: outer2, + child: Focus( + focusNode: inner2, + child: const SizedBox(width: 10, height: 10), + ), + ), + ), + ], + ), + ), + ), + ); + + expect(FocusManager.instance.primaryFocus, isNull); + inner1.requestFocus(); + await tester.pump(); + expect(FocusManager.instance.primaryFocus, inner1); + outer2.nextFocus(); + await tester.pump(); + expect(FocusManager.instance.primaryFocus, inner2); + }); + + testWidgetsWithLeakTracking('Move focus to next node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -212,7 +320,7 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Move focus to previous node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move focus to previous node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -286,9 +394,15 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Move focus to next/previous node while skipping nodes in policy', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move focus to next/previous node while skipping nodes in policy', (WidgetTester tester) async { final List<FocusNode> nodes = List<FocusNode>.generate(7, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( FocusTraversalGroup( policy: SkipAllButFirstAndLastPolicy(), @@ -320,11 +434,14 @@ void main() { expect(nodes[0].hasPrimaryFocus, isTrue); }); - testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node'); + addTearDown(testNode1.dispose); final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); + addTearDown(testNode2.dispose); + await tester.pumpWidget( MaterialApp( home: FocusTraversalGroup( @@ -386,9 +503,10 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); + addTearDown(testNode1.dispose); bool calledCallback = false; await tester.pumpWidget( @@ -431,7 +549,7 @@ void main() { }); group(ReadingOrderTraversalPolicy, () { - testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -470,7 +588,7 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Move reading focus to next node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move reading focus to next node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -585,7 +703,47 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Move reading focus to previous node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Requesting nextFocus on node focuses its descendant', (WidgetTester tester) async { + for (final bool canRequestFocus in <bool>{true, false}) { + final FocusNode node1 = FocusNode(); + final FocusNode node2 = FocusNode(); + addTearDown(() { + node1.dispose(); + node2.dispose(); + }); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: FocusScope( + child: Focus( + focusNode: node1, + canRequestFocus: canRequestFocus, + child: Focus( + focusNode: node2, + child: Container(), + ), + ), + ), + ), + ), + ); + + final bool didFindNode = node1.nextFocus(); + await tester.pump(); + expect(didFindNode, isTrue); + if (canRequestFocus) { + expect(node1.hasPrimaryFocus, isTrue); + expect(node2.hasPrimaryFocus, isFalse); + } else { + expect(node1.hasPrimaryFocus, isFalse); + expect(node2.hasPrimaryFocus, isTrue); + } + } + }); + + testWidgetsWithLeakTracking('Move reading focus to previous node.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); @@ -659,10 +817,17 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async { const int nodeCount = 10; final FocusScopeNode scopeNode = FocusScopeNode(); + addTearDown(scopeNode.dispose); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + Widget buildTest(TextDirection topDirection) { return Directionality( textDirection: topDirection, @@ -774,9 +939,15 @@ void main() { expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 8, 7, 9])); }); - testWidgets('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async { const int nodeCount = 10; final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -868,9 +1039,10 @@ void main() { expect(order, orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); }); - testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); + addTearDown(testNode1.dispose); bool calledCallback = false; await tester.pumpWidget( @@ -915,7 +1087,7 @@ void main() { }); group(OrderedTraversalPolicy, () { - testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); await tester.pumpWidget(FocusTraversalGroup( @@ -954,9 +1126,15 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async { const int nodeCount = 10; final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -993,9 +1171,15 @@ void main() { } }); - testWidgets('Move focus to next/previous node using numerical order.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move focus to next/previous node using numerical order.', (WidgetTester tester) async { const int nodeCount = 10; final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1034,12 +1218,18 @@ void main() { } }); - testWidgets('Move focus to next/previous node using lexical order.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move focus to next/previous node using lexical order.', (WidgetTester tester) async { const int nodeCount = 10; /// Generate ['J' ... 'A']; final List<String> keys = List<String>.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1)); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node ${keys[index]}')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1078,10 +1268,17 @@ void main() { } }); - testWidgets('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async { const int nodeCount = 10; final FocusScopeNode scopeNode = FocusScopeNode(); + addTearDown(scopeNode.dispose); final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1205,11 +1402,14 @@ void main() { expect(order, orderedEquals(expectedOrder)); }); - testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node'); + addTearDown(testNode1.dispose); final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); + addTearDown(testNode2.dispose); + await tester.pumpWidget( MaterialApp( home: FocusTraversalGroup( @@ -1277,9 +1477,10 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on the next/previous focus.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); + addTearDown(testNode1.dispose); bool calledCallback = false; await tester.pumpWidget( @@ -1324,7 +1525,7 @@ void main() { }); group(DirectionalFocusTraversalPolicyMixin, () { - testWidgets('Move focus in all directions.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Move focus in all directions.', (WidgetTester tester) async { final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); @@ -1464,9 +1665,15 @@ void main() { expect(scope.hasFocus, isTrue); }); - testWidgets('Directional focus avoids hysteresis.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directional focus avoids hysteresis.', (WidgetTester tester) async { List<bool?> focus = List<bool?>.generate(6, (int _) => null); final List<FocusNode> nodes = List<FocusNode>.generate(6, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); + Focus makeFocus(int index) { return Focus( debugLabel: '[$index]', @@ -1582,11 +1789,16 @@ void main() { clear(); }); - testWidgets('Directional prefers the closest node even on irregular grids', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Directional prefers the closest node even on irregular grids', (WidgetTester tester) async { const int cols = 3; const int rows = 3; List<bool?> focus = List<bool?>.generate(rows * cols, (int _) => null); final List<FocusNode> nodes = List<FocusNode>.generate(rows * cols, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); Widget makeFocus(int row, int col) { final int index = row * rows + col; @@ -1718,10 +1930,15 @@ void main() { clear(); }); - testWidgets('Closest vertical is picked when only out of band items are considered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Closest vertical is picked when only out of band items are considered', (WidgetTester tester) async { const int rows = 4; List<bool?> focus = List<bool?>.generate(rows, (int _) => null); final List<FocusNode> nodes = List<FocusNode>.generate(rows, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); Widget makeFocus(int row) { return Padding( @@ -1804,10 +2021,15 @@ void main() { clear(); }); - testWidgets('Closest horizontal is picked when only out of band items are considered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Closest horizontal is picked when only out of band items are considered', (WidgetTester tester) async { const int cols = 4; List<bool?> focus = List<bool?>.generate(cols, (int _) => null); final List<FocusNode> nodes = List<FocusNode>.generate(cols, (int index) => FocusNode(debugLabel: 'Node $index')); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); Widget makeFocus(int col) { return Padding( @@ -1890,7 +2112,7 @@ void main() { clear(); }); - testWidgets('Can find first focus in all directions.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can find first focus in all directions.', (WidgetTester tester) async { final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); @@ -1950,10 +2172,13 @@ void main() { expect(policy.findFirstFocusInDirection(scope, TraversalDirection.right), equals(upperLeftNode)); }); - testWidgets('Can find focus when policy data dirty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can find focus when policy data dirty', (WidgetTester tester) async { final FocusNode focusTop = FocusNode(debugLabel: 'top'); + addTearDown(focusTop.dispose); final FocusNode focusCenter = FocusNode(debugLabel: 'center'); + addTearDown(focusCenter.dispose); final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); + addTearDown(focusBottom.dispose); final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); await tester.pumpWidget(FocusTraversalGroup( @@ -2001,7 +2226,7 @@ void main() { expect(focusTop.hasFocus, isTrue); }); - testWidgets('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async { final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); @@ -2090,12 +2315,76 @@ void main() { expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 - testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal actions works when current focus skip traversal', (WidgetTester tester) async { + final GlobalKey key1 = GlobalKey(debugLabel: 'key1'); + final GlobalKey key2 = GlobalKey(debugLabel: 'key2'); + final GlobalKey key3 = GlobalKey(debugLabel: 'key3'); + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return TestRoute( + child: Directionality( + textDirection: TextDirection.ltr, + child: FocusScope( + debugLabel: 'scope', + child: Column( + children: <Widget>[ + Row( + children: <Widget>[ + Focus( + autofocus: true, + skipTraversal: true, + debugLabel: '1', + child: SizedBox(width: 100, height: 100, key: key1), + ), + Focus( + debugLabel: '2', + child: SizedBox(width: 100, height: 100, key: key2), + ), + Focus( + debugLabel: '3', + child: SizedBox(width: 100, height: 100, key: key3), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), + ); + + expect(Focus.of(key1.currentContext!).hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + expect(Focus.of(key2.currentContext!).hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + // Skips key 1 because it skips traversal. + expect(Focus.of(key2.currentContext!).hasPrimaryFocus, isTrue); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + expect(Focus.of(key3.currentContext!).hasPrimaryFocus, isTrue); + }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 + + testWidgetsWithLeakTracking('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async { final List<int> items = List<int>.generate(11, (int index) => index).toList(); final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); final FocusNode topNode = FocusNode(debugLabel: 'Header'); + addTearDown(topNode.dispose); final FocusNode bottomNode = FocusNode(debugLabel: 'Footer'); + addTearDown(bottomNode.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Column( @@ -2187,12 +2476,21 @@ void main() { expect(controller.offset, equals(0.0)); }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 - testWidgets('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async { final List<int> items = List<int>.generate(11, (int index) => index).toList(); final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); final FocusNode leftNode = FocusNode(debugLabel: 'Left Side'); + addTearDown(leftNode.dispose); final FocusNode rightNode = FocusNode(debugLabel: 'Right Side'); + addTearDown(rightNode.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Row( @@ -2285,23 +2583,31 @@ void main() { expect(controller.offset, equals(0.0)); }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 - testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async { final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); final TextEditingController controller1 = TextEditingController(); + addTearDown(controller1.dispose); final TextEditingController controller2 = TextEditingController(); + addTearDown(controller2.dispose); final TextEditingController controller3 = TextEditingController(); + addTearDown(controller3.dispose); final TextEditingController controller4 = TextEditingController(); + addTearDown(controller4.dispose); final FocusNode focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft'); + addTearDown(focusNodeUpperLeft.dispose); final FocusNode focusNodeUpperRight = FocusNode(debugLabel: 'upperRight'); + addTearDown(focusNodeUpperRight.dispose); final FocusNode focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft'); + addTearDown(focusNodeLowerLeft.dispose); final FocusNode focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight'); + addTearDown(focusNodeLowerRight.dispose); - Widget generateTestWidgets(bool ignoreTextFields) { + Widget generatetestWidgetsWithLeakTracking(bool ignoreTextFields) { final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left, ignoreTextFields: ignoreTextFields), const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right, ignoreTextFields: ignoreTextFields), @@ -2380,7 +2686,7 @@ void main() { ); } - await tester.pumpWidget(generateTestWidgets(false)); + await tester.pumpWidget(generatetestWidgetsWithLeakTracking(false)); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); @@ -2392,7 +2698,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); - await tester.pumpWidget(generateTestWidgets(true)); + await tester.pumpWidget(generatetestWidgetsWithLeakTracking(true)); expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); @@ -2408,7 +2714,7 @@ void main() { expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async { final List<Object> events = <Object>[]; await tester.pumpWidget(MaterialApp(home: Container())); @@ -2424,7 +2730,7 @@ void main() { expect(events.length, 2); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title'))))); final FocusNode? initialFocus = primaryFocus; await tester.sendKeyEvent(LogicalKeyboardKey.tab); @@ -2432,7 +2738,7 @@ void main() { expect(primaryFocus, equals(initialFocus)); }); - testWidgets('Focus traversal does not break when no focusable is available on a WidgetsApp', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Focus traversal does not break when no focusable is available on a WidgetsApp', (WidgetTester tester) async { final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( @@ -2458,9 +2764,10 @@ void main() { expect(events.length, 2); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Custom requestFocusCallback gets called on focusInDirection up/down/left/right.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom requestFocusCallback gets called on focusInDirection up/down/left/right.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final FocusNode testNode1 = FocusNode(debugLabel: 'Focus Node'); + addTearDown(testNode1.dispose); bool calledCallback = false; await tester.pumpWidget( @@ -2514,7 +2821,7 @@ void main() { }); group(FocusTraversalGroup, () { - testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(FocusTraversalGroup(child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); @@ -2522,11 +2829,13 @@ void main() { semantics.dispose(); }); - testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? gotFocus; + await tester.pumpWidget( FocusTraversalGroup( descendantsAreFocusable: false, @@ -2561,13 +2870,16 @@ void main() { expect(unfocusableNode.hasFocus, isFalse); }); - testWidgets('Group applies correct policy if focus tree is different from widget tree.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Group applies correct policy if focus tree is different from widget tree.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key4 = GlobalKey(debugLabel: '4'); final FocusNode focusNode = FocusNode(debugLabel: 'child'); + addTearDown(focusNode.dispose); final FocusNode parentFocusNode = FocusNode(debugLabel: 'parent'); + addTearDown(parentFocusNode.dispose); + await tester.pumpWidget( Column( children: <Widget>[ @@ -2603,9 +2915,11 @@ void main() { expect(FocusTraversalGroup.of(key2.currentContext!), const TypeMatcher<SkipAllButFirstAndLastPolicy>()); }); - testWidgets("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async { final FocusNode node1 = FocusNode(); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(); + addTearDown(node2.dispose); await tester.pumpWidget( FocusTraversalGroup( @@ -2638,9 +2952,11 @@ void main() { expect(node2.hasPrimaryFocus, isFalse); }); - testWidgets("FocusTraversalGroup with skipTraversal for all descendants set to true doesn't cause an exception.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("FocusTraversalGroup with skipTraversal for all descendants set to true doesn't cause an exception.", (WidgetTester tester) async { final FocusNode node1 = FocusNode(); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(); + addTearDown(node2.dispose); await tester.pumpWidget( FocusTraversalGroup( @@ -2674,11 +2990,13 @@ void main() { expect(node2.hasPrimaryFocus, isFalse); }); - testWidgets("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); bool? gotFocus; + await tester.pumpWidget( FocusTraversalGroup( child: Column( @@ -2723,9 +3041,11 @@ void main() { expect(unfocusableNode.hasFocus, isFalse); }); - testWidgets("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'Test Key'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); + await tester.pumpWidget( FocusTraversalGroup( child: Directionality( @@ -2754,9 +3074,11 @@ void main() { }); group(RawKeyboardListener, () { - testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( RawKeyboardListener( focusNode: focusNode, @@ -2781,9 +3103,11 @@ void main() { semantics.dispose(); }); - testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( RawKeyboardListener( focusNode: focusNode, @@ -2798,11 +3122,15 @@ void main() { }); group(ExcludeFocusTraversal, () { - testWidgets("Descendants aren't traversable", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Descendants aren't traversable", (WidgetTester tester) async { final FocusNode node1 = FocusNode(debugLabel: 'node 1'); + addTearDown(node1.dispose); final FocusNode node2 = FocusNode(debugLabel: 'node 2'); + addTearDown(node2.dispose); final FocusNode node3 = FocusNode(debugLabel: 'node 3'); + addTearDown(node3.dispose); final FocusNode node4 = FocusNode(debugLabel: 'node 4'); + addTearDown(node4.dispose); await tester.pumpWidget( FocusTraversalGroup( @@ -2846,7 +3174,7 @@ void main() { expect(node4.hasPrimaryFocus, isTrue); }); - testWidgets("Doesn't introduce a Semantics node", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Doesn't introduce a Semantics node", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(ExcludeFocusTraversal(child: Container())); final TestSemantics expectedSemantics = TestSemantics.root(); @@ -2862,9 +3190,11 @@ void main() { // other focusable HTML elements surrounding Flutter. // // See also: https://github.com/flutter/flutter/issues/114463 - testWidgets('Default route edge traversal behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default route edge traversal behavior', (WidgetTester tester) async { final FocusNode nodeA = FocusNode(); + addTearDown(nodeA.dispose); final FocusNode nodeB = FocusNode(); + addTearDown(nodeB.dispose); Future<bool> nextFocus() async { final bool result = Actions.invoke( @@ -2942,12 +3272,15 @@ void main() { // This test creates a FocusScopeNode configured to traverse focus in a closed // loop. After traversing one loop, it changes the behavior to leave the // FlutterView, then verifies that the new behavior did indeed take effect. - testWidgets('FocusScopeNode.traversalEdgeBehavior takes effect after update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusScopeNode.traversalEdgeBehavior takes effect after update', (WidgetTester tester) async { final FocusScopeNode scope = FocusScopeNode(); + addTearDown(scope.dispose); expect(scope.traversalEdgeBehavior, TraversalEdgeBehavior.closedLoop); final FocusNode nodeA = FocusNode(); + addTearDown(nodeA.dispose); final FocusNode nodeB = FocusNode(); + addTearDown(nodeB.dispose); Future<bool> nextFocus() async { final bool result = Actions.invoke( @@ -3031,7 +3364,7 @@ void main() { expect(nodeB.hasFocus, true); }); - testWidgets('NextFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NextFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { expect( NextFocusAction().toKeyEventResult(const NextFocusIntent(), true), KeyEventResult.handled, @@ -3042,7 +3375,7 @@ void main() { ); }); - testWidgets('PreviousFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PreviousFocusAction converts invoke result to KeyEventResult', (WidgetTester tester) async { expect( PreviousFocusAction().toKeyEventResult(const PreviousFocusIntent(), true), KeyEventResult.handled, @@ -3053,9 +3386,10 @@ void main() { ); }); - testWidgets('RequestFocusAction calls the RequestFocusIntent.requestFocusCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RequestFocusAction calls the RequestFocusIntent.requestFocusCallback', (WidgetTester tester) async { bool calledCallback = false; final FocusNode nodeA = FocusNode(); + addTearDown(nodeA.dispose); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index 98d10c636afd5..6d2996c723640 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -6,9 +6,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('onSaved callback is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSaved callback is called', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); String? fieldValue; @@ -48,7 +49,7 @@ void main() { await checkText(''); }); - testWidgets('onChanged callback is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onChanged callback is called', (WidgetTester tester) async { String? fieldValue; Widget builder() { @@ -85,7 +86,7 @@ void main() { await checkText(''); }); - testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Validator sets the error text only when validate is called', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); String? errorText(String? value) => '${value ?? ''}/error'; @@ -139,7 +140,7 @@ void main() { await checkErrorText(''); }); - testWidgets('Should announce error text when validate returns error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Should announce error text when validate returns error', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); await tester.pumpWidget( MaterialApp( @@ -178,7 +179,7 @@ void main() { }); - testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isValid returns true when a field is valid', (WidgetTester tester) async { final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>(); final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>(); const String validString = 'Valid string'; @@ -223,7 +224,7 @@ void main() { expect(fieldKey2.currentState!.isValid, isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'isValid returns false when the field is invalid and does not change error display', (WidgetTester tester) async { final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>(); @@ -272,7 +273,7 @@ void main() { }, ); - testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiple TextFormFields communicate', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>(); // Input 2's validator depends on a input 1's value. @@ -322,7 +323,7 @@ void main() { await checkErrorText(''); }); - testWidgets('Provide initial value to input when no controller is specified', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Provide initial value to input when no controller is specified', (WidgetTester tester) async { const String initialValue = 'hello'; final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); @@ -366,8 +367,9 @@ void main() { expect(editableText.widget.controller.text, equals('world')); }); - testWidgets('Controller defines initial value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Controller defines initial value', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'hello'); + addTearDown(controller.dispose); const String initialValue = 'hello'; final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); @@ -413,10 +415,11 @@ void main() { expect(controller.text, equals('world')); }); - testWidgets('TextFormField resets to its initial value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextFormField resets to its initial value', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); final TextEditingController controller = TextEditingController(text: 'Plover'); + addTearDown(controller.dispose); Widget builder() { return MaterialApp( @@ -459,9 +462,11 @@ void main() { expect(controller.text, equals('Plover')); }); - testWidgets('TextEditingController updates to/from form field value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextEditingController updates to/from form field value', (WidgetTester tester) async { final TextEditingController controller1 = TextEditingController(text: 'Foo'); + addTearDown(controller1.dispose); final TextEditingController controller2 = TextEditingController(text: 'Bar'); + addTearDown(controller2.dispose); final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); TextEditingController? currentController; @@ -566,7 +571,7 @@ void main() { expect(controller2.text, equals('Xyzzy')); }); - testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); String? fieldValue; @@ -620,7 +625,7 @@ void main() { expect(formKey.currentState!.validate(), isTrue); }); - testWidgets('Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction', (WidgetTester tester) async { late FormFieldState<String> formFieldState; String? errorText(String? value) => '$value/error'; @@ -656,7 +661,7 @@ void main() { expect(find.text(errorText('foo')!), findsNothing); }); - testWidgets('auto-validate before value changes if autovalidateMode was set to always', (WidgetTester tester) async { + testWidgetsWithLeakTracking('auto-validate before value changes if autovalidateMode was set to always', (WidgetTester tester) async { late FormFieldState<String> formFieldState; String? errorText(String? value) => '$value/error'; @@ -689,7 +694,7 @@ void main() { expect(formFieldState.hasError, isTrue); }); - testWidgets('Form auto-validates form fields only after one of them changes if autovalidateMode is onUserInteraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Form auto-validates form fields only after one of them changes if autovalidateMode is onUserInteraction', (WidgetTester tester) async { const String initialValue = 'foo'; String? errorText(String? value) => 'error/$value'; @@ -743,7 +748,7 @@ void main() { expect(find.text(errorText(initialValue)!), findsNWidgets(2)); }); - testWidgets('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async { String? errorText(String? value) => 'error/$value'; Widget builder() { @@ -773,7 +778,7 @@ void main() { expect(find.text(errorText('')!), findsOneWidget); }); - testWidgets('Form.reset() resets form fields, and auto validation will only happen on the next user interaction if autovalidateMode is onUserInteraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Form.reset() resets form fields, and auto validation will only happen on the next user interaction if autovalidateMode is onUserInteraction', (WidgetTester tester) async { final GlobalKey<FormState> formState = GlobalKey<FormState>(); String? errorText(String? value) => '$value/error'; @@ -818,7 +823,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/63753. - testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Validate form should return correct validation if the value is composing', (WidgetTester tester) async { final GlobalKey<FormState> formKey = GlobalKey<FormState>(); String? fieldValue; @@ -854,4 +859,157 @@ void main() { expect(fieldValue, '123456'); expect(formKey.currentState!.validate(), isFalse); }); + + testWidgetsWithLeakTracking('hasInteractedByUser returns false when the input has not changed', (WidgetTester tester) async { + final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>(); + + final Widget widget = MaterialApp( + home: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: TextFormField( + key: fieldKey, + ), + ), + ), + ), + ), + ); + + await tester.pumpWidget(widget); + + expect(fieldKey.currentState!.hasInteractedByUser, isFalse); + }); + + testWidgetsWithLeakTracking('hasInteractedByUser returns true after the input has changed', (WidgetTester tester) async { + final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>(); + + final Widget widget = MaterialApp( + home: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: TextFormField( + key: fieldKey, + ), + ), + ), + ), + ), + ); + + await tester.pumpWidget(widget); + + // initially, the field has not been interacted with + expect(fieldKey.currentState!.hasInteractedByUser, isFalse); + + // after entering text, the field has been interacted with + await tester.enterText(find.byType(TextFormField), 'foo'); + expect(fieldKey.currentState!.hasInteractedByUser, isTrue); + }); + + testWidgetsWithLeakTracking('hasInteractedByUser returns false after the field is reset', (WidgetTester tester) async { + final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>(); + + final Widget widget = MaterialApp( + home: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: TextFormField( + key: fieldKey, + ), + ), + ), + ), + ), + ); + + await tester.pumpWidget(widget); + + // initially, the field has not been interacted with + expect(fieldKey.currentState!.hasInteractedByUser, isFalse); + + // after entering text, the field has been interacted with + await tester.enterText(find.byType(TextFormField), 'foo'); + expect(fieldKey.currentState!.hasInteractedByUser, isTrue); + + // after resetting the field, it has not been interacted with again + fieldKey.currentState!.reset(); + expect(fieldKey.currentState!.hasInteractedByUser, isFalse); + }); + + testWidgets('Validator is nullified and error text behaves accordingly', + (WidgetTester tester) async { + final GlobalKey<FormState> formKey = GlobalKey<FormState>(); + bool useValidator = false; + late StateSetter setState; + + String? validator(String? value) { + if (value == null || value.isEmpty) { + return 'test_error'; + } + return null; + } + + Widget builder() { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return MaterialApp( + home: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Material( + child: Form( + key: formKey, + child: TextFormField( + validator: useValidator ? validator : null, + ), + ), + ), + ), + ), + ), + ); + }, + ); + } + + await tester.pumpWidget(builder()); + + // Start with no validator. + await tester.enterText(find.byType(TextFormField), ''); + await tester.pump(); + formKey.currentState!.validate(); + await tester.pump(); + expect(find.text('test_error'), findsNothing); + + // Now use the validator. + setState(() { + useValidator = true; + }); + await tester.pump(); + formKey.currentState!.validate(); + await tester.pump(); + expect(find.text('test_error'), findsOneWidget); + + // Remove the validator again and expect the error to disappear. + setState(() { + useValidator = false; + }); + await tester.pump(); + formKey.currentState!.validate(); + await tester.pump(); + expect(find.text('test_error'), findsNothing); + }); } diff --git a/packages/flutter/test/widgets/fractionally_sized_box_test.dart b/packages/flutter/test/widgets/fractionally_sized_box_test.dart index ddebf06919963..1a9f7ef67574f 100644 --- a/packages/flutter/test/widgets/fractionally_sized_box_test.dart +++ b/packages/flutter/test/widgets/fractionally_sized_box_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('FractionallySizedBox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FractionallySizedBox', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(OverflowBox( minWidth: 0.0, @@ -29,7 +30,7 @@ void main() { expect(box.localToGlobal(Offset.zero), equals(const Offset(25.0, 37.5))); }); - testWidgets('FractionallySizedBox alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FractionallySizedBox alignment', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.rtl, @@ -45,7 +46,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(800.0 - 400.0 / 2.0, 0.0 + 300.0 / 2.0))); }); - testWidgets('FractionallySizedBox alignment (direction-sensitive)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FractionallySizedBox alignment (direction-sensitive)', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.rtl, @@ -61,7 +62,7 @@ void main() { expect(box.localToGlobal(box.size.center(Offset.zero)), equals(const Offset(0.0 + 400.0 / 2.0, 0.0 + 300.0 / 2.0))); }); - testWidgets('OverflowBox alignment with FractionallySizedBox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBox alignment with FractionallySizedBox', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.rtl, diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 4d82aa85308ab..d66f3466911b6 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; typedef ElementRebuildCallback = void Function(StatefulElement element); @@ -22,13 +23,13 @@ class _MyGlobalObjectKey<T extends State<StatefulWidget>> extends GlobalObjectKe } void main() { - testWidgets('UniqueKey control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UniqueKey control test', (WidgetTester tester) async { final Key key = UniqueKey(); expect(key, hasOneLineDescription); expect(key, isNot(equals(UniqueKey()))); }); - testWidgets('ObjectKey control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ObjectKey control test', (WidgetTester tester) async { final Object a = Object(); final Object b = Object(); final Key keyA = ObjectKey(a); @@ -41,7 +42,7 @@ void main() { expect(keyA, isNot(equals(keyB))); }); - testWidgets('GlobalObjectKey toString test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalObjectKey toString test', (WidgetTester tester) async { const GlobalObjectKey one = GlobalObjectKey(1); const GlobalObjectKey<TestState> two = GlobalObjectKey<TestState>(2); const GlobalObjectKey three = _MyGlobalObjectKey(3); @@ -53,7 +54,7 @@ void main() { expect(four.toString(), equals('[_MyGlobalObjectKey<TestState> ${describeIdentity(4)}]')); }); - testWidgets('GlobalObjectKey control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalObjectKey control test', (WidgetTester tester) async { final Object a = Object(); final Object b = Object(); final Key keyA = GlobalObjectKey(a); @@ -66,7 +67,7 @@ void main() { expect(keyA, isNot(equals(keyB))); }); - testWidgets('GlobalKey correct case 1 - can move global key from container widget to layoutbuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey correct case 1 - can move global key from container widget to layoutbuilder', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'correct'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -101,7 +102,7 @@ void main() { )); }); - testWidgets('GlobalKey correct case 2 - can move global key from layoutbuilder to container widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey correct case 2 - can move global key from layoutbuilder to container widget', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'correct'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -135,7 +136,7 @@ void main() { )); }); - testWidgets('GlobalKey correct case 3 - can deal with early rebuild in layoutbuilder - move backward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey correct case 3 - can deal with early rebuild in layoutbuilder - move backward', (WidgetTester tester) async { const Key key1 = GlobalObjectKey('Text1'); const Key key2 = GlobalObjectKey('Text2'); Key? rebuiltKeyOfSecondChildBeforeLayout; @@ -224,7 +225,7 @@ void main() { expect(rebuiltKeyOfSecondChildAfterLayout, key1); }); - testWidgets('GlobalKey correct case 4 - can deal with early rebuild in layoutbuilder - move forward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey correct case 4 - can deal with early rebuild in layoutbuilder - move forward', (WidgetTester tester) async { const Key key1 = GlobalObjectKey('Text1'); const Key key2 = GlobalObjectKey('Text2'); const Key key3 = GlobalObjectKey('Text3'); @@ -327,7 +328,7 @@ void main() { expect(rebuiltKeyOfThirdChildAfterLayout, key2); }); - testWidgets('GlobalKey correct case 5 - can deal with early rebuild in layoutbuilder - only one global key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey correct case 5 - can deal with early rebuild in layoutbuilder - only one global key', (WidgetTester tester) async { const Key key1 = GlobalObjectKey('Text1'); Key? rebuiltKeyOfSecondChildBeforeLayout; Key? rebuiltKeyOfThirdChildAfterLayout; @@ -418,7 +419,7 @@ void main() { expect(rebuiltKeyOfThirdChildAfterLayout, key1); }); - testWidgets('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -447,7 +448,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( @@ -493,7 +494,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -520,7 +521,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -547,7 +548,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -565,7 +566,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 6 - splitting and not changing type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 6 - splitting and not changing type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -583,7 +584,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 7 - appearing later', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 7 - appearing later', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -602,7 +603,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 8 - appearing earlier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 8 - appearing earlier', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -621,7 +622,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 9 - moving and appearing later', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 9 - moving and appearing later', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -642,7 +643,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 10 - moving and appearing earlier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 10 - moving and appearing earlier', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -663,7 +664,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 11 - double sibling appearance', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 11 - double sibling appearance', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -675,7 +676,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 12 - all kinds of badness at once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 12 - all kinds of badness at once', (WidgetTester tester) async { final Key key1 = GlobalKey(debugLabel: 'problematic'); final Key key2 = GlobalKey(debugLabel: 'problematic'); // intentionally the same label final Key key3 = GlobalKey(debugLabel: 'also problematic'); @@ -723,7 +724,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async { final Key key1 = GlobalKey(debugLabel: 'problematic'); final Key key2 = GlobalKey(debugLabel: 'problematic'); // intentionally the same label final Key key3 = GlobalKey(debugLabel: 'also problematic'); @@ -770,7 +771,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 14 - moving during build - before', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 14 - moving during build - before', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -789,7 +790,7 @@ void main() { )); }); - testWidgets('GlobalKey duplication 15 - duplicating during build - before', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 15 - duplicating during build - before', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -810,7 +811,7 @@ void main() { expect(tester.takeException(), isFlutterError); }); - testWidgets('GlobalKey duplication 16 - moving during build - after', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 16 - moving during build - after', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -829,7 +830,7 @@ void main() { )); }); - testWidgets('GlobalKey duplication 17 - duplicating during build - after', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 17 - duplicating during build - after', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( textDirection: TextDirection.ltr, @@ -857,7 +858,7 @@ void main() { expect(count, 1); }); - testWidgets('GlobalKey duplication 18 - subtree build duplicate key with same type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 18 - subtree build duplicate key with same type', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); final Stack stack = Stack( textDirection: TextDirection.ltr, @@ -894,7 +895,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 19 - subtree build duplicate key with different types', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 19 - subtree build duplicate key with different types', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); final Stack stack = Stack( textDirection: TextDirection.ltr, @@ -922,7 +923,7 @@ void main() { ); }); - testWidgets('GlobalKey duplication 20 - real duplication with early rebuild in layoutbuilder will throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey duplication 20 - real duplication with early rebuild in layoutbuilder will throw', (WidgetTester tester) async { const Key key1 = GlobalObjectKey('Text1'); const Key key2 = GlobalObjectKey('Text2'); Key? rebuiltKeyOfSecondChildBeforeLayout; @@ -1023,14 +1024,17 @@ void main() { ); }); - testWidgets('GlobalKey - detach and re-attach child to different parents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey - detach and re-attach child to different parents', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( - controller: ScrollController(), + controller: scrollController, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(<Widget>[ @@ -1104,7 +1108,7 @@ void main() { expect(tabController.index, 0); }); - testWidgets('Defunct setState throws exception', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defunct setState throws exception', (WidgetTester tester) async { late StateSetter setState; await tester.pumpWidget(StatefulBuilder( @@ -1122,12 +1126,12 @@ void main() { expect(() { setState(() { }); }, throwsFlutterError); }); - testWidgets('State toString', (WidgetTester tester) async { + testWidgetsWithLeakTracking('State toString', (WidgetTester tester) async { final TestState state = TestState(); expect(state.toString(), contains('no widget')); }); - testWidgets('debugPrintGlobalKeyedWidgetLifecycle control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugPrintGlobalKeyedWidgetLifecycle control test', (WidgetTester tester) async { expect(debugPrintGlobalKeyedWidgetLifecycle, isFalse); final DebugPrintCallback oldCallback = debugPrint; @@ -1150,7 +1154,7 @@ void main() { expect(log[1], matches('Discarding .+ from inactive elements list.')); }); - testWidgets('MultiChildRenderObjectElement.children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MultiChildRenderObjectElement.children', (WidgetTester tester) async { GlobalKey key0, key1, key2; await tester.pumpWidget(Column( key: key0 = GlobalKey(), @@ -1169,7 +1173,7 @@ void main() { ); }); - testWidgets('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - mount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - mount', (WidgetTester tester) async { await tester.pumpWidget( Column( children: <Widget>[ @@ -1194,7 +1198,7 @@ void main() { ); }); - testWidgets('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - update', (WidgetTester tester) async { await tester.pumpWidget( Column( children: <Widget>[ @@ -1227,7 +1231,7 @@ void main() { ); }); - testWidgets('Element diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Element diagnostics', (WidgetTester tester) async { GlobalKey key0; await tester.pumpWidget(Column( key: key0 = GlobalKey(), @@ -1319,7 +1323,7 @@ void main() { } }); - testWidgets('didUpdateDependencies is not called on a State that never rebuilds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didUpdateDependencies is not called on a State that never rebuilds', (WidgetTester tester) async { final GlobalKey<DependentState> key = GlobalKey<DependentState>(); /// Initial build - should call didChangeDependencies, not deactivate @@ -1348,7 +1352,7 @@ void main() { expect(state.deactivatedCount, 2); }); - testWidgets('StatefulElement subclass can decorate State.build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StatefulElement subclass can decorate State.build', (WidgetTester tester) async { late bool isDidChangeDependenciesDecorated; late bool isBuildDecorated; @@ -1372,7 +1376,7 @@ void main() { expect(isDidChangeDependenciesDecorated, isFalse); }); group('BuildContext.debugDoingbuild', () { - testWidgets('StatelessWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StatelessWidget', (WidgetTester tester) async { late bool debugDoingBuildOnBuild; await tester.pumpWidget( StatelessWidgetSpy( @@ -1387,7 +1391,7 @@ void main() { expect(context.debugDoingBuild, isFalse); expect(debugDoingBuildOnBuild, isTrue); }); - testWidgets('StatefulWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('StatefulWidget', (WidgetTester tester) async { late bool debugDoingBuildOnBuild; late bool debugDoingBuildOnInitState; late bool debugDoingBuildOnDidChangeDependencies; @@ -1456,11 +1460,12 @@ void main() { expect(debugDoingBuildOnDispose, isFalse); expect(debugDoingBuildOnDeactivate, isFalse); }); - testWidgets('RenderObjectWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObjectWidget', (WidgetTester tester) async { late bool debugDoingBuildOnCreateRenderObject; bool? debugDoingBuildOnUpdateRenderObject; bool? debugDoingBuildOnDidUnmountRenderObject; final ValueNotifier<int> notifier = ValueNotifier<int>(0); + addTearDown(notifier.dispose); late BuildContext spyContext; @@ -1516,7 +1521,7 @@ void main() { }); }); - testWidgets('A widget whose element has an invalid visitChildren implementation triggers a useful error message', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A widget whose element has an invalid visitChildren implementation triggers a useful error message', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(_WidgetWithNoVisitChildren(_StatefulLeaf(key: key))); (key.currentState! as _StatefulLeafState).markNeedsBuild(); @@ -1542,11 +1547,13 @@ void main() { ); }); - testWidgets('Can create BuildOwner that does not interfere with pointer router or raw key event handler', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can create BuildOwner that does not interfere with pointer router or raw key event handler', (WidgetTester tester) async { final int pointerRouterCount = GestureBinding.instance.pointerRouter.debugGlobalRouteCount; final RawKeyEventHandler? rawKeyEventHandler = RawKeyboard.instance.keyEventHandler; expect(rawKeyEventHandler, isNotNull); - BuildOwner(focusManager: FocusManager()); + final FocusManager focusManager = FocusManager(); + addTearDown(focusManager.dispose); + BuildOwner(focusManager: focusManager); expect(GestureBinding.instance.pointerRouter.debugGlobalRouteCount, pointerRouterCount); expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler)); }); @@ -1601,7 +1608,7 @@ void main() { expect(dependenciesProperty.toDescription(), '[ButtonBarTheme, Directionality, FocusTraversalOrder]'); }); - testWidgets('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async { final int initialCount = tester.binding.buildOwner!.globalKeyCount; final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); @@ -1615,7 +1622,7 @@ void main() { expect(tester.binding.buildOwner!.globalKeyCount, initialCount + 0); }); - testWidgets('Widget and State properties are nulled out when unmounted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget and State properties are nulled out when unmounted', (WidgetTester tester) async { await tester.pumpWidget(const _StatefulLeaf()); final StatefulElement element = tester.element<StatefulElement>(find.byType(_StatefulLeaf)); expect(element.state, isA<State<_StatefulLeaf>>()); @@ -1631,7 +1638,7 @@ void main() { expect(() => element.widget, throwsA(isA<TypeError>())); }); - testWidgets('LayerLink can be swapped between parent and child container layers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayerLink can be swapped between parent and child container layers', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/96959. final LayerLink link = LayerLink(); await tester.pumpWidget(_TestLeaderLayerWidget( @@ -1653,7 +1660,7 @@ void main() { }); - testWidgets('Deactivate and activate are called correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Deactivate and activate are called correctly', (WidgetTester tester) async { final List<String> states = <String>[]; Widget build([Key? key]) { return StatefulWidgetSpy( @@ -1687,7 +1694,7 @@ void main() { expect(states, <String>['deactivate', 'dispose']); }); - testWidgets('RenderObjectElement.unmount disposes of its renderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObjectElement.unmount disposes of its renderObject', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); final RenderObjectElement element = tester.allElements.whereType<RenderObjectElement>().last; final RenderObject renderObject = element.renderObject; @@ -1699,7 +1706,7 @@ void main() { expect(renderObject.debugDisposed, true); }); - testWidgets('Getting the render object of an unmounted element throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Getting the render object of an unmounted element throws', (WidgetTester tester) async { await tester.pumpWidget(const _StatefulLeaf()); final StatefulElement element = tester.element<StatefulElement>(find.byType(_StatefulLeaf)); expect(element.state, isA<State<_StatefulLeaf>>()); @@ -1754,7 +1761,7 @@ The findRenderObject() method was called for the following element: expect(child.doesDependOnInheritedElement(ancestor), isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'MultiChildRenderObjectElement.updateChildren test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/120762. diff --git a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart index 4632cf53bc596..fc43260892e64 100644 --- a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart +++ b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Vertical gesture detector has up/down actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical gesture detector has up/down actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int callCount = 0; @@ -44,7 +45,7 @@ void main() { semantics.dispose(); }); - testWidgets('Horizontal gesture detector has up/down actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal gesture detector has up/down actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int callCount = 0; @@ -78,7 +79,7 @@ void main() { semantics.dispose(); }); - testWidgets('All registered handlers for the gesture kind are called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('All registered handlers for the gesture kind are called', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Set<String> logs = <String>{}; @@ -102,7 +103,7 @@ void main() { semantics.dispose(); }); - testWidgets('Replacing recognizers should update semantic handlers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Replacing recognizers should update semantic handlers', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // How the test is set up: @@ -173,7 +174,7 @@ void main() { }); group("RawGestureDetector's custom semantics delegate", () { - testWidgets('should update semantics notations when switching from the default delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should update semantics notations when switching from the default delegate', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(() => LongPressGestureRecognizer(), null) @@ -208,7 +209,7 @@ void main() { semantics.dispose(); }); - testWidgets('should update semantics notations when switching to the default delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should update semantics notations when switching to the default delegate', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(() => LongPressGestureRecognizer(), null) @@ -243,7 +244,7 @@ void main() { semantics.dispose(); }); - testWidgets('should update semantics notations when switching from a different custom delegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should update semantics notations when switching from a different custom delegate', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(() => LongPressGestureRecognizer(), null) @@ -279,7 +280,7 @@ void main() { semantics.dispose(); }); - testWidgets('should correctly call callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should correctly call callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> logs = <String>[]; final GlobalKey<RawGestureDetectorState> detectorKey = GlobalKey(); @@ -321,7 +322,7 @@ void main() { group("RawGestureDetector's default semantics delegate", () { group('should map onTap to', () { - testWidgets('null when there is no TapGR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null when there is no TapGR', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -339,7 +340,7 @@ void main() { semantics.dispose(); }); - testWidgets('non-null when there is TapGR with no callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-null when there is TapGR with no callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -360,7 +361,7 @@ void main() { semantics.dispose(); }); - testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a callback that correctly calls callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey detectorKey = GlobalKey(); final List<String> logs = <String>[]; @@ -394,7 +395,7 @@ void main() { }); group('should map onLongPress to', () { - testWidgets('null when there is no LongPressGR ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null when there is no LongPressGR ', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -412,7 +413,7 @@ void main() { semantics.dispose(); }); - testWidgets('non-null when there is LongPressGR with no callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-null when there is LongPressGR with no callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -433,7 +434,7 @@ void main() { semantics.dispose(); }); - testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a callback that correctly calls callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey detectorKey = GlobalKey(); final List<String> logs = <String>[]; @@ -466,7 +467,7 @@ void main() { }); group('should map onHorizontalDragUpdate to', () { - testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null when there is no matching recognizers ', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -484,7 +485,7 @@ void main() { semantics.dispose(); }); - testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -526,7 +527,7 @@ void main() { semantics.dispose(); }); - testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a callback that correctly calls callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey detectorKey = GlobalKey(); final List<String> logs = <String>[]; @@ -576,7 +577,7 @@ void main() { }); group('should map onVerticalDragUpdate to', () { - testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null when there is no matching recognizers ', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -594,7 +595,7 @@ void main() { semantics.dispose(); }); - testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( @@ -617,7 +618,7 @@ void main() { semantics.dispose(); }); - testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a callback that correctly calls callbacks', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey detectorKey = GlobalKey(); final List<String> logs = <String>[]; @@ -666,7 +667,7 @@ void main() { }); }); - testWidgets('should update semantics notations when receiving new gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should update semantics notations when receiving new gestures', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Center( diff --git a/packages/flutter/test/widgets/gesture_detector_test.dart b/packages/flutter/test/widgets/gesture_detector_test.dart index 40d3569d11715..3d30a8f32ff1b 100644 --- a/packages/flutter/test/widgets/gesture_detector_test.dart +++ b/packages/flutter/test/widgets/gesture_detector_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { const Offset forcePressOffset = Offset(400.0, 50.0); - testWidgets('Uncontested scrolls start immediately', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Uncontested scrolls start immediately', (WidgetTester tester) async { bool didStartDrag = false; double? updatedDragDelta; bool didEndDrag = false; @@ -58,7 +59,7 @@ void main() { await tester.pumpWidget(Container()); }); - testWidgets('Match two scroll gestures in succession', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Match two scroll gestures in succession', (WidgetTester tester) async { int gestureCount = 0; double dragDistance = 0.0; @@ -91,7 +92,7 @@ void main() { await tester.pumpWidget(Container()); }); - testWidgets("Pan doesn't crash", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Pan doesn't crash", (WidgetTester tester) async { bool didStartPan = false; Offset? panDelta; bool didEndPan = false; @@ -135,7 +136,7 @@ void main() { }, ); - testWidgets('Translucent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Translucent', (WidgetTester tester) async { bool didReceivePointerDown; bool didTap; @@ -206,7 +207,7 @@ void main() { expect(didTap, isTrue); }, variant: buttonVariant); - testWidgets('Empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty', (WidgetTester tester) async { bool didTap = false; await tester.pumpWidget( Center( @@ -228,7 +229,7 @@ void main() { expect(didTap, isTrue); }, variant: buttonVariant); - testWidgets('Only container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Only container', (WidgetTester tester) async { bool didTap = false; await tester.pumpWidget( Center( @@ -251,7 +252,7 @@ void main() { expect(didTap, isFalse); }, variant: buttonVariant); - testWidgets('cache render object', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cache render object', (WidgetTester tester) async { void inputCallback() { } await tester.pumpWidget( @@ -283,7 +284,7 @@ void main() { expect(renderObj1, same(renderObj2)); }, variant: buttonVariant); - testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap down occurs after kPressTimeout', (WidgetTester tester) async { int tapDown = 0; int tap = 0; int tapCancel = 0; @@ -391,7 +392,7 @@ void main() { expect(longPress, 1); }, variant: buttonVariant); - testWidgets('Long Press Up Callback called after long press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Long Press Up Callback called after long press', (WidgetTester tester) async { int longPressUp = 0; await tester.pumpWidget( @@ -441,7 +442,7 @@ void main() { }, variant: buttonVariant); }); - testWidgets('Primary and secondary long press callbacks should work together in GestureDetector', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Primary and secondary long press callbacks should work together in GestureDetector', (WidgetTester tester) async { bool primaryLongPress = false, secondaryLongPress = false; await tester.pumpWidget( @@ -477,7 +478,7 @@ void main() { expect(secondaryLongPress, isTrue); }); - testWidgets('Force Press Callback called after force press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Force Press Callback called after force press', (WidgetTester tester) async { int forcePressStart = 0; int forcePressPeaked = 0; int forcePressUpdate = 0; @@ -580,7 +581,7 @@ void main() { expect(forcePressEnded, 1); }); - testWidgets('Force Press Callback not called if long press triggered before force press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Force Press Callback not called if long press triggered before force press', (WidgetTester tester) async { int forcePressStart = 0; int longPressTimes = 0; @@ -645,7 +646,7 @@ void main() { expect(forcePressStart, 0); }); - testWidgets('Force Press Callback not called if drag triggered before force press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Force Press Callback not called if drag triggered before force press', (WidgetTester tester) async { int forcePressStart = 0; int horizontalDragStart = 0; @@ -706,7 +707,7 @@ void main() { }); group("RawGestureDetectorState's debugFillProperties", () { - testWidgets('when default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when default', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( @@ -724,7 +725,7 @@ void main() { ]); }); - testWidgets('should show gestures, custom semantics and behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should show gestures, custom semantics and behavior', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( @@ -761,7 +762,7 @@ void main() { ]); }); - testWidgets('should not show semantics when excludeFromSemantics is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should not show semantics when excludeFromSemantics is true', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final GlobalKey key = GlobalKey(); await tester.pumpWidget(RawGestureDetector( @@ -832,7 +833,7 @@ void main() { } }); - testWidgets('replaceGestureRecognizers not during layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('replaceGestureRecognizers not during layout', (WidgetTester tester) async { final GlobalKey<RawGestureDetectorState> key = GlobalKey<RawGestureDetectorState>(); await tester.pumpWidget( Directionality( @@ -876,7 +877,7 @@ void main() { }); }); - testWidgets('supportedDevices update test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('supportedDevices update test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/111716 bool didStartPan = false; Offset? panDelta; @@ -946,7 +947,7 @@ void main() { expect(didEndPan, isTrue); }); - testWidgets('supportedDevices is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('supportedDevices is respected', (WidgetTester tester) async { bool didStartPan = false; Offset? panDelta; bool didEndPan = false; @@ -994,7 +995,7 @@ void main() { }); group('DoubleTap', () { - testWidgets('onDoubleTap is called even if onDoubleTapDown has not been not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onDoubleTap is called even if onDoubleTapDown has not been not provided', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( @@ -1017,7 +1018,7 @@ void main() { expect(log, <String>['double-tap']); }); - testWidgets('onDoubleTapDown is called even if onDoubleTap has not been not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onDoubleTapDown is called even if onDoubleTap has not been not provided', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/gesture_disambiguation_test.dart b/packages/flutter/test/widgets/gesture_disambiguation_test.dart index 0788d5a5c1653..617e1046deb7a 100644 --- a/packages/flutter/test/widgets/gesture_disambiguation_test.dart +++ b/packages/flutter/test/widgets/gesture_disambiguation_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('onTap detection with canceled pointer and a drag listener', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap detection with canceled pointer and a drag listener', (WidgetTester tester) async { int detector1TapCount = 0; int detector2TapCount = 0; diff --git a/packages/flutter/test/widgets/global_keys_duplicated_test.dart b/packages/flutter/test/widgets/global_keys_duplicated_test.dart index ec37b19be247d..f2e2430cf8f7b 100644 --- a/packages/flutter/test/widgets/global_keys_duplicated_test.dart +++ b/packages/flutter/test/widgets/global_keys_duplicated_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // There's also some duplicate GlobalKey tests in the framework_test.dart file. void main() { - testWidgets('GlobalKey children of one node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey children of one node', (WidgetTester tester) async { // This is actually a test of the regular duplicate key logic, which // happens before the duplicate GlobalKey logic. await tester.pumpWidget(const Stack(children: <Widget>[ @@ -23,7 +24,7 @@ void main() { expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); }); - testWidgets('GlobalKey children of two nodes - A', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey children of two nodes - A', (WidgetTester tester) async { await tester.pumpWidget(const Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -43,7 +44,7 @@ void main() { expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); - testWidgets('GlobalKey children of two different nodes - B', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey children of two different nodes - B', (WidgetTester tester) async { await tester.pumpWidget(const Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -61,7 +62,7 @@ void main() { expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); - testWidgets('GlobalKey children of two nodes - C', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GlobalKey children of two nodes - C', (WidgetTester tester) async { late StateSetter nestedSetState; bool flag = false; await tester.pumpWidget(Stack( diff --git a/packages/flutter/test/widgets/global_keys_moving_test.dart b/packages/flutter/test/widgets/global_keys_moving_test.dart index 4984289950a31..1fd8eaf1be174 100644 --- a/packages/flutter/test/widgets/global_keys_moving_test.dart +++ b/packages/flutter/test/widgets/global_keys_moving_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Item { GlobalKey key1 = GlobalKey(); @@ -55,7 +56,7 @@ Widget builder() { } void main() { - testWidgets('moving subtrees with global keys - smoketest', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moving subtrees with global keys - smoketest', (WidgetTester tester) async { await tester.pumpWidget(builder()); final StatefulLeafState leaf = tester.firstState(find.byType(StatefulLeaf)); leaf.markNeedsBuild(); diff --git a/packages/flutter/test/widgets/grid_paper_test.dart b/packages/flutter/test/widgets/grid_paper_test.dart index d64a14cfc7431..cd0c8d93bb802 100644 --- a/packages/flutter/test/widgets/grid_paper_test.dart +++ b/packages/flutter/test/widgets/grid_paper_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('GridPaper control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridPaper control test', (WidgetTester tester) async { await tester.pumpWidget(const GridPaper()); final List<Layer> layers1 = tester.layers; await tester.pumpWidget(const GridPaper()); diff --git a/packages/flutter/test/widgets/grid_view_layout_test.dart b/packages/flutter/test/widgets/grid_view_layout_test.dart index 221b96080b1be..ea1e43e725dd5 100644 --- a/packages/flutter/test/widgets/grid_view_layout_test.dart +++ b/packages/flutter/test/widgets/grid_view_layout_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Empty GridView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty GridView', (WidgetTester tester) async { final List<Widget> children = <Widget>[ const DecoratedBox(decoration: BoxDecoration()), const DecoratedBox(decoration: BoxDecoration()), diff --git a/packages/flutter/test/widgets/grid_view_test.dart b/packages/flutter/test/widgets/grid_view_test.dart index 1a4f29cedaf3a..03014dbd2984e 100644 --- a/packages/flutter/test/widgets/grid_view_test.dart +++ b/packages/flutter/test/widgets/grid_view_test.dart @@ -6,14 +6,14 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; import 'states.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('GridView.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -51,7 +51,7 @@ void main() { expect(finderCalled, true); }); - testWidgets('Empty GridView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty GridView', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -63,7 +63,7 @@ void main() { ); }); - testWidgets('GridView.count control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -136,7 +136,7 @@ void main() { log.clear(); }); - testWidgets('GridView.extent control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.extent control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -183,7 +183,7 @@ void main() { log.clear(); }); - testWidgets('GridView large scroll jump', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView large scroll jump', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( @@ -275,7 +275,7 @@ void main() { } }); - testWidgets('GridView - change crossAxisCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView - change crossAxisCount', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( @@ -346,7 +346,7 @@ void main() { expect(find.text('4'), findsNothing); }); - testWidgets('SliverGridRegularTileLayout - can handle close to zero mainAxisStride', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGridRegularTileLayout - can handle close to zero mainAxisStride', (WidgetTester tester) async { const SliverGridDelegateWithMaxCrossAxisExtent delegate = SliverGridDelegateWithMaxCrossAxisExtent( childAspectRatio: 1e300, maxCrossAxisExtent: 500.0, @@ -370,7 +370,7 @@ void main() { expect(layout.getMinChildIndexForScrollOffset(1000.0), 0.0); }); - testWidgets('GridView - change maxChildCrossAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView - change maxChildCrossAxisExtent', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( @@ -441,7 +441,7 @@ void main() { expect(find.text('4'), findsNothing); }); - testWidgets('One-line GridView paints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('One-line GridView paints', (WidgetTester tester) async { const Color green = Color(0xFF00FF00); final Container container = Container( @@ -470,7 +470,7 @@ void main() { expect(find.byType(GridView), isNot(paints..rect(color: green)..rect(color: green)..rect(color: green))); }); - testWidgets('GridView in zero context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView in zero context', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -491,7 +491,7 @@ void main() { expect(find.text('1'), findsNothing); }); - testWidgets('GridView in unbounded context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView in unbounded context', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -511,7 +511,7 @@ void main() { expect(find.text('19'), findsOneWidget); }); - testWidgets('GridView.builder control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder control test', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -532,7 +532,7 @@ void main() { expect(find.text('12'), findsNothing); }); - testWidgets('GridView.builder with undefined itemCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder with undefined itemCount', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -554,7 +554,7 @@ void main() { expect(find.text('13'), findsOneWidget); }); - testWidgets('GridView cross axis layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView cross axis layout', (WidgetTester tester) async { final Key target = UniqueKey(); Widget build(TextDirection textDirection) { @@ -580,7 +580,7 @@ void main() { expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 200.0)); }); - testWidgets('GridView crossAxisSpacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView crossAxisSpacing', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/27151. final Key target = UniqueKey(); @@ -608,7 +608,7 @@ void main() { expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 194.0)); }); - testWidgets('GridView does not cache itemBuilder calls', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView does not cache itemBuilder calls', (WidgetTester tester) async { final Map<int, int> counters = <int, int>{}; await tester.pumpWidget(Directionality( @@ -643,7 +643,7 @@ void main() { expect(counters[4], 2); }); - testWidgets('GridView does not report visual overflow unnecessarily', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView does not report visual overflow unnecessarily', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -664,9 +664,18 @@ void main() { final TestClipPaintingContext context = TestClipPaintingContext(); renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.none)); - }); - - testWidgets('GridView respects clipBehavior', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + notDisposedAllowList: <String, int?> { + // https://github.com/flutter/flutter/issues/134575 + 'OffsetLayer': 1, + // https://github.com/flutter/flutter/issues/134572 + 'ContainerLayer': 1, + }, + )); + + testWidgetsWithLeakTracking('GridView respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -730,9 +739,14 @@ void main() { // 4th, check that a non-default clip behavior can be sent to the painting context. renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.antiAlias)); - }); - - testWidgets('GridView.builder respects clipBehavior', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134572 + notDisposedAllowList: <String, int?> {'ContainerLayer': 1}, + )); + + testWidgetsWithLeakTracking('GridView.builder respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -748,7 +762,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('GridView.custom respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.custom respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -766,7 +780,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('GridView.count respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -781,7 +795,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('GridView.extent respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.extent respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -796,7 +810,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('SliverGridDelegateWithFixedCrossAxisCount mainAxisExtent works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGridDelegateWithFixedCrossAxisCount mainAxisExtent works as expected', (WidgetTester tester) async { const int crossAxisCount = 4; const double mainAxisExtent = 100.0; @@ -822,7 +836,7 @@ void main() { expect(tester.getSize(find.text('4')), equals(const Size(200.0, mainAxisExtent))); }); - testWidgets('SliverGridDelegateWithMaxCrossAxisExtent mainAxisExtent works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGridDelegateWithMaxCrossAxisExtent mainAxisExtent works as expected', (WidgetTester tester) async { const double maxCrossAxisExtent = 200.0; const double mainAxisExtent = 100.0; @@ -848,7 +862,7 @@ void main() { expect(tester.getSize(find.text('4')), equals(const Size(200.0, mainAxisExtent))); }); - testWidgets('SliverGridDelegateWithMaxCrossAxisExtent throws assertion error when maxCrossAxisExtent is 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGridDelegateWithMaxCrossAxisExtent throws assertion error when maxCrossAxisExtent is 0', (WidgetTester tester) async { const double maxCrossAxisExtent = 0; expect(() => Directionality( @@ -857,6 +871,44 @@ void main() { maxCrossAxisExtent: maxCrossAxisExtent, ), ), throwsAssertionError); + }); + + testWidgetsWithLeakTracking('SliverGrid sets correct extent for null returning builder delegate', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/130685 + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GridView.builder( + controller: controller, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemBuilder: (BuildContext context, int index) { + if (index == 12) { + return null; + } + return Container( + height: 100, + width: 100, + color: const Color(0xFFFF8A80), + alignment: Alignment.center, + child: Text('item ${index+1}'), + ); + }, + ), + )); + await tester.pumpAndSettle(); + expect(controller.position.maxScrollExtent, double.infinity); + expect(controller.position.pixels, 0.0); + await tester.fling(find.byType(GridView), const Offset(0.0, -1300.0), 100.0); + await tester.pumpAndSettle(); + // The actual extent of the children is 472.0. This should be reflected when + // the builder returns null (meaning we have reached the end). + expect(controller.position.maxScrollExtent, 472.0); + expect(controller.position.pixels, 472.0); }); } diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 463f5ba99c8d4..46d26c1ea10c0 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../painting/image_test_utils.dart' show TestImageProvider; @@ -190,7 +191,7 @@ Future<void> main() async { transitionFromUserGestures = false; }); - testWidgets('Heroes animate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes animate', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: routes)); @@ -300,7 +301,7 @@ Future<void> main() async { expect(find.byKey(thirdKey), isInCard); }); - testWidgets('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); final UniqueKey heroKey = UniqueKey(); await tester.pumpWidget( @@ -395,7 +396,7 @@ Future<void> main() async { expect(find.byKey(heroKey), findsNothing); }); - testWidgets('Heroes animate should hide original hero', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes animate should hide original hero', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: routes)); // Checks initial state. expect(find.byKey(firstKey), isOnstage); @@ -418,7 +419,7 @@ Future<void> main() async { expect(find.byKey(secondKey), isInCard); }); - testWidgets('Destination hero is rebuilt midflight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination hero is rebuilt midflight', (WidgetTester tester) async { final MutatingRoute route = MutatingRoute(); await tester.pumpWidget(MaterialApp( @@ -443,7 +444,7 @@ Future<void> main() async { await tester.pump(const Duration(seconds: 1)); }); - testWidgets('Heroes animation is fastOutSlowIn', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes animation is fastOutSlowIn', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: routes)); await tester.tap(find.text('two')); await tester.pump(); // begin navigation @@ -483,7 +484,7 @@ Future<void> main() async { ); }); - testWidgets('Heroes are not interactive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes are not interactive', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(MaterialApp( @@ -553,7 +554,7 @@ Future<void> main() async { expect(log, equals(<String>['bar'])); }); - testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<void>( @@ -574,7 +575,7 @@ Future<void> main() async { await tester.pump(); // ...and removes it straight away (since it's already at 0.0) }); - testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<void>( @@ -603,7 +604,7 @@ Future<void> main() async { await tester.pump(); }); - testWidgets('One route, two heroes, same tag, throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('One route, two heroes, same tag, throws', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: ListView( @@ -658,7 +659,7 @@ Future<void> main() async { ); }); - testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero push transition interrupted by a pop', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( routes: routes, )); @@ -723,7 +724,7 @@ Future<void> main() async { expect(find.byKey(secondKey), findsNothing); }); - testWidgets('Hero pop transition interrupted by a push', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero pop transition interrupted by a push', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( routes: routes, @@ -800,7 +801,7 @@ Future<void> main() async { expect(find.byKey(firstKey), findsNothing); }); - testWidgets('Destination hero disappears mid-flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination hero disappears mid-flight', (WidgetTester tester) async { const Key homeHeroKey = Key('home hero'); const Key routeHeroKey = Key('route hero'); bool routeIncludesHero = true; @@ -903,7 +904,7 @@ Future<void> main() async { }); - testWidgets('Destination hero scrolls mid-flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination hero scrolls mid-flight', (WidgetTester tester) async { const Key homeHeroKey = Key('home hero'); const Key routeHeroKey = Key('route hero'); const Key routeContainerKey = Key('route hero container'); @@ -990,7 +991,7 @@ Future<void> main() async { expect(finalHeroY, 75.0); // 100 less 25 for the scroll }); - testWidgets('Destination hero scrolls out of view mid-flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Destination hero scrolls out of view mid-flight', (WidgetTester tester) async { const Key homeHeroKey = Key('home hero'); const Key routeHeroKey = Key('route hero'); const Key routeContainerKey = Key('route hero container'); @@ -1067,7 +1068,7 @@ Future<void> main() async { expect(find.byKey(routeHeroKey), findsNothing); }); - testWidgets('Aborted flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Aborted flight', (WidgetTester tester) async { // See https://github.com/flutter/flutter/issues/5798 const Key heroABKey = Key('AB hero'); const Key heroBCKey = Key('BC hero'); @@ -1202,7 +1203,7 @@ Future<void> main() async { expect(tester.getTopLeft(find.byKey(heroBCKey)).dy, 0.0); }); - testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stateful hero child state survives flight', (WidgetTester tester) async { final MaterialPageRoute<void> route = MaterialPageRoute<void>( builder: (BuildContext context) { return Material( @@ -1286,7 +1287,7 @@ Future<void> main() async { }); - testWidgets('Hero createRectTween', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero createRectTween', (WidgetTester tester) async { RectTween createRectTween(Rect? begin, Rect? end) { return MaterialRectCenterArcTween(begin: begin, end: end); } @@ -1398,7 +1399,7 @@ Future<void> main() async { expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); }); - testWidgets('Hero createRectTween for Navigator that is not full screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero createRectTween for Navigator that is not full screen', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25272 RectTween createRectTween(Rect? begin, Rect? end) { @@ -1519,7 +1520,7 @@ Future<void> main() async { }); - testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pop interrupts push, reverses flight', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(routes: routes)); await tester.tap(find.text('twoInset')); await tester.pump(); // begin navigation from / to /twoInset. @@ -1612,7 +1613,7 @@ Future<void> main() async { expect(tester.getTopLeft(find.byKey(firstKey)).dx, x0); }); - testWidgets('Can override flight shuttle in to hero', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can override flight shuttle in to hero', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: ListView( @@ -1656,7 +1657,7 @@ Future<void> main() async { expect(find.text('baz'), findsOneWidget); }); - testWidgets('Can override flight shuttle in from hero', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can override flight shuttle in from hero', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: ListView( @@ -1699,7 +1700,7 @@ Future<void> main() async { }); // Regression test for https://github.com/flutter/flutter/issues/77720. - testWidgets("toHero's shuttle builder over fromHero's shuttle builder", (WidgetTester tester) async { + testWidgetsWithLeakTracking("toHero's shuttle builder over fromHero's shuttle builder", (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: ListView( @@ -1752,7 +1753,7 @@ Future<void> main() async { expect(find.text('toHero text'), findsOneWidget); }); - testWidgets('Can override flight launch pads', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can override flight launch pads', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: ListView( @@ -1799,7 +1800,7 @@ Future<void> main() async { expect(find.text('Joker'), findsOneWidget); }); - testWidgets('Heroes do not transition on back gestures by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes do not transition on back gestures by default', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( routes: routes, )); @@ -1838,7 +1839,7 @@ Future<void> main() async { expect(find.byKey(secondKey), isInCard); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Heroes can transition on gesture in one frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes can transition on gesture in one frame', (WidgetTester tester) async { transitionFromUserGestures = true; await tester.pumpWidget(MaterialApp( routes: routes, @@ -1881,7 +1882,7 @@ Future<void> main() async { expect(find.byKey(secondKey), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Heroes animate should hide destination hero and display original hero in case of dismissed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes animate should hide destination hero and display original hero in case of dismissed', (WidgetTester tester) async { transitionFromUserGestures = true; await tester.pumpWidget(MaterialApp( routes: routes, @@ -1917,7 +1918,7 @@ Future<void> main() async { expect(find.byKey(secondKey), isInCard); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Handles transitions when a non-default initial route is set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Handles transitions when a non-default initial route is set', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( routes: routes, initialRoute: '/two', @@ -1927,7 +1928,7 @@ Future<void> main() async { expect(find.text('three'), findsOneWidget); }); - testWidgets('Can push/pop on outer Navigator if nested Navigator contains Heroes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can push/pop on outer Navigator if nested Navigator contains Heroes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28042. const String heroTag = 'You are my hero!'; @@ -2001,7 +2002,7 @@ Future<void> main() async { expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget); }); - testWidgets('Can hero from route in root Navigator to route in nested Navigator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hero from route in root Navigator to route in nested Navigator', (WidgetTester tester) async { const String heroTag = 'foo'; final GlobalKey<NavigatorState> rootNavigator = GlobalKey(); final Key smallContainer = UniqueKey(); @@ -2087,7 +2088,7 @@ Future<void> main() async { expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100)); }); - testWidgets('Hero within a Hero, throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero within a Hero, throws', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -2105,7 +2106,7 @@ Future<void> main() async { expect(tester.takeException(), isAssertionError); }); - testWidgets('Can push/pop on outer Navigator if nested Navigators contains same Heroes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can push/pop on outer Navigator if nested Navigators contains same Heroes', (WidgetTester tester) async { const String heroTag = 'foo'; final GlobalKey<NavigatorState> rootNavigator = GlobalKey<NavigatorState>(); final Key rootRouteHero = UniqueKey(); @@ -2189,7 +2190,7 @@ Future<void> main() async { expect(find.byKey(nestedRouteHeroOne, skipOffstage: false), findsOneWidget); }); - testWidgets('Hero within a Hero subtree, throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero within a Hero subtree, throws', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -2207,7 +2208,7 @@ Future<void> main() async { expect(tester.takeException(), isAssertionError); }); - testWidgets('Hero within a Hero subtree with Builder, throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero within a Hero subtree with Builder, throws', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -2229,7 +2230,7 @@ Future<void> main() async { expect(tester.takeException(),isAssertionError); }); - testWidgets('Hero within a Hero subtree with LayoutBuilder, throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero within a Hero subtree with LayoutBuilder, throws', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -2251,7 +2252,7 @@ Future<void> main() async { expect(tester.takeException(), isAssertionError); }); - testWidgets('Heroes fly on pushReplacement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes fly on pushReplacement', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28041. const String heroTag = 'foo'; @@ -2338,7 +2339,7 @@ Future<void> main() async { expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100)); }); - testWidgets('On an iOS back swipe and snap, only a single flight should take place', (WidgetTester tester) async { + testWidgetsWithLeakTracking('On an iOS back swipe and snap, only a single flight should take place', (WidgetTester tester) async { int shuttlesBuilt = 0; Widget shuttleBuilder( BuildContext flightContext, @@ -2401,7 +2402,7 @@ Future<void> main() async { expect(shuttlesBuilt, 2); }); - testWidgets( + testWidgetsWithLeakTracking( "From hero's state should be preserved, " 'heroes work well with child widgets that has global keys', (WidgetTester tester) async { @@ -2468,7 +2469,7 @@ Future<void> main() async { }, ); - testWidgets( + testWidgetsWithLeakTracking( "Hero works with images that don't have both width and height specified", // Regression test for https://github.com/flutter/flutter/issues/32356 // and https://github.com/flutter/flutter/issues/31503 @@ -2556,7 +2557,7 @@ Future<void> main() async { ); // Regression test for https://github.com/flutter/flutter/issues/38183. - testWidgets('Remove user gesture driven flights when the gesture is invalid', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Remove user gesture driven flights when the gesture is invalid', (WidgetTester tester) async { transitionFromUserGestures = true; await tester.pumpWidget(MaterialApp( routes: routes, @@ -2585,7 +2586,7 @@ Future<void> main() async { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); // Regression test for https://github.com/flutter/flutter/issues/40239. - testWidgets( + testWidgetsWithLeakTracking( 'In a pop transition, when fromHero is null, the to hero should eventually become visible', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); @@ -2634,7 +2635,7 @@ Future<void> main() async { }, ); - testWidgets('popped hero uses fastOutSlowIn curve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('popped hero uses fastOutSlowIn curve', (WidgetTester tester) async { final Key container1 = UniqueKey(); final Key container2 = UniqueKey(); final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); @@ -2712,7 +2713,7 @@ Future<void> main() async { expect(heroSize, tween.transform(1.0)); }); - testWidgets('Heroes in enabled HeroMode do transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes in enabled HeroMode do transition', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Column( @@ -2783,7 +2784,7 @@ Future<void> main() async { expect(find.byKey(secondKey), isInCard); }); - testWidgets('Heroes in disabled HeroMode do not transition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Heroes in disabled HeroMode do not transition', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Column( @@ -2861,7 +2862,7 @@ Future<void> main() async { expect(find.byKey(secondKey), isOnstage); }); - testWidgets('kept alive Hero does not throw when the transition begins', (WidgetTester tester) async { + testWidgetsWithLeakTracking('kept alive Hero does not throw when the transition begins', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( @@ -2914,9 +2915,10 @@ Future<void> main() async { expect(find.byType(Placeholder), findsOneWidget); }); - testWidgets('toHero becomes unpaintable after the transition begins', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toHero becomes unpaintable after the transition begins', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); RenderAnimatedOpacity? findRenderAnimatedOpacity() { RenderObject? parent = tester.renderObject(find.byType(Placeholder)); @@ -2989,7 +2991,7 @@ Future<void> main() async { expect(find.byType(Placeholder), findsNothing); }); - testWidgets('diverting to a keepalive but unpaintable hero', (WidgetTester tester) async { + testWidgetsWithLeakTracking('diverting to a keepalive but unpaintable hero', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( @@ -3069,7 +3071,7 @@ Future<void> main() async { expect(tester.takeException(), isNull); }); - testWidgets('smooth transition between different incoming data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('smooth transition between different incoming data', (WidgetTester tester) async { addTearDown(tester.view.reset); final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); diff --git a/packages/flutter/test/widgets/hit_testing_test.dart b/packages/flutter/test/widgets/hit_testing_test.dart index bd4cd9377a99b..39ff67cca8950 100644 --- a/packages/flutter/test/widgets/hit_testing_test.dart +++ b/packages/flutter/test/widgets/hit_testing_test.dart @@ -6,16 +6,17 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('toString control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toString control test', (WidgetTester tester) async { await tester.pumpWidget(const Center(child: Text('Hello', textDirection: TextDirection.ltr))); final HitTestResult result = tester.hitTestOnBinding(Offset.zero); expect(result, hasOneLineDescription); expect(result.path.first, hasOneLineDescription); }); - testWidgets('A mouse click should only cause one hit test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A mouse click should only cause one hit test', (WidgetTester tester) async { int hitCount = 0; await tester.pumpWidget( _HitTestCounter( @@ -31,7 +32,7 @@ void main() { expect(hitCount, 1); }); - testWidgets('Non-mouse events should not cause movement hit tests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Non-mouse events should not cause movement hit tests', (WidgetTester tester) async { int hitCount = 0; await tester.pumpWidget( _HitTestCounter( diff --git a/packages/flutter/test/widgets/html_element_view_test.dart b/packages/flutter/test/widgets/html_element_view_test.dart index b485aef6c9235..dd8866e9faafe 100644 --- a/packages/flutter/test/widgets/html_element_view_test.dart +++ b/packages/flutter/test/widgets/html_element_view_test.dart @@ -6,20 +6,45 @@ library; import 'dart:async'; +import 'dart:ui_web' as ui_web; +import 'package:collection/collection.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/src/widgets/_html_element_view_web.dart' + show debugOverridePlatformViewRegistry; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:web/web.dart' as web; -import '../services/fake_platform_views.dart'; +final Object _mockHtmlElement = Object(); +Object _mockViewFactory(int id, {Object? params}) { + return _mockHtmlElement; +} void main() { + late FakePlatformViewRegistry fakePlatformViewRegistry; + + setUp(() { + fakePlatformViewRegistry = FakePlatformViewRegistry(); + + // Simulate the engine registering default factores. + fakePlatformViewRegistry.registerViewFactory(ui_web.PlatformViewRegistry.defaultVisibleViewType, (int viewId, {Object? params}) { + params!; + params as Map<Object?, Object?>; + return web.document.createElement(params['tagName']! as String); + }); + fakePlatformViewRegistry.registerViewFactory(ui_web.PlatformViewRegistry.defaultInvisibleViewType, (int viewId, {Object? params}) { + params!; + params as Map<Object?, Object?>; + return web.document.createElement(params['tagName']! as String); + }); + }); + group('HtmlElementView', () { testWidgets('Create HTML view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( @@ -32,17 +57,16 @@ void main() { ); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Create HTML view with PlatformViewCreatedCallback', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); bool hasPlatformViewCreated = false; void onPlatformViewCreatedCallBack(int id) { @@ -66,17 +90,16 @@ void main() { expect(hasPlatformViewCreated, true); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Create HTML view with creation params', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Column( children: <Widget>[ @@ -101,18 +124,17 @@ void main() { ); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 1, 'webview', 'foobar'), - FakeHtmlPlatformView(currentViewId + 2, 'webview', 123), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 1, viewType: 'webview', params: 'foobar', htmlElement: _mockHtmlElement), + (id: currentViewId + 2, viewType: 'webview', params: 123, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Resize HTML view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -123,7 +145,7 @@ void main() { ), ); - viewsController.resizeCompleter = Completer<void>(); + final Completer<void> resizeCompleter = Completer<void>(); await tester.pumpWidget( const Center( @@ -135,22 +157,21 @@ void main() { ), ); - viewsController.resizeCompleter.complete(); + resizeCompleter.complete(); await tester.pump(); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Change HTML view type', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); - viewsController.registerViewType('maps'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); + fakePlatformViewRegistry.registerViewFactory('maps', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -172,16 +193,15 @@ void main() { ); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 2, 'maps'), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 2, viewType: 'maps', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Dispose HTML view', (WidgetTester tester) async { - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -202,15 +222,14 @@ void main() { ); expect( - viewsController.views, + fakePlatformViewRegistry.views, isEmpty, ); }); testWidgets('HTML view survives widget tree change', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); final GlobalKey key = GlobalKey(); await tester.pumpWidget( Center( @@ -233,9 +252,9 @@ void main() { ); expect( - viewsController.views, - unorderedEquals(<FakeHtmlPlatformView>[ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals(<FakePlatformView>[ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); @@ -244,8 +263,7 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); expect(currentViewId, greaterThanOrEqualTo(0)); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( Semantics( @@ -278,4 +296,206 @@ void main() { handle.dispose(); }); }); + + group('HtmlElementView.fromTagName', () { + setUp(() { + debugOverridePlatformViewRegistry = fakePlatformViewRegistry; + }); + + tearDown(() { + debugOverridePlatformViewRegistry = null; + }); + + testWidgets('Create platform view from tagName', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName(tagName: 'div'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + expect(fakePlatformView.id, currentViewId + 1); + expect(fakePlatformView.viewType, ui_web.PlatformViewRegistry.defaultVisibleViewType); + expect(fakePlatformView.params, <dynamic, dynamic>{'tagName': 'div'}); + + // The HTML element should be a div. + final web.HTMLElement htmlElement = fakePlatformView.htmlElement as web.HTMLElement; + expect(htmlElement.tagName, equalsIgnoringCase('div')); + }); + + testWidgets('Create invisible platform view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName(tagName: 'script', isVisible: false), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + expect(fakePlatformView.id, currentViewId + 1); + // The view should be invisible. + expect(fakePlatformView.viewType, ui_web.PlatformViewRegistry.defaultInvisibleViewType); + expect(fakePlatformView.params, <dynamic, dynamic>{'tagName': 'script'}); + + // The HTML element should be a script. + final web.HTMLElement htmlElement = fakePlatformView.htmlElement as web.HTMLElement; + expect(htmlElement.tagName, equalsIgnoringCase('script')); + }); + + testWidgets('onElementCreated', (WidgetTester tester) async { + final List<Object> createdElements = <Object>[]; + void onElementCreated(Object element) { + createdElements.add(element); + } + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName( + tagName: 'table', + onElementCreated: onElementCreated, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + + expect(createdElements, hasLength(1)); + final Object createdElement = createdElements.single; + + expect(createdElement, fakePlatformView.htmlElement); + }); + }); +} + +typedef FakeViewFactory = ({ + String viewType, + bool isVisible, + Function viewFactory, +}); + +typedef FakePlatformView = ({ + int id, + String viewType, + Object? params, + Object htmlElement, +}); + +class FakePlatformViewRegistry implements ui_web.PlatformViewRegistry { + FakePlatformViewRegistry() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall); + } + + Set<FakePlatformView> get views => Set<FakePlatformView>.unmodifiable(_views); + final Set<FakePlatformView> _views = <FakePlatformView>{}; + + final Set<FakeViewFactory> _registeredViewTypes = <FakeViewFactory>{}; + + @override + bool registerViewFactory(String viewType, Function viewFactory, {bool isVisible = true}) { + if (_findRegisteredViewFactory(viewType) != null) { + return false; + } + _registeredViewTypes.add(( + viewType: viewType, + isVisible: isVisible, + viewFactory: viewFactory, + )); + return true; + } + + @override + Object getViewById(int viewId) { + return _findViewById(viewId)!.htmlElement; + } + + FakeViewFactory? _findRegisteredViewFactory(String viewType) { + return _registeredViewTypes.singleWhereOrNull( + (FakeViewFactory registered) => registered.viewType == viewType, + ); + } + + FakePlatformView? _findViewById(int viewId) { + return _views.singleWhereOrNull( + (FakePlatformView view) => view.id == viewId, + ); + } + + Future<dynamic> _onMethodCall(MethodCall call) { + switch (call.method) { + case 'create': + return _create(call); + case 'dispose': + return _dispose(call); + } + return Future<dynamic>.sync(() => null); + } + + Future<dynamic> _create(MethodCall call) async { + final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>; + final int id = args['id'] as int; + final String viewType = args['viewType'] as String; + final Object? params = args['params']; + + if (_findViewById(id) != null) { + throw PlatformException( + code: 'error', + message: 'Trying to create an already created platform view, view id: $id', + ); + } + + final FakeViewFactory? registered = _findRegisteredViewFactory(viewType); + if (registered == null) { + throw PlatformException( + code: 'error', + message: 'Trying to create a platform view of unregistered type: $viewType', + ); + } + + final ui_web.ParameterizedPlatformViewFactory viewFactory = + registered.viewFactory as ui_web.ParameterizedPlatformViewFactory; + + _views.add(( + id: id, + viewType: viewType, + params: params, + htmlElement: viewFactory(id, params: params), + )); + return null; + } + + Future<dynamic> _dispose(MethodCall call) async { + final int id = call.arguments as int; + + final FakePlatformView? view = _findViewById(id); + if (view == null) { + throw PlatformException( + code: 'error', + message: 'Trying to dispose a platform view with unknown id: $id', + ); + } + + _views.remove(view); + return null; + } } diff --git a/packages/flutter/test/widgets/hyperlink_test.dart b/packages/flutter/test/widgets/hyperlink_test.dart index 4e303befd1d2d..d35d45ed5b626 100644 --- a/packages/flutter/test/widgets/hyperlink_test.dart +++ b/packages/flutter/test/widgets/hyperlink_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can tap a hyperlink', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can tap a hyperlink', (WidgetTester tester) async { bool didTapLeft = false; final TapGestureRecognizer tapLeft = TapGestureRecognizer() ..onTap = () { diff --git a/packages/flutter/test/widgets/icon_test.dart b/packages/flutter/test/widgets/icon_test.dart index 4b53d670a33ed..bdb690995aecf 100644 --- a/packages/flutter/test/widgets/icon_test.dart +++ b/packages/flutter/test/widgets/icon_test.dart @@ -6,11 +6,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Can set opacity for an Icon', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set opacity for an Icon', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -27,7 +28,7 @@ void main() { expect(text.text.style!.color, const Color(0xFF666666).withOpacity(0.5)); }); - testWidgets('Icon sizing - no theme, default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon sizing - no theme, default size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -41,7 +42,7 @@ void main() { expect(renderObject.size, equals(const Size.square(24.0))); }); - testWidgets('Icon sizing - no theme, explicit size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon sizing - no theme, explicit size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -58,7 +59,7 @@ void main() { expect(renderObject.size, equals(const Size.square(96.0))); }); - testWidgets('Icon sizing - sized theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon sizing - sized theme', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -75,7 +76,7 @@ void main() { expect(renderObject.size, equals(const Size.square(36.0))); }); - testWidgets('Icon sizing - sized theme, explicit size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon sizing - sized theme, explicit size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -95,7 +96,7 @@ void main() { expect(renderObject.size, equals(const Size.square(48.0))); }); - testWidgets('Icon sizing - sizeless theme, default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon sizing - sizeless theme, default size', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -113,7 +114,7 @@ void main() { }); - testWidgets('Icon with custom font', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon with custom font', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -127,7 +128,7 @@ void main() { expect(richText.text.style!.fontFamily, equals('Roboto')); }); - testWidgets('Icon with custom fontFamilyFallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon with custom fontFamilyFallback', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -141,7 +142,7 @@ void main() { expect(richText.text.style!.fontFamilyFallback, equals(<String>['FallbackFont'])); }); - testWidgets('Icon with semantic label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon with semantic label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -161,7 +162,7 @@ void main() { semantics.dispose(); }); - testWidgets('Null icon with semantic label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Null icon with semantic label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -181,7 +182,7 @@ void main() { semantics.dispose(); }); - testWidgets("Changing semantic label from null doesn't rebuild tree ", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Changing semantic label from null doesn't rebuild tree ", (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -212,7 +213,7 @@ void main() { expect(richText2, same(richText1)); }); - testWidgets('IconData comparison', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IconData comparison', (WidgetTester tester) async { expect(const IconData(123), const IconData(123)); expect(const IconData(123), isNot(const IconData(123, matchTextDirection: true))); expect(const IconData(123), isNot(const IconData(123, fontFamily: 'f'))); @@ -225,7 +226,7 @@ void main() { }); - testWidgets('Fill, weight, grade, and optical size variations are passed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fill, weight, grade, and optical size variations are passed', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -258,7 +259,7 @@ void main() { ]); }); - testWidgets('Fill, weight, grade, and optical size can be set at the theme-level', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fill, weight, grade, and optical size can be set at the theme-level', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -283,7 +284,7 @@ void main() { ]); }); - testWidgets('Theme-level fill, weight, grade, and optical size can be overridden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Theme-level fill, weight, grade, and optical size can be overridden', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/image_filter_quality_test.dart b/packages/flutter/test/widgets/image_filter_quality_test.dart index d8b1ee367b0f8..5025b25e1867e 100644 --- a/packages/flutter/test/widgets/image_filter_quality_test.dart +++ b/packages/flutter/test/widgets/image_filter_quality_test.dart @@ -13,17 +13,18 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Image at default filterQuality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image at default filterQuality', (WidgetTester tester) async { await testImageQuality(tester, null); }); - testWidgets('Image at high filterQuality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image at high filterQuality', (WidgetTester tester) async { await testImageQuality(tester, ui.FilterQuality.high); }); - testWidgets('Image at none filterQuality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image at none filterQuality', (WidgetTester tester) async { await testImageQuality(tester, ui.FilterQuality.none); }); } @@ -137,7 +138,7 @@ class _TestImageProvider extends ImageProvider<Object> { } @override - ImageStreamCompleter load(Object key, DecoderCallback decode) { + ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) { _loadCallCount += 1; return _streamCompleter; } diff --git a/packages/flutter/test/widgets/image_filter_test.dart b/packages/flutter/test/widgets/image_filter_test.dart index 29da67bae4d7a..0cad97b4c46b7 100644 --- a/packages/flutter/test/widgets/image_filter_test.dart +++ b/packages/flutter/test/widgets/image_filter_test.dart @@ -14,9 +14,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Image filter - blur', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - blur', (WidgetTester tester) async { await tester.pumpWidget( RepaintBoundary( child: ImageFiltered( @@ -31,7 +32,7 @@ void main() { ); }); - testWidgets('Image filter - blur with offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - blur with offset', (WidgetTester tester) async { final Key key = GlobalKey(); await tester.pumpWidget( RepaintBoundary( @@ -51,7 +52,7 @@ void main() { ); }); - testWidgets('Image filter - dilate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - dilate', (WidgetTester tester) async { await tester.pumpWidget( RepaintBoundary( child: ImageFiltered( @@ -66,7 +67,7 @@ void main() { ); }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/101874 - testWidgets('Image filter - erode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - erode', (WidgetTester tester) async { await tester.pumpWidget( RepaintBoundary( child: ImageFiltered( @@ -82,7 +83,7 @@ void main() { ); }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/101874 - testWidgets('Image filter - matrix', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - matrix', (WidgetTester tester) async { final ImageFilter matrix = ImageFilter.matrix(Float64List.fromList(<double>[ 0.5, 0.0, 0.0, 0.0, // 0.0, 0.5, 0.0, 0.0, // @@ -119,7 +120,7 @@ void main() { ); }); - testWidgets('Image filter - matrix with offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - matrix with offset', (WidgetTester tester) async { final Matrix4 matrix = Matrix4.rotationZ(pi / 18); final ImageFilter matrixFilter = ImageFilter.matrix(matrix.storage); final Key key = GlobalKey(); @@ -157,7 +158,7 @@ void main() { ); }); - testWidgets('Image filter - reuses its layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - reuses its layer', (WidgetTester tester) async { Future<void> pumpWithSigma(double sigma) async { await tester.pumpWidget( RepaintBoundary( @@ -178,7 +179,7 @@ void main() { expect(renderObject.debugLayer, same(originalLayer)); }); - testWidgets('Image filter - enabled and disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image filter - enabled and disabled', (WidgetTester tester) async { Future<void> pumpWithEnabledState(bool enabled) async { await tester.pumpWidget( RepaintBoundary( diff --git a/packages/flutter/test/widgets/image_headers_test.dart b/packages/flutter/test/widgets/image_headers_test.dart index f00f00fcdaecb..2d833f49e0205 100644 --- a/packages/flutter/test/widgets/image_headers_test.dart +++ b/packages/flutter/test/widgets/image_headers_test.dart @@ -7,13 +7,14 @@ import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; void main() { final MockHttpClient client = MockHttpClient(); - testWidgets('Headers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Headers', (WidgetTester tester) async { HttpOverrides.runZoned<Future<void>>(() async { await tester.pumpWidget(Image.network( 'https://www.example.com/images/frame.png', diff --git a/packages/flutter/test/widgets/image_icon_test.dart b/packages/flutter/test/widgets/image_icon_test.dart index 0d4709406a9c8..2aa1d73290341 100644 --- a/packages/flutter/test/widgets/image_icon_test.dart +++ b/packages/flutter/test/widgets/image_icon_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../painting/mocks_for_image_cache.dart'; @@ -20,7 +21,7 @@ void main() { ); }); - testWidgets('ImageIcon sizing - no theme, default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon sizing - no theme, default size', (WidgetTester tester) async { await tester.pumpWidget( Center( child: ImageIcon(image), @@ -32,7 +33,7 @@ void main() { expect(find.byType(Image), findsOneWidget); }); - testWidgets('Icon opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Icon opacity', (WidgetTester tester) async { await tester.pumpWidget( Center( child: IconTheme( @@ -45,7 +46,7 @@ void main() { expect(tester.widget<Image>(find.byType(Image)).color!.alpha, equals(128)); }); - testWidgets('ImageIcon sizing - no theme, explicit size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon sizing - no theme, explicit size', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: ImageIcon( @@ -59,7 +60,7 @@ void main() { expect(renderObject.size, equals(const Size.square(96.0))); }); - testWidgets('ImageIcon sizing - sized theme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon sizing - sized theme', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: IconTheme( @@ -73,7 +74,7 @@ void main() { expect(renderObject.size, equals(const Size.square(36.0))); }); - testWidgets('ImageIcon sizing - sized theme, explicit size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon sizing - sized theme, explicit size', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: IconTheme( @@ -90,7 +91,7 @@ void main() { expect(renderObject.size, equals(const Size.square(48.0))); }); - testWidgets('ImageIcon sizing - sizeless theme, default size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon sizing - sizeless theme, default size', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: IconTheme( @@ -104,7 +105,7 @@ void main() { expect(renderObject.size, equals(const Size.square(24.0))); }); - testWidgets('ImageIcon has semantics data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ImageIcon has semantics data', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( const Directionality( diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index 46f0478058d59..afacf9ae92283 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; @@ -155,6 +156,7 @@ void main() { const String image = 'assets/image.png'; final Map<double, ui.Image> images = <double, ui.Image>{}; + setUpAll(() async { for (final double scale in const <double>[0.5, 1.0, 1.5, 2.0, 4.0, 10.0]) { final int dimension = (48 * scale).floor(); @@ -162,7 +164,13 @@ void main() { } }); - testWidgets('Image for device pixel ratio 1.0', (WidgetTester tester) async { + tearDownAll(() { + for (final ui.Image image in images.values) { + image.dispose(); + } + }); + + testWidgetsWithLeakTracking('Image for device pixel ratio 1.0', (WidgetTester tester) async { const double ratio = 1.0; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -174,7 +182,7 @@ void main() { expect(getRenderImage(tester, key).scale, 1.0); }); - testWidgets('Image for device pixel ratio 0.5', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 0.5', (WidgetTester tester) async { const double ratio = 0.5; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -186,7 +194,7 @@ void main() { expect(getRenderImage(tester, key).scale, 1.0); }); - testWidgets('Image for device pixel ratio 1.5', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 1.5', (WidgetTester tester) async { const double ratio = 1.5; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -201,7 +209,7 @@ void main() { // A 1.75 DPR screen is typically a low-resolution screen, such that physical // pixels are visible to the user. For such screens we prefer to pick the // higher resolution image, if available. - testWidgets('Image for device pixel ratio 1.75', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 1.75', (WidgetTester tester) async { const double ratio = 1.75; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -213,7 +221,7 @@ void main() { expect(getRenderImage(tester, key).scale, 2.0); }); - testWidgets('Image for device pixel ratio 2.3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 2.3', (WidgetTester tester) async { const double ratio = 2.3; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -225,7 +233,7 @@ void main() { expect(getRenderImage(tester, key).scale, 2.0); }); - testWidgets('Image for device pixel ratio 3.7', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 3.7', (WidgetTester tester) async { const double ratio = 3.7; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -237,7 +245,7 @@ void main() { expect(getRenderImage(tester, key).scale, 4.0); }); - testWidgets('Image for device pixel ratio 5.1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 5.1', (WidgetTester tester) async { const double ratio = 5.1; Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images)); @@ -249,7 +257,7 @@ void main() { expect(getRenderImage(tester, key).scale, 4.0); }); - testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async { // If both a main asset and a 1.0x asset are specified, then prefer // the 1.0x asset. @@ -279,19 +287,19 @@ void main() { expect(getRenderImage(tester, key).image!.height, 480); }); - testWidgets('Image cache resize upscale display 5', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image cache resize upscale display 5', (WidgetTester tester) async { final Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 20, 20)); expect(getRenderImage(tester, key).size, const Size(5.0, 5.0)); }); - testWidgets('Image cache resize upscale display 50', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image cache resize upscale display 50', (WidgetTester tester) async { final Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 50, 50, 20, 20)); expect(getRenderImage(tester, key).size, const Size(50.0, 50.0)); }); - testWidgets('Image cache resize downscale display 5', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image cache resize downscale display 5', (WidgetTester tester) async { final Key key = GlobalKey(); await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 1, 1)); expect(getRenderImage(tester, key).size, const Size(5.0, 5.0)); @@ -301,7 +309,7 @@ void main() { // visible physical pixel size (see the test for 1.75 DPR above). However, // if higher resolution assets are not available we will pick the best // available. - testWidgets('Low-resolution assets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Low-resolution assets', (WidgetTester tester) async { const Map<Object?, Object?> manifest = <Object?, Object?>{ 'assets/image.png': <Map<String, Object>>[ <String, Object>{'asset': 'assets/image.png'}, diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart index 6d574d524ee1e..905ba88d4aa4b 100644 --- a/packages/flutter/test/widgets/image_rtl_test.dart +++ b/packages/flutter/test/widgets/image_rtl_test.dart @@ -7,8 +7,7 @@ import 'dart:ui' as ui show Image; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestImageProvider extends ImageProvider<TestImageProvider> { const TestImageProvider(this.image); @@ -21,7 +20,7 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { } @override - ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) { + ImageStreamCompleter loadImage(TestImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter( SynchronousFuture<ImageInfo>(ImageInfo(image: image)), ); @@ -30,10 +29,16 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { void main() { late ui.Image testImage; + setUpAll(() async { testImage = await createTestImage(width: 16, height: 9); }); - testWidgets('DecorationImage RTL with alignment topEnd and match', (WidgetTester tester) async { + + tearDownAll(() { + testImage.dispose(); + }); + + testWidgetsWithLeakTracking('DecorationImage RTL with alignment topEnd and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -72,7 +77,7 @@ void main() { expect(find.byType(Container), isNot(paints..scale()..scale())); }); - testWidgets('DecorationImage LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -108,7 +113,7 @@ void main() { expect(find.byType(Container), isNot(paints..scale())); }); - testWidgets('DecorationImage RTL with alignment topEnd', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage RTL with alignment topEnd', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -143,7 +148,7 @@ void main() { expect(find.byType(Container), isNot(paints..scale())); }); - testWidgets('DecorationImage LTR with alignment topEnd', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage LTR with alignment topEnd', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -178,7 +183,7 @@ void main() { expect(find.byType(Container), isNot(paints..scale())); }); - testWidgets('DecorationImage RTL with alignment center-right and match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage RTL with alignment center-right and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -210,7 +215,7 @@ void main() { expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('DecorationImage RTL with alignment center-right and no match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage RTL with alignment center-right and no match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -237,7 +242,7 @@ void main() { expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('DecorationImage LTR with alignment center-right and match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage LTR with alignment center-right and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -265,7 +270,7 @@ void main() { expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('DecorationImage LTR with alignment center-right and no match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationImage LTR with alignment center-right and no match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -293,7 +298,7 @@ void main() { expect(find.byType(Container), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('Image RTL with alignment topEnd and match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image RTL with alignment topEnd and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -330,7 +335,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..scale()..scale())); }); - testWidgets('Image LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image LTR with alignment topEnd (and pointless match)', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -364,7 +369,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..scale())); }); - testWidgets('Image RTL with alignment topEnd', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image RTL with alignment topEnd', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -397,7 +402,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..scale())); }); - testWidgets('Image LTR with alignment topEnd', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image LTR with alignment topEnd', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -430,7 +435,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..scale())); }); - testWidgets('Image RTL with alignment center-right and match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image RTL with alignment center-right and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -458,7 +463,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('Image RTL with alignment center-right and no match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image RTL with alignment center-right and no match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -483,7 +488,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('Image LTR with alignment center-right and match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image LTR with alignment center-right and match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -509,7 +514,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('Image LTR with alignment center-right and no match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image LTR with alignment center-right and no match', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -535,7 +540,7 @@ void main() { expect(find.byType(SizedBox), isNot(paints..drawImageRect()..drawImageRect())); }); - testWidgets('Image - Switch needing direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Image - Switch needing direction', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index ba06c7b42169a..cbabca138f9b4 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -501,12 +501,12 @@ void main() { final _TestImageProvider imageProvider = _TestImageProvider(); await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true)); final State<Image> image = tester.state/*State<Image>*/(find.byType(Image)); - expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)')); + expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners, 0 ephemeralErrorListeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)')); imageProvider.complete(image100x100); await tester.pump(); - expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 1 listener), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); + expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 1 listener, 0 ephemeralErrorListeners), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); await tester.pumpWidget(Container()); - expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners), pixels: null, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); + expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners, 0 ephemeralErrorListeners), pixels: null, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); }); testWidgets('Stream completer errors can be listened to by attaching before resolving', (WidgetTester tester) async { @@ -2083,7 +2083,7 @@ class _TestImageProvider extends ImageProvider<Object> { } @override - ImageStreamCompleter load(Object key, DecoderCallback decode) { + ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) { _loadCallCount += 1; return _streamCompleter; } @@ -2198,7 +2198,7 @@ class _FailingImageProvider extends ImageProvider<int> { } @override - ImageStreamCompleter load(int key, DecoderCallback decode) { + ImageStreamCompleter loadImage(int key, ImageDecoderCallback decode) { if (failOnLoad) { throw throws; } diff --git a/packages/flutter/test/widgets/implicit_animations_test.dart b/packages/flutter/test/widgets/implicit_animations_test.dart index 4059687665586..d71b942d23dba 100644 --- a/packages/flutter/test/widgets/implicit_animations_test.dart +++ b/packages/flutter/test/widgets/implicit_animations_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class MockOnEndFunction { int called = 0; @@ -24,7 +25,7 @@ void main() { mockOnEndFunction = MockOnEndFunction(); }); - testWidgets('BoxConstraintsTween control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('BoxConstraintsTween control test', (WidgetTester tester) async { final BoxConstraintsTween tween = BoxConstraintsTween( begin: BoxConstraints.tight(const Size(20.0, 50.0)), end: BoxConstraints.tight(const Size(10.0, 30.0)), @@ -36,7 +37,7 @@ void main() { expect(result.maxHeight, 45.0); }); - testWidgets('DecorationTween control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DecorationTween control test', (WidgetTester tester) async { final DecorationTween tween = DecorationTween( begin: const BoxDecoration(color: Color(0xFF00FF00)), end: const BoxDecoration(color: Color(0xFFFFFF00)), @@ -45,7 +46,7 @@ void main() { expect(result.color, const Color(0xFF3FFF00)); }); - testWidgets('EdgeInsetsTween control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EdgeInsetsTween control test', (WidgetTester tester) async { final EdgeInsetsTween tween = EdgeInsetsTween( begin: const EdgeInsets.symmetric(vertical: 50.0), end: const EdgeInsets.only(top: 10.0, bottom: 30.0), @@ -57,7 +58,7 @@ void main() { expect(result.bottom, 45.0); }); - testWidgets('Matrix4Tween control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Matrix4Tween control test', (WidgetTester tester) async { final Matrix4Tween tween = Matrix4Tween( begin: Matrix4.translationValues(10.0, 20.0, 30.0), end: Matrix4.translationValues(14.0, 24.0, 34.0), @@ -66,7 +67,7 @@ void main() { expect(result, equals(Matrix4.translationValues(11.0, 21.0, 31.0))); }); - testWidgets('AnimatedContainer onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedContainer onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -89,7 +90,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedPadding onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPadding onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -112,7 +113,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedAlign onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedAlign onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -135,7 +136,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedPositioned onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositioned onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -158,7 +159,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedPositionedDirectional onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPositionedDirectional onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -181,7 +182,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedSlide onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSlide onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -203,7 +204,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedSlide transition test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedSlide transition test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( switchKey: switchKey, @@ -241,7 +242,7 @@ void main() { expect(state.builds, equals(2)); }); - testWidgets('AnimatedScale onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedScale onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -263,7 +264,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedScale transition test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedScale transition test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( switchKey: switchKey, @@ -301,7 +302,7 @@ void main() { expect(state.builds, equals(2)); }); - testWidgets('AnimatedRotation onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedRotation onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -323,7 +324,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedRotation transition test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedRotation transition test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( switchKey: switchKey, @@ -361,7 +362,7 @@ void main() { expect(state.builds, equals(2)); }); - testWidgets('AnimatedOpacity onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedOpacity onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -383,7 +384,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedOpacity transition test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedOpacity transition test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( switchKey: switchKey, @@ -421,7 +422,7 @@ void main() { expect(state.builds, equals(2)); }); - testWidgets('AnimatedFractionallySizedBox onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedFractionallySizedBox onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -443,7 +444,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('SliverAnimatedOpacity onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAnimatedOpacity onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(TestAnimatedWidget( callback: mockOnEndFunction.handler, switchKey: switchKey, @@ -464,7 +465,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('SliverAnimatedOpacity transition test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAnimatedOpacity transition test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( switchKey: switchKey, @@ -502,7 +503,7 @@ void main() { expect(state.builds, equals(2)); }); - testWidgets('AnimatedDefaultTextStyle onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedDefaultTextStyle onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -525,7 +526,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedPhysicalModel onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedPhysicalModel onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -548,7 +549,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('TweenAnimationBuilder onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TweenAnimationBuilder onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -571,7 +572,7 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('AnimatedTheme onEnd callback test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedTheme onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( child: TestAnimatedWidget( callback: mockOnEndFunction.handler, @@ -594,11 +595,12 @@ void main() { await tapTest2and3(tester, widgetFinder, mockOnEndFunction); }); - testWidgets('Ensure CurvedAnimations are disposed on widget change', + testWidgetsWithLeakTracking('Ensure CurvedAnimations are disposed on widget change', (WidgetTester tester) async { final GlobalKey<ImplicitlyAnimatedWidgetState<AnimatedOpacity>> key = GlobalKey<ImplicitlyAnimatedWidgetState<AnimatedOpacity>>(); final ValueNotifier<Curve> curve = ValueNotifier<Curve>(const Interval(0.0, 0.5)); + addTearDown(curve.dispose); await tester.pumpWidget(wrap( child: ValueListenableBuilder<Curve>( valueListenable: curve, diff --git a/packages/flutter/test/widgets/implicit_semantics_test.dart b/packages/flutter/test/widgets/implicit_semantics_test.dart index 42ef79223539a..41d6ac7e21fbd 100644 --- a/packages/flutter/test/widgets/implicit_semantics_test.dart +++ b/packages/flutter/test/widgets/implicit_semantics_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Implicit Semantics merge behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Implicit Semantics merge behavior', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -172,7 +173,7 @@ void main() { semantics.dispose(); }); - testWidgets('Do not merge with conflicts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not merge with conflicts', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/inherited_dependencies_test.dart b/packages/flutter/test/widgets/inherited_dependencies_test.dart index 2ed743e47cc07..22840f52892f8 100644 --- a/packages/flutter/test/widgets/inherited_dependencies_test.dart +++ b/packages/flutter/test/widgets/inherited_dependencies_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/src/widgets/basic.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('InheritedWidget dependencies show up in diagnostic properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedWidget dependencies show up in diagnostic properties', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(Directionality( key: key, diff --git a/packages/flutter/test/widgets/inherited_model_test.dart b/packages/flutter/test/widgets/inherited_model_test.dart index 6753361cb42e4..b1ae5ef2f69c9 100644 --- a/packages/flutter/test/widgets/inherited_model_test.dart +++ b/packages/flutter/test/widgets/inherited_model_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // A simple "flat" InheritedModel: the data model is just 3 integer // valued fields: a, b, c. @@ -73,7 +74,7 @@ class _ShowABCFieldState extends State<ShowABCField> { } void main() { - testWidgets('InheritedModel basics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedModel basics', (WidgetTester tester) async { int a = 0; int b = 1; int c = 2; @@ -189,7 +190,7 @@ void main() { expect(find.text('a: 2 b: 2 c: 3'), findsOneWidget); }); - testWidgets('Looking up an non existent InheritedModel ancestor returns null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Looking up an non existent InheritedModel ancestor returns null', (WidgetTester tester) async { ABCModel? inheritedModel; await tester.pumpWidget( @@ -205,7 +206,7 @@ void main() { expect(inheritedModel, null); }); - testWidgets('Inner InheritedModel shadows the outer one', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inner InheritedModel shadows the outer one', (WidgetTester tester) async { int a = 0; int b = 1; int c = 2; @@ -323,7 +324,7 @@ void main() { expect(find.text('a: 102 b: 102 c: null'), findsOneWidget); }); - testWidgets('InheritedModel inner models supported aspect change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedModel inner models supported aspect change', (WidgetTester tester) async { int a = 0; int b = 1; int c = 2; diff --git a/packages/flutter/test/widgets/inherited_test.dart b/packages/flutter/test/widgets/inherited_test.dart index e1c9b8c83f011..478ed93d419d4 100644 --- a/packages/flutter/test/widgets/inherited_test.dart +++ b/packages/flutter/test/widgets/inherited_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -55,7 +56,7 @@ class ChangeNotifierInherited extends InheritedNotifier<ChangeNotifier> { } void main() { - testWidgets('Inherited notifies dependents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited notifies dependents', (WidgetTester tester) async { final List<TestInherited> log = <TestInherited>[]; final Builder builder = Builder( @@ -81,7 +82,7 @@ void main() { expect(log, equals(<TestInherited>[first, third])); }); - testWidgets('Update inherited when reparenting state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update inherited when reparenting state', (WidgetTester tester) async { final GlobalKey globalKey = GlobalKey(); final List<TestInherited> log = <TestInherited>[]; @@ -111,7 +112,7 @@ void main() { expect(log, equals(<TestInherited>[first, second])); }); - testWidgets('Update inherited when removing node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update inherited when removing node', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -166,7 +167,7 @@ void main() { log.clear(); }); - testWidgets('Update inherited when removing node and child has global key', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update inherited when removing node and child has global key', (WidgetTester tester) async { final List<String> log = <String>[]; @@ -230,7 +231,7 @@ void main() { log.clear(); }); - testWidgets('Update inherited when removing node and child has global key with constant child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update inherited when removing node and child has global key with constant child', (WidgetTester tester) async { final List<int> log = <int>[]; final Key key = GlobalKey(); @@ -289,7 +290,7 @@ void main() { log.clear(); }); - testWidgets('Update inherited when removing node and child has global key with constant child, minimised', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Update inherited when removing node and child has global key with constant child, minimised', (WidgetTester tester) async { final List<int> log = <int>[]; @@ -336,7 +337,7 @@ void main() { log.clear(); }); - testWidgets('Inherited widget notifies descendants when descendant previously failed to find a match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited widget notifies descendants when descendant previously failed to find a match', (WidgetTester tester) async { int? inheritedValue = -1; final Widget inner = Container( @@ -365,7 +366,7 @@ void main() { expect(inheritedValue, equals(3)); }); - testWidgets("Inherited widget doesn't notify descendants when descendant did not previously fail to find a match and had no dependencies", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Inherited widget doesn't notify descendants when descendant did not previously fail to find a match and had no dependencies", (WidgetTester tester) async { int buildCount = 0; final Widget inner = Container( @@ -392,7 +393,7 @@ void main() { expect(buildCount, equals(1)); }); - testWidgets('Inherited widget does notify descendants when descendant did not previously fail to find a match but did have other dependencies', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited widget does notify descendants when descendant did not previously fail to find a match but did have other dependencies', (WidgetTester tester) async { int buildCount = 0; final Widget inner = Container( @@ -423,10 +424,11 @@ void main() { expect(buildCount, equals(2)); }); - testWidgets("BuildContext.getInheritedWidgetOfExactType doesn't create a dependency", (WidgetTester tester) async { + testWidgetsWithLeakTracking("BuildContext.getInheritedWidgetOfExactType doesn't create a dependency", (WidgetTester tester) async { int buildCount = 0; final GlobalKey<void> inheritedKey = GlobalKey(); final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); final Widget builder = Builder( builder: (BuildContext context) { @@ -449,7 +451,7 @@ void main() { expect(buildCount, equals(1)); }); - testWidgets('initState() dependency on Inherited asserts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initState() dependency on Inherited asserts', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/5491 bool exceptionCaught = false; @@ -461,9 +463,10 @@ void main() { expect(exceptionCaught, isTrue); }); - testWidgets('InheritedNotifier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedNotifier', (WidgetTester tester) async { int buildCount = 0; final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); final Widget builder = Builder( builder: (BuildContext context) { diff --git a/packages/flutter/test/widgets/inherited_theme_test.dart b/packages/flutter/test/widgets/inherited_theme_test.dart index 1b677ac81af26..6315cc4674a04 100644 --- a/packages/flutter/test/widgets/inherited_theme_test.dart +++ b/packages/flutter/test/widgets/inherited_theme_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestRoute extends PageRouteBuilder<void> { TestRoute(Widget child) : super( @@ -26,7 +27,7 @@ class IconTextBox extends StatelessWidget { } void main() { - testWidgets('InheritedTheme.captureAll()', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedTheme.captureAll()', (WidgetTester tester) async { const double fontSize = 32; const double iconSize = 48; const Color textColor = Color(0xFF00FF00); @@ -146,7 +147,7 @@ void main() { expect(getIconStyle().fontSize, iconSize); }); - testWidgets('InheritedTheme.captureAll() multiple IconTheme ancestors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedTheme.captureAll() multiple IconTheme ancestors', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/39087 const Color outerColor = Color(0xFF0000FF); @@ -206,7 +207,7 @@ void main() { expect(getIconStyle(icon2).fontSize, iconSize); }); - testWidgets('InheritedTheme.captureAll() multiple DefaultTextStyle ancestors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InheritedTheme.captureAll() multiple DefaultTextStyle ancestors', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/39087 const Color textColor = Color(0xFF00FF00); diff --git a/packages/flutter/test/widgets/init_state_test.dart b/packages/flutter/test/widgets/init_state_test.dart index e70267e55e6d5..759f325d9171c 100644 --- a/packages/flutter/test/widgets/init_state_test.dart +++ b/packages/flutter/test/widgets/init_state_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; List<String> ancestors = <String>[]; @@ -28,9 +29,9 @@ class TestWidgetState extends State<TestWidget> { } void main() { - testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('initState() is called when we are in the tree', (WidgetTester tester) async { await tester.pumpWidget(const Parent(child: TestWidget())); - expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>'])); + expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RootWidget'])); }); } diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index b2adda8d87b1f..cf6be6a599864 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -8,14 +8,24 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; import 'gesture_utils.dart'; void main() { group('InteractiveViewer', () { - testWidgets('child fits in viewport', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + late TransformationController transformationController; + + setUp(() { + transformationController = TransformationController(); + }); + + tearDown(() { + transformationController.dispose(); + }); + + testWidgetsWithLeakTracking('child fits in viewport', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -65,8 +75,7 @@ void main() { expect(transformationController.value, isNot(equals(Matrix4.identity()))); }); - testWidgets('boundary slightly bigger than child', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('boundary slightly bigger than child', (WidgetTester tester) async { const double boundaryMargin = 10.0; await tester.pumpWidget( MaterialApp( @@ -121,8 +130,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), 200.0 / 220.0); }); - testWidgets('child bigger than viewport', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('child bigger than viewport', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -197,8 +205,7 @@ void main() { expect(transformationController.value, isNot(equals(Matrix4.identity()))); }); - testWidgets('child has no dimensions', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('child has no dimensions', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -232,8 +239,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('no boundary', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('no boundary', (WidgetTester tester) async { const double minScale = 0.8; await tester.pumpWidget( MaterialApp( @@ -288,8 +294,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), minScale); }); - testWidgets('PanAxis.free allows panning in all directions for diagonal gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.free allows panning in all directions for diagonal gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -326,8 +331,7 @@ void main() { expect(translation.y, childOffset.dy - childInterior.dy); }); - testWidgets('PanAxis.aligned allows panning in one direction only for diagonal gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.aligned allows panning in one direction only for diagonal gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -365,8 +369,7 @@ void main() { expect(translation.y, childOffset.dy - childInterior.dy); }); - testWidgets('PanAxis.aligned allows panning in one direction only for horizontal leaning gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.aligned allows panning in one direction only for horizontal leaning gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -404,8 +407,7 @@ void main() { expect(translation.y, 0.0); }); - testWidgets('PanAxis.horizontal allows panning in the horizontal direction only for diagonal gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.horizontal allows panning in the horizontal direction only for diagonal gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -443,8 +445,7 @@ void main() { expect(translation.y, 0.0); }); - testWidgets('PanAxis.horizontal allows panning in the horizontal direction only for horizontal leaning gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.horizontal allows panning in the horizontal direction only for horizontal leaning gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -482,8 +483,7 @@ void main() { expect(translation.y, 0.0); }); - testWidgets('PanAxis.horizontal does not allow panning in vertical direction on vertical gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.horizontal does not allow panning in vertical direction on vertical gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -521,8 +521,7 @@ void main() { expect(translation.y, 0.0); }); - testWidgets('PanAxis.vertical allows panning in the vertical direction only for diagonal gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.vertical allows panning in the vertical direction only for diagonal gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -560,8 +559,7 @@ void main() { expect(translation.x, 0.0); }); - testWidgets('PanAxis.vertical allows panning in the vertical direction only for vertical leaning gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.vertical allows panning in the vertical direction only for vertical leaning gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -599,8 +597,7 @@ void main() { expect(translation.x, 0.0); }); - testWidgets('PanAxis.vertical does not allow panning in horizontal direction on vertical gesture', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('PanAxis.vertical does not allow panning in horizontal direction on vertical gesture', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -638,8 +635,7 @@ void main() { expect(translation.y, 0.0); }); - testWidgets('inertia fling and boundary sliding', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('inertia fling and boundary sliding', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -697,8 +693,7 @@ void main() { expect(translation.y, moreOrLessEquals(boundaryMargin, epsilon: 1e-9)); }); - testWidgets('Scaling automatically causes a centering translation', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Scaling automatically causes a centering translation', (WidgetTester tester) async { const double boundaryMargin = 50.0; const double minScale = 0.1; await tester.pumpWidget( @@ -782,8 +777,7 @@ void main() { expect(newSceneFocalPoint.dy, moreOrLessEquals(sceneFocalPoint.dy, epsilon: 1.0)); }); - testWidgets('Scaling automatically causes a centering translation even when alignPanAxis is set', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Scaling automatically causes a centering translation even when alignPanAxis is set', (WidgetTester tester) async { const double boundaryMargin = 50.0; const double minScale = 0.1; await tester.pumpWidget( @@ -874,8 +868,7 @@ void main() { expect(newSceneFocalPoint.dy, moreOrLessEquals(sceneFocalPoint.dy, epsilon: 1.0)); }); - testWidgets('Can scale with mouse', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Can scale with mouse', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -896,8 +889,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), greaterThan(1.0)); }); - testWidgets('Cannot scale with mouse when scale is disabled', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Cannot scale with mouse when scale is disabled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -919,8 +911,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), equals(1.0)); }); - testWidgets('Scale with mouse returns onInteraction properties', (WidgetTester tester) async{ - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Scale with mouse returns onInteraction properties', (WidgetTester tester) async{ late Offset focalPoint; late Offset localFocalPoint; late double scaleChange; @@ -971,8 +962,7 @@ void main() { expect(scenePoint, const Offset(100, 100)); }); - testWidgets('Scaling amount is equal forth and back with a mouse scroll', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Scaling amount is equal forth and back with a mouse scroll', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1003,8 +993,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), 1.0); }); - testWidgets('onInteraction can be used to get scene point', (WidgetTester tester) async{ - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('onInteraction can be used to get scene point', (WidgetTester tester) async{ late Offset focalPoint; late Offset localFocalPoint; late double scaleChange; @@ -1057,8 +1046,7 @@ void main() { expect(scenePoint.dy, greaterThan(0.0)); }); - testWidgets('onInteraction is called even when disabled (touch)', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('onInteraction is called even when disabled (touch)', (WidgetTester tester) async { bool calledStart = false; bool calledUpdate = false; bool calledEnd = false; @@ -1129,8 +1117,7 @@ void main() { expect(calledEnd, isTrue); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets('onInteraction is called even when disabled (mouse)', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('onInteraction is called even when disabled (mouse)', (WidgetTester tester) async { bool calledStart = false; bool calledUpdate = false; bool calledEnd = false; @@ -1189,10 +1176,9 @@ void main() { expect(calledEnd, isTrue); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows })); - testWidgets('viewport changes size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('viewport changes size', (WidgetTester tester) async { addTearDown(tester.view.reset); - final TransformationController transformationController = TransformationController(); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1239,8 +1225,7 @@ void main() { expect(transformationController.value, equals(Matrix4.identity())); }); - testWidgets('gesture can start as pan and become scale', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('gesture can start as pan and become scale', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -1297,8 +1282,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/65304 - testWidgets('can view beyond boundary when necessary for a small child', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('can view beyond boundary when necessary for a small child', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1337,8 +1321,7 @@ void main() { expect(transformationController.value, equals(Matrix4.identity())); }); - testWidgets('scale does not jump when wrapped in GestureDetector', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('scale does not jump when wrapped in GestureDetector', (WidgetTester tester) async { double? initialScale; double? scale; await tester.pumpWidget( @@ -1413,7 +1396,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), greaterThan(1.0)); }); - testWidgets('Check if ClipRect is present in the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check if ClipRect is present in the tree', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1454,8 +1437,7 @@ void main() { ); }); - testWidgets('builder can change widgets that are off-screen', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('builder can change widgets that are off-screen', (WidgetTester tester) async { const double childHeight = 10.0; await tester.pumpWidget( MaterialApp( @@ -1539,7 +1521,7 @@ void main() { // Accessing the intrinsic size of a LayoutBuilder throws an error, so // InteractiveViewer only uses a LayoutBuilder when it's needed by // InteractiveViewer.builder. - testWidgets('LayoutBuilder is only used for InteractiveViewer.builder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder is only used for InteractiveViewer.builder', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1571,9 +1553,8 @@ void main() { expect(find.byType(LayoutBuilder), findsOneWidget); }); - testWidgets('scaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scaleFactor', (WidgetTester tester) async { const double scrollAmount = 30.0; - final TransformationController transformationController = TransformationController(); Future<void> pumpScaleFactor(double scaleFactor) { return tester.pumpWidget( MaterialApp( @@ -1651,7 +1632,7 @@ void main() { expect(scaleHighZoomedIn - scaleHighZoomedOut, lessThan(scaleZoomedIn - scaleZoomedOut)); }); - testWidgets('alignment argument is used properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alignment argument is used properly', (WidgetTester tester) async { const Alignment alignment = Alignment.center; await tester.pumpWidget(MaterialApp( @@ -1667,9 +1648,10 @@ void main() { expect(transform.alignment, alignment); }); - testWidgets('interactionEndFrictionCoefficient', (WidgetTester tester) async { + testWidgetsWithLeakTracking('interactionEndFrictionCoefficient', (WidgetTester tester) async { // Use the default interactionEndFrictionCoefficient. final TransformationController transformationController1 = TransformationController(); + addTearDown(transformationController1.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1695,6 +1677,7 @@ void main() { // Next try a custom interactionEndFrictionCoefficient. final TransformationController transformationController2 = TransformationController(); + addTearDown(transformationController2.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1723,8 +1706,7 @@ void main() { expect(translation2.y, lessThan(translation1.y)); }); - testWidgets('discrete scroll pointer events', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('discrete scroll pointer events', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -1767,8 +1749,7 @@ void main() { expect(translation.y, -125); }); - testWidgets('discrete scale pointer event', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('discrete scale pointer event', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -1804,8 +1785,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), 2.5); // capped at maxScale (2.5) }); - testWidgets('trackpadScrollCausesScale', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('trackpadScrollCausesScale', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -1840,8 +1820,7 @@ void main() { expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.499302500056767)); }); - testWidgets('trackpad pointer scroll events cause scale', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('trackpad pointer scroll events cause scale', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( @@ -1891,8 +1870,7 @@ void main() { expect(translation.y, moreOrLessEquals(-99.37155332430822)); }); - testWidgets('Scaling inertia', (WidgetTester tester) async { - final TransformationController transformationController = TransformationController(); + testWidgetsWithLeakTracking('Scaling inertia', (WidgetTester tester) async { const double boundaryMargin = 50.0; await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/intrinsic_width_test.dart b/packages/flutter/test/widgets/intrinsic_width_test.dart index 20848ef9f8d30..b3c1c05e58a14 100644 --- a/packages/flutter/test/widgets/intrinsic_width_test.dart +++ b/packages/flutter/test/widgets/intrinsic_width_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Intrinsic stepWidth, stepHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Intrinsic stepWidth, stepHeight', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25224 Widget buildFrame(double? stepWidth, double? stepHeight) { return Center( diff --git a/packages/flutter/test/widgets/invert_colors_test.dart b/packages/flutter/test/widgets/invert_colors_test.dart index 2c475a9715efe..11be7632ae117 100644 --- a/packages/flutter/test/widgets/invert_colors_test.dart +++ b/packages/flutter/test/widgets/invert_colors_test.dart @@ -10,9 +10,10 @@ library; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('InvertColors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InvertColors', (WidgetTester tester) async { await tester.pumpWidget(const RepaintBoundary( child: SizedBox( width: 200.0, @@ -29,7 +30,7 @@ void main() { ); }); - testWidgets('InvertColors and ColorFilter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('InvertColors and ColorFilter', (WidgetTester tester) async { await tester.pumpWidget(const RepaintBoundary( child: SizedBox( width: 200.0, diff --git a/packages/flutter/test/widgets/keep_alive_test.dart b/packages/flutter/test/widgets/keep_alive_test.dart index a92e9b24ca934..c44843916d72c 100644 --- a/packages/flutter/test/widgets/keep_alive_test.dart +++ b/packages/flutter/test/widgets/keep_alive_test.dart @@ -7,6 +7,7 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Leaf extends StatefulWidget { const Leaf({ @@ -46,7 +47,15 @@ List<Widget> generateList(Widget child) { } void main() { - testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async { + test('KeepAlive debugTypicalAncestorWidgetClass', () { + final KeepAlive keepAlive = KeepAlive(keepAlive: false, child: Container()); + expect( + keepAlive.debugTypicalAncestorWidgetDescription, + 'SliverWithKeepAliveWidget or TwoDimensionalViewport', + ); + }); + + testWidgetsWithLeakTracking('KeepAlive with ListView with itemExtent', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -94,7 +103,7 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing); }); - testWidgets('KeepAlive with ListView without itemExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('KeepAlive with ListView without itemExtent', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -141,7 +150,7 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing); }); - testWidgets('KeepAlive with GridView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('KeepAlive with GridView', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -190,7 +199,7 @@ void main() { expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing); }); - testWidgets('KeepAlive render tree description', (WidgetTester tester) async { + testWidgetsWithLeakTracking('KeepAlive render tree description', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -205,7 +214,7 @@ void main() { ); // The important lines below are the ones marked with "<----" expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - 'RenderView#00000\n' + '_ReusableRenderView#00000\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' @@ -379,7 +388,7 @@ void main() { await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); await tester.pump(); expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - 'RenderView#00000\n' + '_ReusableRenderView#00000\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' diff --git a/packages/flutter/test/widgets/key_test.dart b/packages/flutter/test/widgets/key_test.dart index b6031887325b8..8ba6247659e0a 100644 --- a/packages/flutter/test/widgets/key_test.dart +++ b/packages/flutter/test/widgets/key_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestValueKey<T> extends ValueKey<T> { const TestValueKey(super.value); @@ -19,7 +20,7 @@ class NotEquals { } void main() { - testWidgets('Keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Keys', (WidgetTester tester) async { expect(ValueKey<int>(nonconst(3)) == ValueKey<int>(nonconst(3)), isTrue); expect(ValueKey<num>(nonconst(3)) == ValueKey<int>(nonconst(3)), isFalse); expect(ValueKey<int>(nonconst(3)) == ValueKey<int>(nonconst(2)), isFalse); diff --git a/packages/flutter/test/widgets/keyboard_listener_test.dart b/packages/flutter/test/widgets/keyboard_listener_test.dart index 985e5dd3f91be..1149bddd68444 100644 --- a/packages/flutter/test/widgets/keyboard_listener_test.dart +++ b/packages/flutter/test/widgets/keyboard_listener_test.dart @@ -5,19 +5,22 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can dispose without keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can dispose without keyboard', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget(KeyboardListener(focusNode: focusNode, child: Container())); await tester.pumpWidget(KeyboardListener(focusNode: focusNode, child: Container())); await tester.pumpWidget(Container()); }); - testWidgets('Fuchsia key event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fuchsia key event', (WidgetTester tester) async { final List<KeyEvent> events = <KeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( KeyboardListener( @@ -39,13 +42,13 @@ void main() { expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft); await tester.pumpWidget(Container()); - focusNode.dispose(); }, skip: isBrowser); // [intended] This is a Fuchsia-specific test. - testWidgets('Web key event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Web key event', (WidgetTester tester) async { final List<KeyEvent> events = <KeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( KeyboardListener( @@ -67,13 +70,13 @@ void main() { expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft); await tester.pumpWidget(Container()); - focusNode.dispose(); }); - testWidgets('Defunct listeners do not receive events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defunct listeners do not receive events', (WidgetTester tester) async { final List<KeyEvent> events = <KeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( KeyboardListener( @@ -101,6 +104,5 @@ void main() { expect(events.length, 0); await tester.pumpWidget(Container()); - focusNode.dispose(); }); } diff --git a/packages/flutter/test/widgets/layout_builder_and_global_keys_test.dart b/packages/flutter/test/widgets/layout_builder_and_global_keys_test.dart index 68e6f4ee27dd8..d3c9178eaa3b9 100644 --- a/packages/flutter/test/widgets/layout_builder_and_global_keys_test.dart +++ b/packages/flutter/test/widgets/layout_builder_and_global_keys_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/src/rendering/sliver.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Wrapper extends StatelessWidget { const Wrapper({ @@ -41,7 +42,7 @@ class StatefulWrapperState extends State<StatefulWrapper> { } void main() { - testWidgets('Moving global key inside a LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving global key inside a LayoutBuilder', (WidgetTester tester) async { final GlobalKey<StatefulWrapperState> key = GlobalKey<StatefulWrapperState>(); await tester.pumpWidget( LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { @@ -60,7 +61,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Moving global key inside a SliverLayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving global key inside a SliverLayoutBuilder', (WidgetTester tester) async { final GlobalKey<StatefulWrapperState> key = GlobalKey<StatefulWrapperState>(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart b/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart index 6388fa320e509..b98714269530a 100644 --- a/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart +++ b/packages/flutter/test/widgets/layout_builder_and_parent_data_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class SizeChanger extends StatefulWidget { const SizeChanger({ @@ -42,7 +43,7 @@ class SizeChangerState extends State<SizeChanger> { } void main() { - testWidgets('Applying parent data inside a LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Applying parent data inside a LayoutBuilder', (WidgetTester tester) async { int frame = 1; await tester.pumpWidget(SizeChanger( // when this is triggered, the child LayoutBuilder will build again child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { diff --git a/packages/flutter/test/widgets/layout_builder_and_state_test.dart b/packages/flutter/test/widgets/layout_builder_and_state_test.dart index f5687492e7144..3db4baef0b78d 100644 --- a/packages/flutter/test/widgets/layout_builder_and_state_test.dart +++ b/packages/flutter/test/widgets/layout_builder_and_state_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -49,7 +50,7 @@ class Wrapper extends StatelessWidget { } void main() { - testWidgets('Calling setState on a widget that moves into a LayoutBuilder in the same frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Calling setState on a widget that moves into a LayoutBuilder in the same frame', (WidgetTester tester) async { StatefulWrapperState statefulWrapper; final Widget inner = Wrapper( child: StatefulWrapper( diff --git a/packages/flutter/test/widgets/layout_builder_mutations_test.dart b/packages/flutter/test/widgets/layout_builder_mutations_test.dart index 14f151bfe01a1..d5e4748d0280b 100644 --- a/packages/flutter/test/widgets/layout_builder_mutations_test.dart +++ b/packages/flutter/test/widgets/layout_builder_mutations_test.dart @@ -11,6 +11,7 @@ import 'package:flutter/src/widgets/media_query.dart'; import 'package:flutter/src/widgets/scroll_view.dart'; import 'package:flutter/src/widgets/sliver_layout_builder.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Wrapper extends StatelessWidget { const Wrapper({ @@ -25,7 +26,7 @@ class Wrapper extends StatelessWidget { } void main() { - testWidgets('Moving a global key from another LayoutBuilder at layout time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving a global key from another LayoutBuilder at layout time', (WidgetTester tester) async { final GlobalKey victimKey = GlobalKey(); await tester.pumpWidget(Row( @@ -71,7 +72,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Moving a global key from another SliverLayoutBuilder at layout time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving a global key from another SliverLayoutBuilder at layout time', (WidgetTester tester) async { final GlobalKey victimKey1 = GlobalKey(); final GlobalKey victimKey2 = GlobalKey(); @@ -128,7 +129,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('LayoutBuilder does not layout twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder does not layout twice', (WidgetTester tester) async { // This widget marks itself dirty when the closest MediaQuery changes. final _LayoutCount widget = _LayoutCount(); late StateSetter setState; diff --git a/packages/flutter/test/widgets/layout_builder_test.dart b/packages/flutter/test/widgets/layout_builder_test.dart index 6a298b4a88b75..18542fcd1a5de 100644 --- a/packages/flutter/test/widgets/layout_builder_test.dart +++ b/packages/flutter/test/widgets/layout_builder_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('LayoutBuilder parent size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder parent size', (WidgetTester tester) async { late Size layoutBuilderSize; final Key childKey = UniqueKey(); final Key parentKey = UniqueKey(); @@ -38,7 +39,7 @@ void main() { expect(childBox.size, equals(const Size(50.0, 100.0))); }); - testWidgets('SliverLayoutBuilder parent geometry', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverLayoutBuilder parent geometry', (WidgetTester tester) async { late SliverConstraints parentConstraints1; late SliverConstraints parentConstraints2; final Key childKey1 = UniqueKey(); @@ -88,7 +89,7 @@ void main() { expect(childSliver2.geometry, parentSliver2.geometry); }); - testWidgets('LayoutBuilder stateful child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder stateful child', (WidgetTester tester) async { late Size layoutBuilderSize; late StateSetter setState; final Key childKey = UniqueKey(); @@ -134,7 +135,7 @@ void main() { expect(childBox.size, equals(const Size(100.0, 200.0))); }); - testWidgets('SliverLayoutBuilder stateful descendants', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverLayoutBuilder stateful descendants', (WidgetTester tester) async { late StateSetter setState; double childWidth = 10.0; double childHeight = 20.0; @@ -203,7 +204,7 @@ void main() { expect(parentSliver.geometry!.paintExtent, 600); }); - testWidgets('LayoutBuilder stateful parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder stateful parent', (WidgetTester tester) async { late Size layoutBuilderSize; late StateSetter setState; final Key childKey = UniqueKey(); @@ -247,7 +248,7 @@ void main() { expect(box.size, equals(const Size(100.0, 200.0))); }); - testWidgets('LayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async { int built = 0; final Widget target = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -270,7 +271,7 @@ void main() { expect(built, 1); }); - testWidgets('LayoutBuilder and Inherited -- do rebuild when using inherited', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder and Inherited -- do rebuild when using inherited', (WidgetTester tester) async { int built = 0; final Widget target = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -294,7 +295,7 @@ void main() { expect(built, 2); }); - testWidgets('SliverLayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverLayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async { int built = 0; final Widget target = Directionality( textDirection: TextDirection.ltr, @@ -325,7 +326,7 @@ void main() { expect(built, 1); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverLayoutBuilder and Inherited -- do rebuild when not using inherited', (WidgetTester tester) async { int built = 0; @@ -360,7 +361,7 @@ void main() { }, ); - testWidgets('nested SliverLayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('nested SliverLayoutBuilder', (WidgetTester tester) async { late SliverConstraints parentConstraints1; late SliverConstraints parentConstraints2; final Key childKey = UniqueKey(); @@ -405,10 +406,11 @@ void main() { expect(parentSliver1.geometry, parentSliver2.geometry); }); - testWidgets('localToGlobal works with SliverLayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('localToGlobal works with SliverLayoutBuilder', (WidgetTester tester) async { final Key childKey1 = UniqueKey(); final Key childKey2 = UniqueKey(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( @@ -461,8 +463,9 @@ void main() { ); }); - testWidgets('hitTest works within SliverLayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hitTest works within SliverLayoutBuilder', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); List<int> hitCounts = <int> [0, 0, 0]; await tester.pumpWidget( @@ -589,7 +592,7 @@ void main() { expect(hitCounts, const <int> [0, 0, 0]); }); - testWidgets('LayoutBuilder does not call builder when layout happens but layout constraints do not change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder does not call builder when layout happens but layout constraints do not change', (WidgetTester tester) async { int builderInvocationCount = 0; Future<void> pumpTestWidget(Size size) async { @@ -665,7 +668,7 @@ void main() { expect(spy.performResizeCount, 2); }); - testWidgets('LayoutBuilder descendant widget can access [RenderBox.size] when rebuilding during layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LayoutBuilder descendant widget can access [RenderBox.size] when rebuilding during layout', (WidgetTester tester) async { Size? childSize; int buildCount = 0; diff --git a/packages/flutter/test/widgets/linked_scroll_view_test.dart b/packages/flutter/test/widgets/linked_scroll_view_test.dart index ee2a5c5921c11..ee1373ec545ac 100644 --- a/packages/flutter/test/widgets/linked_scroll_view_test.dart +++ b/packages/flutter/test/widgets/linked_scroll_view_test.dart @@ -14,6 +14,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class LinkedScrollController extends ScrollController { LinkedScrollController({ this.before, this.after }); @@ -381,7 +382,7 @@ class _TestState extends State<Test> { } void main() { - testWidgets('LinkedScrollController - 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinkedScrollController - 1', (WidgetTester tester) async { await tester.pumpWidget(const Test()); expect(find.text('Hello A'), findsOneWidget); expect(find.text('Hello 1'), findsOneWidget); @@ -459,7 +460,7 @@ void main() { expect(find.text('Hello D'), findsNothing); expect(find.text('Hello 4'), findsOneWidget); }); - testWidgets('LinkedScrollController - 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('LinkedScrollController - 2', (WidgetTester tester) async { await tester.pumpWidget(const Test()); expect(find.text('Hello A'), findsOneWidget); expect(find.text('Hello B'), findsOneWidget); diff --git a/packages/flutter/test/widgets/list_body_test.dart b/packages/flutter/test/widgets/list_body_test.dart index 6fcc17e1b2c55..0bf2b609b8243 100644 --- a/packages/flutter/test/widgets/list_body_test.dart +++ b/packages/flutter/test/widgets/list_body_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/src/foundation/assertions.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const List<Widget> children = <Widget>[ SizedBox(width: 200.0, height: 150.0), @@ -15,20 +16,21 @@ const List<Widget> children = <Widget>[ void expectRects(WidgetTester tester, List<Rect> expected) { final Finder finder = find.byType(SizedBox); - finder.precache(); final List<Rect> actual = <Rect>[]; - for (int i = 0; i < expected.length; ++i) { - final Finder current = finder.at(i); - expect(current, findsOneWidget); - actual.add(tester.getRect(finder.at(i))); - } + finder.runCached(() { + for (int i = 0; i < expected.length; ++i) { + final Finder current = finder.at(i); + expect(current, findsOneWidget); + actual.add(tester.getRect(finder.at(i))); + } + }); expect(() => finder.at(expected.length), throwsRangeError); expect(actual, equals(expected)); } void main() { - testWidgets('ListBody down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListBody down', (WidgetTester tester) async { await tester.pumpWidget(const Flex( direction: Axis.vertical, children: <Widget>[ ListBody(children: children) ], @@ -45,7 +47,7 @@ void main() { ); }); - testWidgets('ListBody up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListBody up', (WidgetTester tester) async { await tester.pumpWidget(const Flex( direction: Axis.vertical, children: <Widget>[ ListBody(reverse: true, children: children) ], @@ -62,7 +64,7 @@ void main() { ); }); - testWidgets('ListBody right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListBody right', (WidgetTester tester) async { await tester.pumpWidget(const Flex( textDirection: TextDirection.ltr, direction: Axis.horizontal, @@ -85,7 +87,7 @@ void main() { ); }); - testWidgets('ListBody left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListBody left', (WidgetTester tester) async { await tester.pumpWidget(const Flex( textDirection: TextDirection.ltr, direction: Axis.horizontal, @@ -108,7 +110,7 @@ void main() { ); }); - testWidgets('Limited space along main axis error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Limited space along main axis error', (WidgetTester tester) async { final FlutterExceptionHandler oldHandler = FlutterError.onError!; final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); @@ -141,7 +143,7 @@ void main() { )); }); - testWidgets('Nested ListBody unbounded cross axis error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested ListBody unbounded cross axis error', (WidgetTester tester) async { final FlutterExceptionHandler oldHandler = FlutterError.onError!; final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); diff --git a/packages/flutter/test/widgets/list_view_builder_test.dart b/packages/flutter/test/widgets/list_view_builder_test.dart index 52b095735e0f5..ccc83e248383a 100644 --- a/packages/flutter/test/widgets/list_view_builder_test.dart +++ b/packages/flutter/test/widgets/list_view_builder_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; void main() { - testWidgets('ListView.builder mount/dismount smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder mount/dismount smoke test', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -61,7 +62,7 @@ void main() { check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[ 6, 7, 8]); }); - testWidgets('ListView.builder vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder vertical', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -79,11 +80,14 @@ void main() { } Widget buildWidget() { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); + return Directionality( textDirection: TextDirection.ltr, child: FlipWidget( left: ListView.builder( - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, itemExtent: 200.0, itemBuilder: itemBuilder, ), @@ -134,7 +138,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView.builder horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder horizontal', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -152,11 +156,14 @@ void main() { } Widget buildWidget() { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); + return Directionality( textDirection: TextDirection.ltr, child: FlipWidget( left: ListView.builder( - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, itemBuilder: itemBuilder, itemExtent: 200.0, scrollDirection: Axis.horizontal, @@ -208,7 +215,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView.builder 10 items, 2-3 items visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder 10 items, 2-3 items visible', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // The root view is 800x600 in the test environment and our list @@ -261,7 +268,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView.builder 30 items with big jump, using prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder 30 items with big jump, using prototypeItem', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // The root view is 800x600 in the test environment and our list @@ -309,7 +316,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView.separated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated', (WidgetTester tester) async { Widget buildFrame({ required int itemCount }) { return Directionality( textDirection: TextDirection.ltr, @@ -355,7 +362,7 @@ void main() { }); - testWidgets('ListView.separated uses correct semanticChildCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated uses correct semanticChildCount', (WidgetTester tester) async { Widget buildFrame({ required int itemCount}) { return Directionality( textDirection: TextDirection.ltr, @@ -403,7 +410,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/72292 - testWidgets('ListView.builder and SingleChildScrollView can work well together', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder and SingleChildScrollView can work well together', (WidgetTester tester) async { Widget builder(int itemCount) { return Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/list_view_correction_test.dart b/packages/flutter/test/widgets/list_view_correction_test.dart index 804e74e113944..c6d44618e9ea0 100644 --- a/packages/flutter/test/widgets/list_view_correction_test.dart +++ b/packages/flutter/test/widgets/list_view_correction_test.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ListView can handle shrinking top elements', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView can handle shrinking top elements', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -65,8 +68,10 @@ void main() { expect(tester.getTopLeft(find.text('2')).dy, equals(200.0)); }); - testWidgets('ListView can handle shrinking top elements with cache extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView can handle shrinking top elements with cache extent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -122,8 +127,10 @@ void main() { expect(tester.getTopLeft(find.text('2')).dy, equals(150.0)); }); - testWidgets('ListView can handle inserts at 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView can handle inserts at 0', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/list_view_fling_test.dart b/packages/flutter/test/widgets/list_view_fling_test.dart index ebf7019b4b48b..aa76b397aeebd 100644 --- a/packages/flutter/test/widgets/list_view_fling_test.dart +++ b/packages/flutter/test/widgets/list_view_fling_test.dart @@ -4,12 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const double kHeight = 10.0; const double kFlingOffset = kHeight * 20.0; void main() { - testWidgets("Flings don't stutter", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Flings don't stutter", (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/list_view_horizontal_test.dart b/packages/flutter/test/widgets/list_view_horizontal_test.dart index 26b0533da0ac1..7e3999b4507b8 100644 --- a/packages/flutter/test/widgets/list_view_horizontal_test.dart +++ b/packages/flutter/test/widgets/list_view_horizontal_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const List<int> items = <int>[0, 1, 2, 3, 4, 5]; @@ -28,7 +29,7 @@ Widget buildFrame({ bool reverse = false, required TextDirection textDirection } } void main() { - testWidgets('Drag horizontally with scroll anchor at start (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag horizontally with scroll anchor at start (LTR)', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(textDirection: TextDirection.ltr)); await tester.pump(const Duration(seconds: 1)); @@ -147,7 +148,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('Drag horizontally with scroll anchor at end (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag horizontally with scroll anchor at end (LTR)', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(reverse: true, textDirection: TextDirection.ltr)); await tester.pump(const Duration(seconds: 1)); @@ -247,7 +248,7 @@ void main() { expect(find.text('5'), findsOneWidget); }); - testWidgets('Drag horizontally with scroll anchor at start (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag horizontally with scroll anchor at start (RTL)', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(textDirection: TextDirection.rtl)); await tester.pump(const Duration(seconds: 1)); @@ -347,7 +348,7 @@ void main() { expect(find.text('5'), findsOneWidget); }); - testWidgets('Drag horizontally with scroll anchor at end (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag horizontally with scroll anchor at end (LTR)', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(reverse: true, textDirection: TextDirection.rtl)); await tester.pump(const Duration(seconds: 1)); diff --git a/packages/flutter/test/widgets/list_view_misc_test.dart b/packages/flutter/test/widgets/list_view_misc_test.dart index 42c216c8966f2..c58b7a2316943 100644 --- a/packages/flutter/test/widgets/list_view_misc_test.dart +++ b/packages/flutter/test/widgets/list_view_misc_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Key blockKey = Key('test'); void main() { - testWidgets('Cannot scroll a non-overflowing block', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot scroll a non-overflowing block', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -36,7 +37,7 @@ void main() { await gesture.up(); }); - testWidgets('Can scroll an overflowing block', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can scroll an overflowing block', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -67,7 +68,7 @@ void main() { await gesture.up(); }); - testWidgets('ListView reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView reverse', (WidgetTester tester) async { int first = 0; int second = 0; @@ -111,8 +112,9 @@ void main() { expect(second, equals(1)); }); - testWidgets('ListView controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView controller', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); Widget buildBlock() { return Directionality( @@ -127,7 +129,7 @@ void main() { expect(controller.offset, equals(0.0)); }); - testWidgets('SliverBlockChildListDelegate.estimateMaxScrollOffset hits end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverBlockChildListDelegate.estimateMaxScrollOffset hits end', (WidgetTester tester) async { final SliverChildListDelegate delegate = SliverChildListDelegate(<Widget>[ Container(), Container(), @@ -161,7 +163,7 @@ void main() { expect(maxScrollOffset, equals(26.0)); }); - testWidgets('Resizing a ListView child restores scroll offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Resizing a ListView child restores scroll offset', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/9221 final AnimationController controller = AnimationController( vsync: const TestVSync(), diff --git a/packages/flutter/test/widgets/list_view_relayout_test.dart b/packages/flutter/test/widgets/list_view_relayout_test.dart index 7d0ff9bb14c9c..f0d0e53ed03c8 100644 --- a/packages/flutter/test/widgets/list_view_relayout_test.dart +++ b/packages/flutter/test/widgets/list_view_relayout_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Nested ListView with shrinkWrap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested ListView with shrinkWrap', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -36,7 +37,7 @@ void main() { ); }); - testWidgets('Underflowing ListView should relayout for additional children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Underflowing ListView should relayout for additional children', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/5950 await tester.pumpWidget( @@ -65,7 +66,7 @@ void main() { expect(find.text('200'), findsOneWidget); }); - testWidgets('Underflowing ListView contentExtent should track additional children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Underflowing ListView contentExtent should track additional children', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -102,7 +103,7 @@ void main() { expect(list.geometry!.scrollExtent, equals(0.0)); }); - testWidgets('Overflowing ListView should relayout for missing children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflowing ListView should relayout for missing children', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -143,7 +144,7 @@ void main() { expect(find.text('400'), findsNothing); }); - testWidgets('Overflowing ListView should not relayout for additional children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflowing ListView should not relayout for additional children', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -177,7 +178,7 @@ void main() { expect(find.text('100'), findsNothing); }); - testWidgets('Overflowing ListView should become scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflowing ListView should become scrollable', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/5920 // When a ListView's viewport hasn't overflowed, scrolling is disabled. // When children are added that cause it to overflow, scrolling should diff --git a/packages/flutter/test/widgets/list_view_semantics_test.dart b/packages/flutter/test/widgets/list_view_semantics_test.dart index c801973a1c072..3fdf022de9552 100644 --- a/packages/flutter/test/widgets/list_view_semantics_test.dart +++ b/packages/flutter/test/widgets/list_view_semantics_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -14,9 +15,10 @@ void main() { const int itemCount = 10; const double itemHeight = 150.0; - testWidgets('forward vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forward vertical', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -44,9 +46,10 @@ void main() { semantics.dispose(); }); - testWidgets('reverse vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverse vertical', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -75,9 +78,10 @@ void main() { semantics.dispose(); }); - testWidgets('forward horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forward horizontal', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -106,9 +110,10 @@ void main() { semantics.dispose(); }); - testWidgets('reverse horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverse horizontal', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/list_view_test.dart b/packages/flutter/test/widgets/list_view_test.dart index 7187f6a726e2f..a9ef0063f05ab 100644 --- a/packages/flutter/test/widgets/list_view_test.dart +++ b/packages/flutter/test/widgets/list_view_test.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; class TestSliverChildListDelegate extends SliverChildListDelegate { @@ -77,7 +78,7 @@ class _StatefulListViewState extends State<_StatefulListView> { void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('ListView.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -113,7 +114,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('ListView.separator respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separator respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -149,7 +150,7 @@ void main() { expect(finderCalled, true); }); - testWidgets('ListView default control', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView default control', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -160,7 +161,7 @@ void main() { ); }); - testWidgets('ListView itemExtent control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView itemExtent control test', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -207,7 +208,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('ListView large scroll jump', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView large scroll jump', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( @@ -249,7 +250,7 @@ void main() { log.clear(); }); - testWidgets('ListView large scroll jump and keepAlive first child not keepAlive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView large scroll jump and keepAlive first child not keepAlive', (WidgetTester tester) async { Future<void> checkAndScroll([ String zero = '0:false' ]) async { expect(find.text(zero), findsOneWidget); expect(find.text('1:false'), findsOneWidget); @@ -286,7 +287,7 @@ void main() { await checkAndScroll('0:true'); }); - testWidgets('ListView can build out of underflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView can build out of underflow', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -342,7 +343,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('ListView can build out of overflow padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView can build out of overflow padding', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -361,7 +362,7 @@ void main() { expect(find.text('padded', skipOffstage: false), findsOneWidget); }); - testWidgets('ListView with itemExtent in unbounded context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView with itemExtent in unbounded context', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -381,7 +382,7 @@ void main() { expect(find.text('19'), findsOneWidget); }); - testWidgets('ListView with shrink wrap in bounded context correctly uses cache extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView with shrink wrap in bounded context correctly uses cache extent', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Directionality( @@ -405,7 +406,7 @@ void main() { handle.dispose(); }); - testWidgets('ListView hidden items should stay hidden if their semantics are updated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView hidden items should stay hidden if their semantics are updated', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Directionality( @@ -440,7 +441,7 @@ void main() { handle.dispose(); }); - testWidgets('didFinishLayout has correct indices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('didFinishLayout has correct indices', (WidgetTester tester) async { final TestSliverChildListDelegate delegate = TestSliverChildListDelegate( List<Widget>.generate( 20, @@ -486,7 +487,7 @@ void main() { delegate.log.clear(); }); - testWidgets('ListView automatically pad MediaQuery on axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView automatically pad MediaQuery on axis', (WidgetTester tester) async { EdgeInsets? innerMediaQueryPadding; await tester.pumpWidget( @@ -514,7 +515,7 @@ void main() { expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0)); }); - testWidgets('ListView clips if overflow is smaller than cacheExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView clips if overflow is smaller than cacheExtent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17426. await tester.pumpWidget( @@ -545,7 +546,7 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); }); - testWidgets('ListView does not clips if no overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView does not clips if no overflow', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -568,7 +569,7 @@ void main() { expect(find.byType(Viewport), isNot(paints..clipRect())); }); - testWidgets('ListView (fixed extent) clips if overflow is smaller than cacheExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView (fixed extent) clips if overflow is smaller than cacheExtent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17426. await tester.pumpWidget( @@ -600,7 +601,7 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); }); - testWidgets('ListView (fixed extent) does not clips if no overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView (fixed extent) does not clips if no overflow', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -624,7 +625,7 @@ void main() { expect(find.byType(Viewport), isNot(paints..clipRect())); }); - testWidgets('ListView.horizontal has implicit scrolling by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.horizontal has implicit scrolling by default', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( Directionality( @@ -657,9 +658,10 @@ void main() { handle.dispose(); }); - testWidgets('Updates viewport dimensions when scroll direction changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updates viewport dimensions when scroll direction changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/43380. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); Widget buildListView({ required Axis scrollDirection }) { return Directionality( @@ -694,7 +696,7 @@ void main() { expect(controller.position.viewportDimension, 100.0); }); - testWidgets('ListView respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -728,9 +730,14 @@ void main() { // 4th, check that a non-default clip behavior can be sent to the painting context. renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.antiAlias)); - }); - - testWidgets('ListView.builder respects clipBehavior', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134572 + notDisposedAllowList: <String, int?> {'ContainerLayer': 1}, + )); + + testWidgetsWithLeakTracking('ListView.builder respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -745,7 +752,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('ListView.custom respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.custom respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -762,7 +769,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('ListView.separated respects clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -777,4 +784,190 @@ void main() { final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); + + // Regression test for https://github.com/flutter/flutter/pull/131393 + testWidgetsWithLeakTracking('itemExtentBuilder test', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + final List<int> buildLog = <int>[]; + late SliverLayoutDimensions sliverLayoutDimensions; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView.builder( + controller: controller, + itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) { + sliverLayoutDimensions = dimensions; + return 100.0; + }, + itemBuilder: (BuildContext context, int index) { + buildLog.insert(0, index); + return Text('Item $index'); + }, + ), + ), + ); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Item 6'), findsNothing); + expect( + sliverLayoutDimensions, + const SliverLayoutDimensions( + scrollOffset: 0.0, + precedingScrollExtent: 0.0, + viewportMainAxisExtent: 600.0, + crossAxisExtent: 800.0, + ) + ); + // viewport(600.0) + cache extent after(250.0) + expect(buildLog.length, 9); + expect(buildLog.min, 0); + expect(buildLog.max, 8); + + buildLog.clear(); + + // Scrolling drastically. + controller.jumpTo(10000.0); + await tester.pump(); + + expect(find.text('Item 99'), findsNothing); + expect(find.text('Item 100'), findsOneWidget); + expect(find.text('Item 105'), findsOneWidget); + expect(find.text('Item 106'), findsNothing); + expect( + sliverLayoutDimensions, + const SliverLayoutDimensions( + scrollOffset: 10000.0, + precedingScrollExtent: 0.0, + viewportMainAxisExtent: 600.0, + crossAxisExtent: 800.0, + ) + ); + // Scrolling drastically only loading the visible and cached area items. + // cache extent before(250.0) + viewport(600.0) + cache extent after(250.0) + expect(buildLog.length, 12); + expect(buildLog.min, 97); + expect(buildLog.max, 108); + + buildLog.clear(); + controller.jumpTo(5000.0); + await tester.pump(); + + expect(find.text('Item 49'), findsNothing); + expect(find.text('Item 50'), findsOneWidget); + expect(find.text('Item 55'), findsOneWidget); + expect(find.text('Item 56'), findsNothing); + expect( + sliverLayoutDimensions, + const SliverLayoutDimensions( + scrollOffset: 5000.0, + precedingScrollExtent: 0.0, + viewportMainAxisExtent: 600.0, + crossAxisExtent: 800.0, + ) + ); + // cache extent before(250.0) + viewport(600.0) + cache extent after(250.0) + expect(buildLog.length, 12); + expect(buildLog.min, 47); + expect(buildLog.max, 58); + + buildLog.clear(); + controller.jumpTo(4700.0); + await tester.pump(); + + expect(find.text('Item 46'), findsNothing); + expect(find.text('Item 47'), findsOneWidget); + expect(find.text('Item 52'), findsOneWidget); + expect(find.text('Item 53'), findsNothing); + expect( + sliverLayoutDimensions, + const SliverLayoutDimensions( + scrollOffset: 4700.0, + precedingScrollExtent: 0.0, + viewportMainAxisExtent: 600.0, + crossAxisExtent: 800.0, + ) + ); + // Only newly entered cached area items need to be loaded. + expect(buildLog.length, 3); + expect(buildLog.min, 44); + expect(buildLog.max, 46); + + buildLog.clear(); + controller.jumpTo(5300.0); + await tester.pump(); + + expect(find.text('Item 52'), findsNothing); + expect(find.text('Item 53'), findsOneWidget); + expect(find.text('Item 58'), findsOneWidget); + expect(find.text('Item 59'), findsNothing); + expect( + sliverLayoutDimensions, + const SliverLayoutDimensions( + scrollOffset: 5300.0, + precedingScrollExtent: 0.0, + viewportMainAxisExtent: 600.0, + crossAxisExtent: 800.0, + ) + ); + // Only newly entered cached area items need to be loaded. + expect(buildLog.length, 6); + expect(buildLog.min, 56); + expect(buildLog.max, 61); + }); + + testWidgetsWithLeakTracking('itemExtent, prototypeItem and itemExtentBuilder conflicts test', (WidgetTester tester) async { + Object? error; + try { + await tester.pumpWidget( + ListView.builder( + itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) { + return 100.0; + }, + itemExtent: 100.0, + itemBuilder: (BuildContext context, int index) { + return Text('Item $index'); + }, + ), + ); + } catch (e) { + error = e; + } + expect(error, isNotNull); + + error = null; + try { + await tester.pumpWidget( + ListView.builder( + itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) { + return 100.0; + }, + prototypeItem: Container(), + itemBuilder: (BuildContext context, int index) { + return Text('Item $index'); + }, + ), + ); + } catch (e) { + error = e; + } + expect(error, isNotNull); + + error = null; + try { + await tester.pumpWidget( + ListView.builder( + itemExtent: 100.0, + prototypeItem: Container(), + itemBuilder: (BuildContext context, int index) { + return Text('Item $index'); + }, + ), + ); + } catch (e) { + error = e; + } + expect(error, isNotNull); + }); } diff --git a/packages/flutter/test/widgets/list_view_vertical_test.dart b/packages/flutter/test/widgets/list_view_vertical_test.dart index 73b0910b0a757..16a103732d0ae 100644 --- a/packages/flutter/test/widgets/list_view_vertical_test.dart +++ b/packages/flutter/test/widgets/list_view_vertical_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const List<int> items = <int>[0, 1, 2, 3, 4, 5]; @@ -20,7 +21,7 @@ Widget buildFrame() { } void main() { - testWidgets('Drag vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag vertically', (WidgetTester tester) async { await tester.pumpWidget(buildFrame()); await tester.pump(); @@ -63,7 +64,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('Drag vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag vertically', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/list_view_viewporting_test.dart b/packages/flutter/test/widgets/list_view_viewporting_test.dart index 43739d6a74fc4..51764016a3ba4 100644 --- a/packages/flutter/test/widgets/list_view_viewporting_test.dart +++ b/packages/flutter/test/widgets/list_view_viewporting_test.dart @@ -5,12 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'test_widgets.dart'; void main() { - testWidgets('ListView mount/dismount smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView mount/dismount smoke test', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -60,7 +60,7 @@ void main() { ])); }); - testWidgets('ListView vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView vertical', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -78,11 +78,14 @@ void main() { } Widget builder() { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); + return Directionality( textDirection: TextDirection.ltr, child: FlipWidget( left: ListView.builder( - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, itemBuilder: itemBuilder, ), right: const Text('Not Today'), @@ -123,7 +126,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView horizontal', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; // the root view is 800x600 in the test environment @@ -141,12 +144,15 @@ void main() { } Widget builder() { + final ScrollController controller = ScrollController(initialScrollOffset: 500.0); + addTearDown(controller.dispose); + return Directionality( textDirection: TextDirection.ltr, child: FlipWidget( left: ListView.builder( scrollDirection: Axis.horizontal, - controller: ScrollController(initialScrollOffset: 500.0), + controller: controller, itemBuilder: itemBuilder, ), right: const Text('Not Today'), @@ -177,7 +183,7 @@ void main() { callbackTracker.clear(); }); - testWidgets('ListView reinvoke builders', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView reinvoke builders', (WidgetTester tester) async { final List<int> callbackTracker = <int>[]; final List<String?> text = <String?>[]; @@ -229,7 +235,7 @@ void main() { text.clear(); }); - testWidgets('ListView reinvoke builders', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView reinvoke builders', (WidgetTester tester) async { late StateSetter setState; ThemeData themeData = ThemeData.light(useMaterial3: false); @@ -272,7 +278,7 @@ void main() { expect(widget.color, equals(Colors.green)); }); - testWidgets('ListView padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView padding', (WidgetTester tester) async { Widget itemBuilder(BuildContext context, int index) { return Container( key: ValueKey<int>(index), @@ -299,7 +305,7 @@ void main() { expect(firstBox.size.width, equals(800.0 - 12.0)); }); - testWidgets('ListView underflow extents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView underflow extents', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -434,8 +440,11 @@ void main() { expect(position.minScrollExtent, equals(0.0)); }); - testWidgets('ListView should not paint hidden children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView should not paint hidden children', (WidgetTester tester) async { const Text text = Text('test'); + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -444,7 +453,7 @@ void main() { height: 200.0, child: ListView( cacheExtent: 500.0, - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, children: const <Widget>[ SizedBox(height: 140.0, child: text), SizedBox(height: 160.0, child: text), @@ -463,14 +472,17 @@ void main() { expect(list, paintsExactlyCountTimes(#drawParagraph, 2)); }); - testWidgets('ListView should paint with offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView should paint with offset', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 120.0); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Scaffold( body: SizedBox( height: 500.0, child: CustomScrollView( - controller: ScrollController(initialScrollOffset: 120.0), + controller: controller, slivers: <Widget>[ const SliverAppBar( expandedHeight: 250.0, @@ -494,9 +506,14 @@ void main() { final RenderObject renderObject = tester.renderObject(find.byType(Scrollable)); expect(renderObject, paintsExactlyCountTimes(#drawParagraph, 10)); - }); - - testWidgets('ListView should paint with rtl', (WidgetTester tester) async { + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134661 + notDisposedAllowList: <String, int?> {'AnnotatedRegionLayer<SystemUiOverlayStyle>': 1}, + )); + + testWidgetsWithLeakTracking('ListView should paint with rtl', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, diff --git a/packages/flutter/test/widgets/list_view_with_inherited_test.dart b/packages/flutter/test/widgets/list_view_with_inherited_test.dart index 1cfdbdaecea89..d47e6f96f9bf4 100644 --- a/packages/flutter/test/widgets/list_view_with_inherited_test.dart +++ b/packages/flutter/test/widgets/list_view_with_inherited_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; List<String> items = <String>[ 'one', @@ -40,7 +41,7 @@ Widget buildFrame() { } void main() { - testWidgets('ListView is a build function (smoketest)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView is a build function (smoketest)', (WidgetTester tester) async { await tester.pumpWidget(buildFrame()); expect(find.text('one'), findsOneWidget); expect(find.text('two'), findsOneWidget); diff --git a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart index a762693219cb0..e58bd6909d2fc 100644 --- a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart +++ b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart @@ -11,8 +11,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart' show TestCallbackPainter, TestClipPaintingContext; void main() { @@ -1286,7 +1286,7 @@ void main() { expect(controller.selectedItem, 10); }); - testWidgets('controller hot swappable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('controller hot swappable', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1303,14 +1303,16 @@ void main() { await tester.drag(find.byType(ListWheelScrollView), const Offset(0.0, -500.0)); await tester.pump(); - final FixedExtentScrollController newController = + final FixedExtentScrollController controller1 = FixedExtentScrollController(initialItem: 30); + addTearDown(controller1.dispose); + // Attaching first controller. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: ListWheelScrollView( - controller: newController, + controller: controller1, itemExtent: 100.0, children: List<Widget>.generate(100, (int index) { return const Placeholder(); @@ -1321,13 +1323,41 @@ void main() { // initialItem doesn't do anything since the scroll position was already // created. - expect(newController.selectedItem, 5); + expect(controller1.selectedItem, 5); - newController.jumpToItem(50); - expect(newController.selectedItem, 50); - expect(newController.position.pixels, 5000.0); + controller1.jumpToItem(50); + expect(controller1.selectedItem, 50); + expect(controller1.position.pixels, 5000.0); - // Now remove the controller + final FixedExtentScrollController controller2 = + FixedExtentScrollController(initialItem: 33); + addTearDown(controller2.dispose); + + // Attaching the second controller. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListWheelScrollView( + controller: controller2, + itemExtent: 100.0, + children: List<Widget>.generate(100, (int index) { + return const Placeholder(); + }), + ), + ), + ); + + // First controller is now detached. + expect(controller1.hasClients, isFalse); + // initialItem doesn't do anything since the scroll position was already + // created. + expect(controller2.selectedItem, 50); + + controller2.jumpToItem(40); + expect(controller2.selectedItem, 40); + expect(controller2.position.pixels, 4000.0); + + // Now, use the internal controller. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1340,9 +1370,59 @@ void main() { ), ); - // Internally, that same controller is still attached and still at the - // same place. - expect(newController.selectedItem, 50); + // Both controllers are now detached. + expect(controller1.hasClients, isFalse); + expect(controller2.hasClients, isFalse); + }); + + testWidgetsWithLeakTracking('controller can be reused', (WidgetTester tester) async { + final FixedExtentScrollController controller = + FixedExtentScrollController(initialItem: 3); + addTearDown(controller.dispose); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListWheelScrollView( + controller: controller, + itemExtent: 100.0, + children: List<Widget>.generate(100, (int index) { + return const Placeholder(); + }), + ), + ), + ); + + // selectedItem is equal to the initialItem. + expect(controller.selectedItem, 3); + expect(controller.position.pixels, 300.0); + + controller.jumpToItem(10); + expect(controller.selectedItem, 10); + expect(controller.position.pixels, 1000.0); + + await tester.pumpWidget(const Center()); + + // Controller is now detached. + expect(controller.hasClients, isFalse); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListWheelScrollView( + controller: controller, + itemExtent: 100.0, + children: List<Widget>.generate(100, (int index) { + return const Placeholder(); + }), + ), + ), + ); + + // Controller is now attached again. + expect(controller.hasClients, isTrue); + expect(controller.selectedItem, 3); + expect(controller.position.pixels, 300.0); }); }); @@ -1518,6 +1598,37 @@ void main() { expect(revealed.rect, const Rect.fromLTWH(165.0, 265.0, 10.0, 10.0)); }); + testWidgets('will not assert on getOffsetToReveal Axis', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + height: 500.0, + width: 300.0, + child: ListWheelScrollView( + controller: ScrollController(initialScrollOffset: 300.0), + itemExtent: 100.0, + children: List<Widget>.generate(10, (int i) { + return Center( + child: SizedBox( + height: 50.0, + width: 50.0, + child: Text('Item $i'), + ), + ); + }), + ), + ), + ), + ), + ); + + final RenderListWheelViewport viewport = tester.allRenderObjects.whereType<RenderListWheelViewport>().first; + final RenderObject target = tester.renderObject(find.text('Item 5')); + viewport.getOffsetToReveal(target, 0.0, axis: Axis.horizontal); + }); + testWidgets('ListWheelScrollView showOnScreen', (WidgetTester tester) async { List<Widget> outerChildren; final List<Widget> innerChildren = List<Widget>.generate(10, (int index) => Container()); diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index f1f8686cc26d4..64daf2e4bb981 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -8,11 +8,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'gesture_utils.dart'; void main() { - testWidgets('Events bubble up the tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Events bubble up the tree', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -46,7 +47,7 @@ void main() { ])); }); - testWidgets('Detects hover events from touch devices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Detects hover events from touch devices', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -74,7 +75,7 @@ void main() { }); group('transformed events', () { - testWidgets('simple offset for touch/signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('simple offset for touch/signal', (WidgetTester tester) async { final List<PointerEvent> events = <PointerEvent>[]; final Key key = UniqueKey(); @@ -145,7 +146,7 @@ void main() { expect(events.single.transform, expectedTransform); }); - testWidgets('scaled for touch/signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scaled for touch/signal', (WidgetTester tester) async { final List<PointerEvent> events = <PointerEvent>[]; final Key key = UniqueKey(); @@ -222,7 +223,7 @@ void main() { expect(events.single.transform, expectedTransform); }); - testWidgets('scaled and offset for touch/signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scaled and offset for touch/signal', (WidgetTester tester) async { final List<PointerEvent> events = <PointerEvent>[]; final Key key = UniqueKey(); @@ -300,7 +301,7 @@ void main() { expect(events.single.transform, expectedTransform); }); - testWidgets('rotated for touch/signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('rotated for touch/signal', (WidgetTester tester) async { final List<PointerEvent> events = <PointerEvent>[]; final Key key = UniqueKey(); @@ -378,9 +379,12 @@ void main() { }); }); - testWidgets("RenderPointerListener's debugFillProperties when default", (WidgetTester tester) async { + testWidgetsWithLeakTracking("RenderPointerListener's debugFillProperties when default", (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - RenderPointerListener().debugFillProperties(builder); + final RenderPointerListener renderListener = RenderPointerListener(); + addTearDown(renderListener.dispose); + + renderListener.debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) @@ -396,9 +400,13 @@ void main() { ]); }); - testWidgets("RenderPointerListener's debugFillProperties when full", (WidgetTester tester) async { + testWidgetsWithLeakTracking("RenderPointerListener's debugFillProperties when full", (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - RenderPointerListener( + + final RenderErrorBox renderErrorBox = RenderErrorBox(); + addTearDown(() => renderErrorBox.dispose()); + + final RenderPointerListener renderListener = RenderPointerListener( onPointerDown: (PointerDownEvent event) {}, onPointerUp: (PointerUpEvent event) {}, onPointerMove: (PointerMoveEvent event) {}, @@ -406,8 +414,11 @@ void main() { onPointerCancel: (PointerCancelEvent event) {}, onPointerSignal: (PointerSignalEvent event) {}, behavior: HitTestBehavior.opaque, - child: RenderErrorBox(), - ).debugFillProperties(builder); + child: renderErrorBox, + ); + addTearDown(renderListener.dispose); + + renderListener.debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) diff --git a/packages/flutter/test/widgets/listview_end_append_test.dart b/packages/flutter/test/widgets/listview_end_append_test.dart index 6d587828fd9ec..1e1cf76869baf 100644 --- a/packages/flutter/test/widgets/listview_end_append_test.dart +++ b/packages/flutter/test/widgets/listview_end_append_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ListView.builder() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/9506 Widget buildFrame(int itemCount) { @@ -35,7 +36,7 @@ void main() { expect(find.text('item 3'), findsOneWidget); }); - testWidgets('ListView.builder() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/9506 Widget buildFrame(int itemCount) { diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter/test/widgets/localizations_test.dart index ff96e095faf5e..213fe955f5955 100644 --- a/packages/flutter/test/widgets/localizations_test.dart +++ b/packages/flutter/test/widgets/localizations_test.dart @@ -6,11 +6,12 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding(); - testWidgets('Locale is available when Localizations widget stops deferring frames', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Locale is available when Localizations widget stops deferring frames', (WidgetTester tester) async { final FakeLocalizationsDelegate delegate = FakeLocalizationsDelegate(); await tester.pumpWidget(Localizations( locale: const Locale('fo'), @@ -37,7 +38,7 @@ void main() { expect(find.text('loaded'), findsOneWidget); }); - testWidgets('Localizations.localeOf throws when no localizations exist', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Localizations.localeOf throws when no localizations exist', (WidgetTester tester) async { final GlobalKey contextKey = GlobalKey(debugLabel: 'Test Key'); await tester.pumpWidget(Container(key: contextKey)); @@ -48,7 +49,7 @@ void main() { ))); }); - testWidgets('Localizations.maybeLocaleOf returns null when no localizations exist', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Localizations.maybeLocaleOf returns null when no localizations exist', (WidgetTester tester) async { final GlobalKey contextKey = GlobalKey(debugLabel: 'Test Key'); await tester.pumpWidget(Container(key: contextKey)); diff --git a/packages/flutter/test/widgets/lookup_boundary_test.dart b/packages/flutter/test/widgets/lookup_boundary_test.dart index 36d2d3b310b7c..d1af4fa09a731 100644 --- a/packages/flutter/test/widgets/lookup_boundary_test.dart +++ b/packages/flutter/test/widgets/lookup_boundary_test.dart @@ -5,10 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('LookupBoundary.dependOnInheritedWidgetOfExactType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { InheritedWidget? containerThroughBoundary; InheritedWidget? containerStoppedAtBoundary; @@ -32,7 +33,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('ignores ancestor boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores ancestor boundary', (WidgetTester tester) async { InheritedWidget? inheritedWidget; final Key inheritedKey = UniqueKey(); @@ -53,7 +54,7 @@ void main() { expect(inheritedWidget, equals(tester.widget(find.byKey(inheritedKey)))); }); - testWidgets('finds widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds widget before boundary', (WidgetTester tester) async { InheritedWidget? containerThroughBoundary; InheritedWidget? containerStoppedAtBoundary; @@ -80,7 +81,7 @@ void main() { expect(containerStoppedAtBoundary, equals(tester.widget(find.byKey(inheritedKey)))); }); - testWidgets('creates dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('creates dependency', (WidgetTester tester) async { MyInheritedWidget? inheritedWidget; final Widget widgetTree = DidChangeDependencySpy( @@ -108,7 +109,7 @@ void main() { expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 2); }); - testWidgets('causes didChangeDependencies to be called on move even if dependency was not fulfilled due to boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('causes didChangeDependencies to be called on move even if dependency was not fulfilled due to boundary', (WidgetTester tester) async { MyInheritedWidget? inheritedWidget; final Key globalKey = GlobalKey(); @@ -173,7 +174,7 @@ void main() { expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 3); }); - testWidgets('causes didChangeDependencies to be called on move even if dependency was non-existant', (WidgetTester tester) async { + testWidgetsWithLeakTracking('causes didChangeDependencies to be called on move even if dependency was non-existant', (WidgetTester tester) async { MyInheritedWidget? inheritedWidget; final Key globalKey = GlobalKey(); @@ -212,7 +213,7 @@ void main() { }); group('LookupBoundary.getElementForInheritedWidgetOfExactType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { InheritedElement? containerThroughBoundary; InheritedElement? containerStoppedAtBoundary; @@ -236,7 +237,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('ignores ancestor boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores ancestor boundary', (WidgetTester tester) async { InheritedElement? inheritedWidget; final Key inheritedKey = UniqueKey(); @@ -257,7 +258,7 @@ void main() { expect(inheritedWidget, equals(tester.element(find.byKey(inheritedKey)))); }); - testWidgets('finds widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds widget before boundary', (WidgetTester tester) async { InheritedElement? containerThroughBoundary; InheritedElement? containerStoppedAtBoundary; @@ -284,7 +285,7 @@ void main() { expect(containerStoppedAtBoundary, equals(tester.element(find.byKey(inheritedKey)))); }); - testWidgets('does not creates dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not creates dependency', (WidgetTester tester) async { final Widget widgetTree = DidChangeDependencySpy( onDidChangeDependencies: (BuildContext context) { @@ -309,7 +310,7 @@ void main() { expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); }); - testWidgets('does not cause didChangeDependencies to be called on move when found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not cause didChangeDependencies to be called on move when found', (WidgetTester tester) async { final Key globalKey = GlobalKey(); final Widget widgetTree = DidChangeDependencySpy( @@ -369,7 +370,7 @@ void main() { expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); }); - testWidgets('does not cause didChangeDependencies to be called on move when nothing was found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not cause didChangeDependencies to be called on move when nothing was found', (WidgetTester tester) async { final Key globalKey = GlobalKey(); final Widget widgetTree = DidChangeDependencySpy( @@ -404,7 +405,7 @@ void main() { }); group('LookupBoundary.findAncestorWidgetOfExactType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { Widget? containerThroughBoundary; Widget? containerStoppedAtBoundary; Widget? boundaryThroughBoundary; @@ -435,7 +436,7 @@ void main() { expect(boundaryStoppedAtBoundary, equals(tester.widget(find.byKey(boundaryKey)))); }); - testWidgets('finds right widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds right widget before boundary', (WidgetTester tester) async { Widget? containerThroughBoundary; Widget? containerStoppedAtBoundary; @@ -466,7 +467,7 @@ void main() { expect(containerStoppedAtBoundary, equals(tester.widget(find.byKey(innerContainerKey)))); }); - testWidgets('works if nothing is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works if nothing is found', (WidgetTester tester) async { Widget? containerStoppedAtBoundary; await tester.pumpWidget(Builder( @@ -479,7 +480,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('does not establish a dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not establish a dependency', (WidgetTester tester) async { Widget? containerThroughBoundary; Widget? containerStoppedAtBoundary; Widget? containerStoppedAtBoundaryUnfulfilled; @@ -520,7 +521,7 @@ void main() { }); group('LookupBoundary.findAncestorStateOfType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; @@ -543,7 +544,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('finds right widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds right widget before boundary', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; @@ -572,7 +573,7 @@ void main() { expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); }); - testWidgets('works if nothing is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works if nothing is found', (WidgetTester tester) async { State? containerStoppedAtBoundary; await tester.pumpWidget(Builder( @@ -585,7 +586,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('does not establish a dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not establish a dependency', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; State? containerStoppedAtBoundaryUnfulfilled; @@ -626,7 +627,7 @@ void main() { }); group('LookupBoundary.findRootAncestorStateOfType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; @@ -649,7 +650,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('finds right widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds right widget before boundary', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; @@ -678,7 +679,7 @@ void main() { expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); }); - testWidgets('works if nothing is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works if nothing is found', (WidgetTester tester) async { State? containerStoppedAtBoundary; await tester.pumpWidget(Builder( @@ -691,7 +692,7 @@ void main() { expect(containerStoppedAtBoundary, isNull); }); - testWidgets('does not establish a dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not establish a dependency', (WidgetTester tester) async { State? containerThroughBoundary; State? containerStoppedAtBoundary; State? containerStoppedAtBoundaryUnfulfilled; @@ -732,7 +733,7 @@ void main() { }); group('LookupBoundary.findAncestorRenderObjectOfType', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { RenderPadding? paddingThroughBoundary; RenderPadding? passingStoppedAtBoundary; @@ -756,7 +757,7 @@ void main() { expect(passingStoppedAtBoundary, isNull); }); - testWidgets('finds right widget before boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('finds right widget before boundary', (WidgetTester tester) async { RenderPadding? paddingThroughBoundary; RenderPadding? paddingStoppedAtBoundary; @@ -788,7 +789,7 @@ void main() { expect(paddingStoppedAtBoundary, equals(tester.renderObject(find.byKey(innerPaddingKey)))); }); - testWidgets('works if nothing is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works if nothing is found', (WidgetTester tester) async { RenderPadding? paddingStoppedAtBoundary; await tester.pumpWidget(Builder( @@ -801,7 +802,7 @@ void main() { expect(paddingStoppedAtBoundary, isNull); }); - testWidgets('does not establish a dependency', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not establish a dependency', (WidgetTester tester) async { RenderPadding? paddingThroughBoundary; RenderPadding? paddingStoppedAtBoundary; RenderWrap? wrapStoppedAtBoundaryUnfulfilled; @@ -843,7 +844,7 @@ void main() { }); group('LookupBoundary.visitAncestorElements', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { final List<Element> throughBoundary = <Element>[]; final List<Element> stoppedAtBoundary = <Element>[]; final List<Element> stoppedAtBoundaryTerminatedEarly = <Element>[]; @@ -909,7 +910,7 @@ void main() { }); group('LookupBoundary.visitChildElements', () { - testWidgets('respects boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('respects boundary', (WidgetTester tester) async { final Key root = UniqueKey(); final Key child1 = UniqueKey(); final Key child2 = UniqueKey(); @@ -961,7 +962,7 @@ void main() { }); group('LookupBoundary.debugIsHidingAncestorWidgetOfExactType', () { - testWidgets('is hiding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is hiding', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Container( padding: const EdgeInsets.all(10), @@ -978,7 +979,7 @@ void main() { expect(isHidden, isTrue); }); - testWidgets('is not hiding entity within boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding entity within boundary', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Container( padding: const EdgeInsets.all(10), @@ -999,7 +1000,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Container( padding: const EdgeInsets.all(10), @@ -1014,7 +1015,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Builder( builder: (BuildContext context) { @@ -1027,7 +1028,7 @@ void main() { }); group('LookupBoundary.debugIsHidingAncestorStateOfType', () { - testWidgets('is hiding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is hiding', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(MyStatefulContainer( child: LookupBoundary( @@ -1042,7 +1043,7 @@ void main() { expect(isHidden, isTrue); }); - testWidgets('is not hiding entity within boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding entity within boundary', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(MyStatefulContainer( child: LookupBoundary( @@ -1059,7 +1060,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(MyStatefulContainer( child: Builder( @@ -1072,7 +1073,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Builder( builder: (BuildContext context) { @@ -1085,7 +1086,7 @@ void main() { }); group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () { - testWidgets('is hiding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is hiding', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Padding( padding: EdgeInsets.zero, @@ -1101,7 +1102,7 @@ void main() { expect(isHidden, isTrue); }); - testWidgets('is not hiding entity within boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding entity within boundary', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Padding( padding: EdgeInsets.zero, @@ -1120,7 +1121,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Padding( padding: EdgeInsets.zero, @@ -1134,7 +1135,7 @@ void main() { expect(isHidden, isFalse); }); - testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is not hiding if no boundary and no entity exists', (WidgetTester tester) async { bool? isHidden; await tester.pumpWidget(Builder( builder: (BuildContext context) { diff --git a/packages/flutter/test/widgets/magnifier_test.dart b/packages/flutter/test/widgets/magnifier_test.dart index b6dda7a2e6757..2175efaf8e82d 100644 --- a/packages/flutter/test/widgets/magnifier_test.dart +++ b/packages/flutter/test/widgets/magnifier_test.dart @@ -9,6 +9,7 @@ import 'package:fake_async/fake_async.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _MockAnimationController extends AnimationController { _MockAnimationController() @@ -42,7 +43,7 @@ void main() { } group('Raw Magnifier', () { - testWidgets('should render with correct focal point and decoration', + testWidgetsWithLeakTracking('should render with correct focal point and decoration', (WidgetTester tester) async { final Key appKey = UniqueKey(); const Size magnifierSize = Size(100, 100); @@ -116,7 +117,7 @@ void main() { magnifierController.removeFromOverlay(); }); - testWidgets( + testWidgetsWithLeakTracking( 'should immediately remove from overlay on no animation controller', (WidgetTester tester) async { await runFakeAsync((FakeAsync async) async { @@ -149,7 +150,7 @@ void main() { }); }); - testWidgets('should update shown based on animation status', + testWidgetsWithLeakTracking('should update shown based on animation status', (WidgetTester tester) async { await runFakeAsync((FakeAsync async) async { final MagnifierController magnifierController = @@ -214,7 +215,7 @@ void main() { }); group('show', () { - testWidgets('should insert below below widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should insert below below widget', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Text('text'), )); @@ -226,6 +227,7 @@ void main() { final OverlayEntry fakeBeforeOverlayEntry = OverlayEntry(builder: (_) => fakeBefore); + addTearDown(() => fakeBeforeOverlayEntry..remove()..dispose()); Overlay.of(context).insert(fakeBeforeOverlayEntry); magnifierController.show( @@ -247,7 +249,7 @@ void main() { expect(allOverlayChildren.first.widget.key, fakeMagnifier.key); }); - testWidgets('should insert newly built widget without animating out if overlay != null', + testWidgetsWithLeakTracking('should insert newly built widget without animating out if overlay != null', (WidgetTester tester) async { await runFakeAsync((FakeAsync async) async { final _MockAnimationController animationController = diff --git a/packages/flutter/test/widgets/mark_needs_build_test.dart b/packages/flutter/test/widgets/mark_needs_build_test.dart index 9fa2d008b3360..af21964be22f8 100644 --- a/packages/flutter/test/widgets/mark_needs_build_test.dart +++ b/packages/flutter/test/widgets/mark_needs_build_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('setState can be called from build, initState, didChangeDependencies, and didUpdateWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState can be called from build, initState, didChangeDependencies, and didUpdateWidget', (WidgetTester tester) async { // Initial build. await tester.pumpWidget( const Directionality( diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index 9516e35e2fdc3..0cafd366143b9 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -7,10 +7,11 @@ import 'dart:ui' show Brightness, DisplayFeature, DisplayFeatureState, DisplayFe import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _MediaQueryAspectCase { const _MediaQueryAspectCase(this.method, this.data); - final Function(BuildContext) method; + final void Function(BuildContext) method; final MediaQueryData data; } @@ -43,27 +44,26 @@ class _MediaQueryAspectVariant extends TestVariant<_MediaQueryAspectCase> { } void main() { - testWidgets('MediaQuery does not have a default', (WidgetTester tester) async { - bool tested = false; + testWidgetsWithLeakTracking('MediaQuery does not have a default', (WidgetTester tester) async { + late final FlutterError error; // Cannot use tester.pumpWidget here because it wraps the widget in a View, // which introduces a MediaQuery ancestor. await pumpWidgetWithoutViewWrapper( tester: tester, widget: Builder( builder: (BuildContext context) { - tested = true; - MediaQuery.of(context); // should throw - return Container(); + try { + MediaQuery.of(context); + } on FlutterError catch (e) { + error = e; + } + return View( + view: tester.view, + child: const SizedBox(), + ); }, ), ); - expect(tested, isTrue); - final dynamic exception = tester.takeException(); - expect(exception, isNotNull); - expect(exception ,isFlutterError); - final FlutterError error = exception as FlutterError; - expect(error.diagnostics.length, 5); - expect(error.diagnostics.last, isA<ErrorHint>()); expect( error.toStringDeep(), startsWith( @@ -88,7 +88,7 @@ void main() { ); }); - testWidgets('MediaQuery.of finds a MediaQueryData when there is one', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.of finds a MediaQueryData when there is one', (WidgetTester tester) async { bool tested = false; await tester.pumpWidget( MediaQuery( @@ -108,7 +108,7 @@ void main() { expect(tested, isTrue); }); - testWidgets('MediaQuery.maybeOf defaults to null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.maybeOf defaults to null', (WidgetTester tester) async { bool tested = false; // Cannot use tester.pumpWidget here because it wraps the widget in a View, // which introduces a MediaQuery ancestor. @@ -119,14 +119,17 @@ void main() { final MediaQueryData? data = MediaQuery.maybeOf(context); expect(data, isNull); tested = true; - return Container(); + return View( + view: tester.view, + child: const SizedBox(), + ); }, ), ); expect(tested, isTrue); }); - testWidgets('MediaQuery.maybeOf finds a MediaQueryData when there is one', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.maybeOf finds a MediaQueryData when there is one', (WidgetTester tester) async { bool tested = false; await tester.pumpWidget( MediaQuery( @@ -144,7 +147,7 @@ void main() { expect(tested, isTrue); }); - testWidgets('MediaQueryData.fromView is sane', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.fromView is sane', (WidgetTester tester) async { final MediaQueryData data = MediaQueryData.fromView(tester.view); expect(data, hasOneLineDescription); expect(data.hashCode, equals(data.copyWith().hashCode)); @@ -159,7 +162,7 @@ void main() { expect(data.displayFeatures, isEmpty); }); - testWidgets('MediaQueryData.fromView uses platformData if provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.fromView uses platformData if provided', (WidgetTester tester) async { const MediaQueryData platformData = MediaQueryData( textScaleFactor: 1234, platformBrightness: Brightness.dark, @@ -177,7 +180,7 @@ void main() { expect(data.hashCode, data.copyWith().hashCode); expect(data.size, tester.view.physicalSize / tester.view.devicePixelRatio); expect(data.devicePixelRatio, tester.view.devicePixelRatio); - expect(data.textScaleFactor, platformData.textScaleFactor); + expect(data.textScaler, TextScaler.linear(platformData.textScaleFactor)); expect(data.platformBrightness, platformData.platformBrightness); expect(data.padding, EdgeInsets.fromViewPadding(tester.view.padding, tester.view.devicePixelRatio)); expect(data.viewPadding, EdgeInsets.fromViewPadding(tester.view.viewPadding, tester.view.devicePixelRatio)); @@ -194,7 +197,7 @@ void main() { expect(data.displayFeatures, tester.view.displayFeatures); }); - testWidgets('MediaQueryData.fromView uses data from platformDispatcher if no platformData is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.fromView uses data from platformDispatcher if no platformData is provided', (WidgetTester tester) async { tester.platformDispatcher ..textScaleFactorTestValue = 123 ..platformBrightnessTestValue = Brightness.dark @@ -206,7 +209,7 @@ void main() { expect(data.hashCode, data.copyWith().hashCode); expect(data.size, tester.view.physicalSize / tester.view.devicePixelRatio); expect(data.devicePixelRatio, tester.view.devicePixelRatio); - expect(data.textScaleFactor, tester.platformDispatcher.textScaleFactor); + expect(data.textScaler, TextScaler.linear(tester.platformDispatcher.textScaleFactor)); expect(data.platformBrightness, tester.platformDispatcher.platformBrightness); expect(data.padding, EdgeInsets.fromViewPadding(tester.view.padding, tester.view.devicePixelRatio)); expect(data.viewPadding, EdgeInsets.fromViewPadding(tester.view.viewPadding, tester.view.devicePixelRatio)); @@ -223,7 +226,7 @@ void main() { expect(data.displayFeatures, tester.view.displayFeatures); }); - testWidgets('MediaQuery.fromView injects a new MediaQuery with data from view, preserving platform-specific data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView injects a new MediaQuery with data from view, preserving platform-specific data', (WidgetTester tester) async { const MediaQueryData platformData = MediaQueryData( textScaleFactor: 1234, platformBrightness: Brightness.dark, @@ -253,7 +256,7 @@ void main() { expect(data, isNot(platformData)); expect(data.size, tester.view.physicalSize / tester.view.devicePixelRatio); expect(data.devicePixelRatio, tester.view.devicePixelRatio); - expect(data.textScaleFactor, platformData.textScaleFactor); + expect(data.textScaler, TextScaler.linear(platformData.textScaleFactor)); expect(data.platformBrightness, platformData.platformBrightness); expect(data.padding, EdgeInsets.fromViewPadding(tester.view.padding, tester.view.devicePixelRatio)); expect(data.viewPadding, EdgeInsets.fromViewPadding(tester.view.viewPadding, tester.view.devicePixelRatio)); @@ -270,7 +273,7 @@ void main() { expect(data.displayFeatures, tester.view.displayFeatures); }); - testWidgets('MediaQuery.fromView injects a new MediaQuery with data from view when no surrounding MediaQuery exists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView injects a new MediaQuery with data from view when no surrounding MediaQuery exists', (WidgetTester tester) async { tester.platformDispatcher ..textScaleFactorTestValue = 123 ..platformBrightnessTestValue = Brightness.dark @@ -289,7 +292,10 @@ void main() { child: Builder( builder: (BuildContext context) { data = MediaQuery.of(context); - return const Placeholder(); + return View( + view: tester.view, + child: const SizedBox(), + ); }, ) ); @@ -300,7 +306,7 @@ void main() { expect(outerData, isNull); expect(data.size, tester.view.physicalSize / tester.view.devicePixelRatio); expect(data.devicePixelRatio, tester.view.devicePixelRatio); - expect(data.textScaleFactor, tester.platformDispatcher.textScaleFactor); + expect(data.textScaler, TextScaler.linear(tester.platformDispatcher.textScaleFactor)); expect(data.platformBrightness, tester.platformDispatcher.platformBrightness); expect(data.padding, EdgeInsets.fromViewPadding(tester.view.padding, tester.view.devicePixelRatio)); expect(data.viewPadding, EdgeInsets.fromViewPadding(tester.view.viewPadding, tester.view.devicePixelRatio)); @@ -317,7 +323,7 @@ void main() { expect(data.displayFeatures, tester.view.displayFeatures); }); - testWidgets('MediaQuery.fromView updates on notifications (no parent data)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView updates on notifications (no parent data)', (WidgetTester tester) async { addTearDown(() => tester.platformDispatcher.clearAllTestValues()); addTearDown(() => tester.view.reset()); @@ -341,7 +347,10 @@ void main() { builder: (BuildContext context) { rebuildCount++; data = MediaQuery.of(context); - return const Placeholder(); + return View( + view: tester.view, + child: const SizedBox(), + ); }, ), ); @@ -352,10 +361,10 @@ void main() { expect(outerData, isNull); expect(rebuildCount, 1); - expect(data.textScaleFactor, 123); + expect(data.textScaler, const TextScaler.linear(123)); tester.platformDispatcher.textScaleFactorTestValue = 456; await tester.pump(); - expect(data.textScaleFactor, 456); + expect(data.textScaler, const TextScaler.linear(456)); expect(rebuildCount, 2); expect(data.platformBrightness, Brightness.dark); @@ -377,7 +386,7 @@ void main() { expect(rebuildCount, 5); }); - testWidgets('MediaQuery.fromView updates on notifications (with parent data)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView updates on notifications (with parent data)', (WidgetTester tester) async { addTearDown(() => tester.platformDispatcher.clearAllTestValues()); addTearDown(() => tester.view.reset()); @@ -392,7 +401,7 @@ void main() { await tester.pumpWidget( MediaQuery( data: const MediaQueryData( - textScaleFactor: 44, + textScaler: TextScaler.linear(44), platformBrightness: Brightness.dark, accessibleNavigation: true, ), @@ -411,10 +420,10 @@ void main() { expect(rebuildCount, 1); - expect(data.textScaleFactor, 44); + expect(data.textScaler, const TextScaler.linear(44)); tester.platformDispatcher.textScaleFactorTestValue = 456; await tester.pump(); - expect(data.textScaleFactor, 44); + expect(data.textScaler, const TextScaler.linear(44)); expect(rebuildCount, 1); expect(data.platformBrightness, Brightness.dark); @@ -436,17 +445,17 @@ void main() { expect(rebuildCount, 2); }); - testWidgets('MediaQuery.fromView updates when parent data changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView updates when parent data changes', (WidgetTester tester) async { late MediaQueryData data; int rebuildCount = 0; - double textScaleFactor = 55; + TextScaler textScaler = const TextScaler.linear(55); late StateSetter stateSetter; await tester.pumpWidget( StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; return MediaQuery( - data: MediaQueryData(textScaleFactor: textScaleFactor), + data: MediaQueryData(textScaler: textScaler), child: MediaQuery.fromView( view: tester.view, child: Builder( @@ -463,22 +472,22 @@ void main() { ); expect(rebuildCount, 1); - expect(data.textScaleFactor, 55); + expect(data.textScaler, const TextScaler.linear(55)); stateSetter(() { - textScaleFactor = 66; + textScaler = const TextScaler.linear(66); }); await tester.pump(); - expect(data.textScaleFactor, 66); + expect(data.textScaler, const TextScaler.linear(66)); expect(rebuildCount, 2); }); - testWidgets('MediaQueryData.copyWith defaults to source', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.copyWith defaults to source', (WidgetTester tester) async { final MediaQueryData data = MediaQueryData.fromView(tester.view); final MediaQueryData copied = data.copyWith(); expect(copied.size, data.size); expect(copied.devicePixelRatio, data.devicePixelRatio); - expect(copied.textScaleFactor, data.textScaleFactor); + expect(copied.textScaler, data.textScaler); expect(copied.padding, data.padding); expect(copied.viewPadding, data.viewPadding); expect(copied.viewInsets, data.viewInsets); @@ -494,12 +503,12 @@ void main() { expect(copied.displayFeatures, data.displayFeatures); }); - testWidgets('MediaQuery.copyWith copies specified values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.copyWith copies specified values', (WidgetTester tester) async { // Random and unique double values are used to ensure that the correct // values are copied over exactly const Size customSize = Size(3.14, 2.72); const double customDevicePixelRatio = 1.41; - const double customTextScaleFactor = 1.62; + const TextScaler customTextScaler = TextScaler.linear(1.23); const EdgeInsets customPadding = EdgeInsets.all(9.10938); const EdgeInsets customViewPadding = EdgeInsets.all(11.24031); const EdgeInsets customViewInsets = EdgeInsets.all(1.67262); @@ -517,7 +526,7 @@ void main() { final MediaQueryData copied = data.copyWith( size: customSize, devicePixelRatio: customDevicePixelRatio, - textScaleFactor: customTextScaleFactor, + textScaler: customTextScaler, padding: customPadding, viewPadding: customViewPadding, viewInsets: customViewInsets, @@ -535,7 +544,7 @@ void main() { ); expect(copied.size, customSize); expect(copied.devicePixelRatio, customDevicePixelRatio); - expect(copied.textScaleFactor, customTextScaleFactor); + expect(copied.textScaler, customTextScaler); expect(copied.padding, customPadding); expect(copied.viewPadding, customViewPadding); expect(copied.viewInsets, customViewInsets); @@ -552,10 +561,10 @@ void main() { expect(copied.displayFeatures, customDisplayFeatures); }); - testWidgets('MediaQuery.removePadding removes specified padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removePadding removes specified padding', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); @@ -573,7 +582,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -608,7 +617,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, EdgeInsets.zero); expect(unpadded.viewPadding, viewInsets); expect(unpadded.viewInsets, viewInsets); @@ -622,10 +631,10 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.removePadding only removes specified padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removePadding only removes specified padding', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); @@ -643,7 +652,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -675,7 +684,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, padding.copyWith(top: 0)); expect(unpadded.viewPadding, viewPadding.copyWith(top: viewInsets.top)); expect(unpadded.viewInsets, viewInsets); @@ -689,10 +698,10 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.removeViewInsets removes specified viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removeViewInsets removes specified viewInsets', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); @@ -710,7 +719,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -745,7 +754,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, padding); expect(unpadded.viewPadding, padding); expect(unpadded.viewInsets, EdgeInsets.zero); @@ -759,10 +768,10 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.removeViewInsets removes only specified viewInsets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removeViewInsets removes only specified viewInsets', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); @@ -780,7 +789,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -812,7 +821,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, padding); expect(unpadded.viewPadding, viewPadding.copyWith(bottom: 8)); expect(unpadded.viewInsets, viewInsets.copyWith(bottom: 0)); @@ -826,10 +835,10 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.removeViewPadding removes specified viewPadding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removeViewPadding removes specified viewPadding', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); @@ -847,7 +856,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -882,7 +891,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, EdgeInsets.zero); expect(unpadded.viewPadding, EdgeInsets.zero); expect(unpadded.viewInsets, viewInsets); @@ -896,10 +905,10 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.removeViewPadding removes only specified viewPadding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removeViewPadding removes only specified viewPadding', (WidgetTester tester) async { const Size size = Size(2.0, 4.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); @@ -917,7 +926,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -949,7 +958,7 @@ void main() { expect(unpadded.size, size); expect(unpadded.devicePixelRatio, devicePixelRatio); - expect(unpadded.textScaleFactor, textScaleFactor); + expect(unpadded.textScaler, textScaler); expect(unpadded.padding, padding.copyWith(left: 0)); expect(unpadded.viewPadding, viewPadding.copyWith(left: 0)); expect(unpadded.viewInsets, viewInsets); @@ -963,21 +972,21 @@ void main() { expect(unpadded.displayFeatures, displayFeatures); }); - testWidgets('MediaQuery.textScaleFactorOf', (WidgetTester tester) async { - late double outsideTextScaleFactor; - late double insideTextScaleFactor; + testWidgetsWithLeakTracking('MediaQuery.textScalerOf', (WidgetTester tester) async { + late TextScaler outsideTextScaler; + late TextScaler insideTextScaler; await tester.pumpWidget( Builder( builder: (BuildContext context) { - outsideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + outsideTextScaler = MediaQuery.textScalerOf(context); return MediaQuery( data: const MediaQueryData( - textScaleFactor: 4.0, + textScaler: TextScaler.linear(4.0), ), child: Builder( builder: (BuildContext context) { - insideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + insideTextScaler = MediaQuery.textScalerOf(context); return Container(); }, ), @@ -986,11 +995,11 @@ void main() { ), ); - expect(outsideTextScaleFactor, 1.0); - expect(insideTextScaleFactor, 4.0); + expect(outsideTextScaler, TextScaler.noScaling); + expect(insideTextScaler, const TextScaler.linear(4.0)); }); - testWidgets('MediaQuery.platformBrightnessOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.platformBrightnessOf', (WidgetTester tester) async { late Brightness outsideBrightness; late Brightness insideBrightness; @@ -1017,7 +1026,7 @@ void main() { expect(insideBrightness, Brightness.dark); }); - testWidgets('MediaQuery.highContrastOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.highContrastOf', (WidgetTester tester) async { late bool outsideHighContrast; late bool insideHighContrast; @@ -1044,7 +1053,34 @@ void main() { expect(insideHighContrast, true); }); - testWidgets('MediaQuery.boldTextOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.onOffSwitchLabelsOf', (WidgetTester tester) async { + late bool outsideOnOffSwitchLabels; + late bool insideOnOffSwitchLabels; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideOnOffSwitchLabels = MediaQuery.onOffSwitchLabelsOf(context); + return MediaQuery( + data: const MediaQueryData( + onOffSwitchLabels: true, + ), + child: Builder( + builder: (BuildContext context) { + insideOnOffSwitchLabels = MediaQuery.onOffSwitchLabelsOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + expect(outsideOnOffSwitchLabels, false); + expect(insideOnOffSwitchLabels, true); + }); + + testWidgetsWithLeakTracking('MediaQuery.boldTextOf', (WidgetTester tester) async { late bool outsideBoldTextOverride; late bool insideBoldTextOverride; @@ -1071,7 +1107,7 @@ void main() { expect(insideBoldTextOverride, true); }); - testWidgets('MediaQuery.fromView creates a MediaQuery', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.fromView creates a MediaQuery', (WidgetTester tester) async { MediaQuery? mediaQueryOutside; MediaQuery? mediaQueryInside; @@ -1096,7 +1132,7 @@ void main() { expect(mediaQueryOutside, isNot(mediaQueryInside)); }); - testWidgets('MediaQueryData.fromWindow is created using window values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.fromWindow is created using window values', (WidgetTester tester) async { final MediaQueryData windowData = MediaQueryData.fromWindow(tester.view); late MediaQueryData fromWindowData; @@ -1132,10 +1168,10 @@ void main() { expect(settingsA, isNot(settingsB)); }); - testWidgets('MediaQuery.removeDisplayFeatures removes specified display features and padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removeDisplayFeatures removes specified display features and padding', (WidgetTester tester) async { const Size size = Size(82.0, 40.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 10.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); @@ -1161,7 +1197,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -1191,7 +1227,7 @@ void main() { expect(subScreenMediaQuery.size, size); expect(subScreenMediaQuery.devicePixelRatio, devicePixelRatio); - expect(subScreenMediaQuery.textScaleFactor, textScaleFactor); + expect(subScreenMediaQuery.textScaler, textScaler); expect(subScreenMediaQuery.padding, EdgeInsets.zero); expect(subScreenMediaQuery.viewPadding, EdgeInsets.zero); expect(subScreenMediaQuery.viewInsets, EdgeInsets.zero); @@ -1204,10 +1240,10 @@ void main() { expect(subScreenMediaQuery.displayFeatures, isEmpty); }); - testWidgets('MediaQuery.removePadding only removes specified display features and padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery.removePadding only removes specified display features and padding', (WidgetTester tester) async { const Size size = Size(82.0, 40.0); const double devicePixelRatio = 2.0; - const double textScaleFactor = 1.2; + const TextScaler textScaler = TextScaler.linear(1.2); const EdgeInsets padding = EdgeInsets.only(top: 1.0, right: 2.0, left: 3.0, bottom: 4.0); const EdgeInsets viewPadding = EdgeInsets.only(top: 6.0, right: 8.0, left: 46.0, bottom: 12.0); const EdgeInsets viewInsets = EdgeInsets.only(top: 5.0, right: 6.0, left: 7.0, bottom: 8.0); @@ -1234,7 +1270,7 @@ void main() { data: const MediaQueryData( size: size, devicePixelRatio: devicePixelRatio, - textScaleFactor: textScaleFactor, + textScaler: textScaler, padding: padding, viewPadding: viewPadding, viewInsets: viewInsets, @@ -1264,7 +1300,7 @@ void main() { expect(subScreenMediaQuery.size, size); expect(subScreenMediaQuery.devicePixelRatio, devicePixelRatio); - expect(subScreenMediaQuery.textScaleFactor, textScaleFactor); + expect(subScreenMediaQuery.textScaler, textScaler); expect( subScreenMediaQuery.padding, const EdgeInsets.only(top: 1.0, right: 2.0, bottom: 4.0), @@ -1286,21 +1322,21 @@ void main() { expect(subScreenMediaQuery.displayFeatures, <DisplayFeature>[cutoutDisplayFeature]); }); - testWidgets('MediaQueryData.gestureSettings is set from view.gestureSettings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQueryData.gestureSettings is set from view.gestureSettings', (WidgetTester tester) async { tester.view.gestureSettings = const GestureSettings(physicalDoubleTapSlop: 100, physicalTouchSlop: 100); addTearDown(() => tester.view.resetGestureSettings()); expect(MediaQueryData.fromView(tester.view).gestureSettings.touchSlop, closeTo(33.33, 0.1)); // Repeating, of course }); - testWidgets('MediaQuery can be partially depended-on', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery can be partially depended-on', (WidgetTester tester) async { MediaQueryData data = const MediaQueryData( size: Size(800, 600), - textScaleFactor: 1.1 + textScaler: TextScaler.linear(1.1), ); int sizeBuildCount = 0; - int textScaleFactorBuildCount = 0; + int textScalerBuildCount = 0; final Widget showSize = Builder( builder: (BuildContext context) { @@ -1309,10 +1345,10 @@ void main() { } ); - final Widget showTextScaleFactor = Builder( + final Widget showTextScaler = Builder( builder: (BuildContext context) { - textScaleFactorBuildCount++; - return Text('textScaleFactor: ${MediaQuery.textScaleFactorOf(context).toStringAsFixed(1)}'); + textScalerBuildCount++; + return Text('textScaler: ${MediaQuery.textScalerOf(context)}'); } ); @@ -1324,7 +1360,7 @@ void main() { child: Column( children: <Widget>[ showSize, - showTextScaleFactor, + showTextScaler, ElevatedButton( onPressed: () { setState(() { @@ -1336,10 +1372,10 @@ void main() { ElevatedButton( onPressed: () { setState(() { - data = data.copyWith(textScaleFactor: data.textScaleFactor + 0.1); + data = data.copyWith(textScaler: TextScaler.noScaling); }); }, - child: const Text('Increase textScaleFactor by 0.1') + child: const Text('Disable text scaling') ) ] ) @@ -1350,26 +1386,26 @@ void main() { await tester.pumpWidget(MaterialApp(home: page)); expect(find.text('size: Size(800.0, 600.0)'), findsOneWidget); - expect(find.text('textScaleFactor: 1.1'), findsOneWidget); + expect(find.text('textScaler: linear (1.1x)'), findsOneWidget); expect(sizeBuildCount, 1); - expect(textScaleFactorBuildCount, 1); + expect(textScalerBuildCount, 1); await tester.tap(find.text('Increase width by 100')); await tester.pumpAndSettle(); expect(find.text('size: Size(900.0, 600.0)'), findsOneWidget); - expect(find.text('textScaleFactor: 1.1'), findsOneWidget); + expect(find.text('textScaler: linear (1.1x)'), findsOneWidget); expect(sizeBuildCount, 2); - expect(textScaleFactorBuildCount, 1); + expect(textScalerBuildCount, 1); - await tester.tap(find.text('Increase textScaleFactor by 0.1')); + await tester.tap(find.text('Disable text scaling')); await tester.pumpAndSettle(); expect(find.text('size: Size(900.0, 600.0)'), findsOneWidget); - expect(find.text('textScaleFactor: 1.2'), findsOneWidget); + expect(find.text('textScaler: no scaling'), findsOneWidget); expect(sizeBuildCount, 2); - expect(textScaleFactorBuildCount, 2); + expect(textScalerBuildCount, 2); }); - testWidgets('MediaQuery partial dependencies', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MediaQuery partial dependencies', (WidgetTester tester) async { MediaQueryData data = const MediaQueryData(); int buildCount = 0; @@ -1435,6 +1471,8 @@ void main() { const _MediaQueryAspectCase(MediaQuery.maybeDevicePixelRatioOf, MediaQueryData(devicePixelRatio: 1.1)), const _MediaQueryAspectCase(MediaQuery.textScaleFactorOf, MediaQueryData(textScaleFactor: 1.1)), const _MediaQueryAspectCase(MediaQuery.maybeTextScaleFactorOf, MediaQueryData(textScaleFactor: 1.1)), + const _MediaQueryAspectCase(MediaQuery.textScalerOf, MediaQueryData(textScaler: TextScaler.linear(1.1))), + const _MediaQueryAspectCase(MediaQuery.maybeTextScalerOf, MediaQueryData(textScaler: TextScaler.linear(1.1))), const _MediaQueryAspectCase(MediaQuery.platformBrightnessOf, MediaQueryData(platformBrightness: Brightness.dark)), const _MediaQueryAspectCase(MediaQuery.maybePlatformBrightnessOf, MediaQueryData(platformBrightness: Brightness.dark)), const _MediaQueryAspectCase(MediaQuery.paddingOf, MediaQueryData(padding: EdgeInsets.all(1))), diff --git a/packages/flutter/test/widgets/memory_allocations_test.dart b/packages/flutter/test/widgets/memory_allocations_test.dart index dbb44ac661851..aff4d51900db3 100644 --- a/packages/flutter/test/widgets/memory_allocations_test.dart +++ b/packages/flutter/test/widgets/memory_allocations_test.dart @@ -6,20 +6,26 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; + int _creations = 0; + int _disposals = 0; + void main() { final MemoryAllocations ma = MemoryAllocations.instance; - setUp(() { - assert(!ma.hasListeners); - }); - test('Publishers dispatch events in debug mode', () async { - int eventCount = 0; - void listener(ObjectEvent event) => eventCount++; + void listener(ObjectEvent event) { + if (event is ObjectDisposed) { + _disposals++; + } + if (event is ObjectCreated) { + _creations++; + } + } ma.addListener(listener); - final int expectedEventCount = await _activateFlutterObjectsAndReturnCountOfEvents(); - expect(eventCount, expectedEventCount); + final _EventStats actual = await _activateFlutterObjectsAndReturnCountOfEvents(); + expect(actual.creations, _creations); + expect(actual.disposals, _disposals); ma.removeListener(listener); expect(ma.hasListeners, isFalse); @@ -29,6 +35,8 @@ void main() { bool stateCreated = false; bool stateDisposed = false; + expect(ma.hasListeners, false); + void listener(ObjectEvent event) { if (event is ObjectCreated && event.object is State) { stateCreated = true; @@ -47,7 +55,7 @@ void main() { expect(stateCreated, isTrue); expect(stateDisposed, isTrue); ma.removeListener(listener); - expect(ma.hasListeners, isFalse); + expect(ma.hasListeners, false); }); } @@ -62,7 +70,8 @@ class _TestElement extends RenderObjectElement with RootElementMixin { _TestElement(): super(_TestLeafRenderObjectWidget()); void makeInactive() { - assignOwner(BuildOwner(focusManager: FocusManager())); + final FocusManager newFocusManager = FocusManager(); + assignOwner(BuildOwner(focusManager: newFocusManager)); mount(null, null); deactivate(); } @@ -109,15 +118,22 @@ class _TestStatefulWidgetState extends State<_TestStatefulWidget> { } } + +class _EventStats { + int creations = 0; + int disposals = 0; +} + /// Create and dispose Flutter objects to fire memory allocation events. -Future<int> _activateFlutterObjectsAndReturnCountOfEvents() async { - int count = 0; +Future<_EventStats> _activateFlutterObjectsAndReturnCountOfEvents() async { + final _EventStats result = _EventStats(); - final _TestElement element = _TestElement(); count++; - final RenderObject renderObject = _TestRenderObject(); count++; + final _TestElement element = _TestElement(); result.creations++; + final RenderObject renderObject = _TestRenderObject(); result.creations++; - element.makeInactive(); element.unmount(); count += 3; - renderObject.dispose(); count++; + element.makeInactive(); result.creations += 3; // 1 for the new BuildOwner, 1 for the new FocusManager, 1 for the new FocusScopeNode + element.unmount(); result.disposals += 2; // 1 for the old BuildOwner, 1 for the element + renderObject.dispose(); result.disposals += 1; - return count; + return result; } diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index 3d8ea7cc2777d..d502f9cd3f61a 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -46,7 +47,7 @@ void main() { }); group('ModalBarrier', () { - testWidgets('prevents interactions with widgets behind it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prevents interactions with widgets behind it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -61,7 +62,7 @@ void main() { expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier'); }); - testWidgets('prevents hover interactions with widgets behind it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prevents hover interactions with widgets behind it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -88,7 +89,7 @@ void main() { expect(hovered, isFalse, reason: 'because the hover is not prevented by ModalBarrier'); }); - testWidgets('does not prevent interactions with widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent interactions with widgets in front of it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -103,7 +104,7 @@ void main() { expect(tapped, isTrue, reason: 'because the tap is prevented by ModalBarrier'); }); - testWidgets('does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async { bool dragged = false; final Widget subject = Stack( textDirection: TextDirection.ltr, @@ -130,7 +131,7 @@ void main() { expect(dragged, isTrue, reason: 'because the drag is prevented by ModalBarrier'); }); - testWidgets('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -158,7 +159,7 @@ void main() { hovered = false; }); - testWidgets('plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { final List<String> playedSystemSounds = <String>[]; try { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( @@ -187,7 +188,7 @@ void main() { expect(playedSystemSounds[0], SystemSoundType.alert.toString()); }); - testWidgets('pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const SecondWidget(), @@ -220,7 +221,7 @@ void main() { ); }); - testWidgets('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const SecondWidget(), @@ -254,7 +255,7 @@ void main() { ); }); - testWidgets('may pop the Navigator when competing with other gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('may pop the Navigator when competing with other gestures', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const SecondWidgetWithCompetence(), @@ -282,7 +283,7 @@ void main() { ); }); - testWidgets('does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { bool willPopCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -327,7 +328,7 @@ void main() { expect(willPopCalled, isTrue); }); - testWidgets('pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { bool willPopCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -372,7 +373,7 @@ void main() { expect(willPopCalled, isTrue); }); - testWidgets('will call onDismiss callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will call onDismiss callback', (WidgetTester tester) async { bool dismissCallbackCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -400,7 +401,7 @@ void main() { expect(dismissCallbackCalled, true); }); - testWidgets('when onDismiss throws, should have correct context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when onDismiss throws, should have correct context', (WidgetTester tester) async { final FlutterExceptionHandler? handler = FlutterError.onError; FlutterErrorDetails? error; FlutterError.onError = (FlutterErrorDetails details) { @@ -423,7 +424,7 @@ void main() { FlutterError.onError = handler; }); - testWidgets('will not pop when given an onDismiss callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will not pop when given an onDismiss callback', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => SecondWidget(onDismiss: () {}), @@ -450,7 +451,7 @@ void main() { ); }); - testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const ModalBarrier(dismissible: false)); @@ -460,7 +461,7 @@ void main() { semantics.dispose(); }); - testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS, macOS and android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible ModalBarrier includes button in semantic tree on iOS, macOS and android', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const Directionality( textDirection: TextDirection.ltr, @@ -486,7 +487,7 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android})); }); group('AnimatedModalBarrier', () { - testWidgets('prevents interactions with widgets behind it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prevents interactions with widgets behind it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -501,7 +502,7 @@ void main() { expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier'); }); - testWidgets('prevents hover interactions with widgets behind it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('prevents hover interactions with widgets behind it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -528,7 +529,7 @@ void main() { expect(hovered, isFalse, reason: 'because the hover is not prevented by AnimatedModalBarrier'); }); - testWidgets('does not prevent interactions with widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent interactions with widgets in front of it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -543,7 +544,7 @@ void main() { expect(tapped, isTrue, reason: 'because the tap is prevented by AnimatedModalBarrier'); }); - testWidgets('does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async { bool dragged = false; final Widget subject = Stack( textDirection: TextDirection.ltr, @@ -570,7 +571,7 @@ void main() { expect(dragged, isTrue, reason: 'because the drag is prevented by AnimatedModalBarrier'); }); - testWidgets('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, children: <Widget>[ @@ -598,7 +599,7 @@ void main() { hovered = false; }); - testWidgets('plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('plays system alert sound when user tries to dismiss it', (WidgetTester tester) async { final List<String> playedSystemSounds = <String>[]; try { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( @@ -627,7 +628,7 @@ void main() { expect(playedSystemSounds[0], SystemSoundType.alert.toString()); }); - testWidgets('pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator when dismissed by primary tap', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const AnimatedSecondWidget(), @@ -660,7 +661,7 @@ void main() { ); }); - testWidgets('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const AnimatedSecondWidget(), @@ -694,7 +695,7 @@ void main() { ); }); - testWidgets('may pop the Navigator when competing with other gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('may pop the Navigator when competing with other gestures', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => const AnimatedSecondWidgetWithCompetence(), @@ -722,7 +723,7 @@ void main() { ); }); - testWidgets('does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async { bool willPopCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -767,7 +768,7 @@ void main() { expect(willPopCalled, isTrue); }); - testWidgets('pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async { bool willPopCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -812,7 +813,7 @@ void main() { expect(willPopCalled, isTrue); }); - testWidgets('will call onDismiss callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will call onDismiss callback', (WidgetTester tester) async { bool dismissCallbackCalled = false; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), @@ -840,7 +841,7 @@ void main() { expect(dismissCallbackCalled, true); }); - testWidgets('will not pop when given an onDismiss callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will not pop when given an onDismiss callback', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => const FirstWidget(), '/modal': (BuildContext context) => AnimatedSecondWidget(onDismiss: () {}), @@ -867,7 +868,7 @@ void main() { ); }); - testWidgets('Undismissible AnimatedModalBarrier hidden in semantic tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Undismissible AnimatedModalBarrier hidden in semantic tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(AnimatedModalBarrier(dismissible: false, color: colorAnimation)); @@ -877,7 +878,7 @@ void main() { semantics.dispose(); }); - testWidgets('Dismissible AnimatedModalBarrier includes button in semantic tree on iOS, macOS and android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dismissible AnimatedModalBarrier includes button in semantic tree on iOS, macOS and android', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -904,9 +905,10 @@ void main() { }); group('SemanticsClipper', () { - testWidgets('SemanticsClipper correctly clips Semantics.rect in four directions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsClipper correctly clips Semantics.rect in four directions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ValueNotifier<EdgeInsets> notifier = ValueNotifier<EdgeInsets>(const EdgeInsets.fromLTRB(10, 20, 30, 40)); + addTearDown(notifier.dispose); const Rect fullScreen = TestSemantics.fullScreen; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -933,7 +935,7 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android})); }); - testWidgets('uses default mouse cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('uses default mouse cursor', (WidgetTester tester) async { await tester.pumpWidget(const Stack( textDirection: TextDirection.ltr, children: <Widget>[ diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index 91e3b3e6e88a7..a26d7b3d799df 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class HoverClient extends StatefulWidget { const HoverClient({ @@ -77,7 +78,7 @@ class _HoverFeedbackState extends State<HoverFeedback> { void main() { // Regression test for https://github.com/flutter/flutter/issues/73330 - testWidgets('hitTestBehavior test - HitTestBehavior.deferToChild/opaque', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hitTestBehavior test - HitTestBehavior.deferToChild/opaque', (WidgetTester tester) async { bool onEnter = false; await tester.pumpWidget(Center( child: MouseRegion( @@ -103,7 +104,7 @@ void main() { expect(onEnter, true); }); - testWidgets('hitTestBehavior test - HitTestBehavior.deferToChild and non-opaque', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hitTestBehavior test - HitTestBehavior.deferToChild and non-opaque', (WidgetTester tester) async { bool onEnterRegion1 = false; bool onEnterRegion2 = false; await tester.pumpWidget(Directionality( @@ -143,7 +144,7 @@ void main() { expect(onEnterRegion1, true); }); - testWidgets('hitTestBehavior test - HitTestBehavior.translucent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hitTestBehavior test - HitTestBehavior.translucent', (WidgetTester tester) async { bool onEnterRegion1 = false; bool onEnterRegion2 = false; await tester.pumpWidget(Directionality( @@ -177,7 +178,7 @@ void main() { expect(onEnterRegion1, true); }); - testWidgets('onEnter and onExit can be triggered with mouse buttons pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onEnter and onExit can be triggered with mouse buttons pressed', (WidgetTester tester) async { PointerEnterEvent? enter; PointerExitEvent? exit; await tester.pumpWidget(Center( @@ -212,7 +213,7 @@ void main() { expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); }); - testWidgets('detects pointer enter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('detects pointer enter', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -244,7 +245,7 @@ void main() { expect(exit, isNull); }); - testWidgets('detects pointer exiting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('detects pointer exiting', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -275,7 +276,7 @@ void main() { expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); }); - testWidgets('triggers pointer enter when a mouse is connected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers pointer enter when a mouse is connected', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -301,7 +302,7 @@ void main() { expect(exit, isNull); }); - testWidgets('triggers pointer exit when a mouse is disconnected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers pointer exit when a mouse is disconnected', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -339,7 +340,7 @@ void main() { expect(exit, isNull); }); - testWidgets('triggers pointer enter when widget appears', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers pointer enter when widget appears', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -375,7 +376,7 @@ void main() { expect(exit, isNull); }); - testWidgets("doesn't trigger pointer exit when widget disappears", (WidgetTester tester) async { + testWidgetsWithLeakTracking("doesn't trigger pointer exit when widget disappears", (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -408,7 +409,7 @@ void main() { expect(exit, isNull); }); - testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers pointer enter when widget moves in', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -450,7 +451,7 @@ void main() { expect(exit, isNull); }); - testWidgets('triggers pointer exit when widget moves out', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers pointer exit when widget moves out', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -492,7 +493,7 @@ void main() { expect(exit!.localPosition, equals(const Offset(50, 50))); }); - testWidgets('detects hover from touch devices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('detects hover from touch devices', (WidgetTester tester) async { PointerEnterEvent? enter; PointerHoverEvent? move; PointerExitEvent? exit; @@ -522,7 +523,7 @@ void main() { expect(exit, isNull); }); - testWidgets('Hover works with nested listeners', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hover works with nested listeners', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; @@ -597,7 +598,7 @@ void main() { clearLists(); }); - testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hover transfers between two listeners', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; @@ -690,7 +691,7 @@ void main() { expect(exit2, isEmpty); }); - testWidgets('applies mouse cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applies mouse cursor', (WidgetTester tester) async { await tester.pumpWidget(const _Scaffold( topLeft: MouseRegion( cursor: SystemMouseCursors.text, @@ -711,7 +712,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); - testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MouseRegion uses updated callbacks', (WidgetTester tester) async { final List<String> logs = <String>[]; Widget hoverableContainer({ PointerEnterEventListener? onEnter, @@ -773,7 +774,7 @@ void main() { expect(logs, <String>['enter2', 'hover2', 'exit2']); }); - testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { await tester.pumpWidget( MouseRegion( onEnter: (PointerEnterEvent _) {}, @@ -795,7 +796,7 @@ void main() { expect(listener.needsCompositing, isFalse); }); - testWidgets('works with transform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works with transform', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/31986. final Key key = UniqueKey(); const double scaleFactor = 2.0; @@ -862,7 +863,7 @@ void main() { events.clear(); }); - testWidgets('needsCompositing is always false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('needsCompositing is always false', (WidgetTester tester) async { // Pretend that we have a mouse connected. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -897,7 +898,7 @@ void main() { expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); }); - testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Callbacks aren't called during build", (WidgetTester tester) async { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); @@ -939,7 +940,7 @@ void main() { expect(numExits, equals(0)); }); - testWidgets("MouseRegion activate/deactivate don't duplicate annotations", (WidgetTester tester) async { + testWidgetsWithLeakTracking("MouseRegion activate/deactivate don't duplicate annotations", (WidgetTester tester) async { final GlobalKey feedbackKey = GlobalKey(); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); @@ -983,7 +984,7 @@ void main() { expect(numExits, equals(0)); }); - testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Exit event when unplugging mouse should have a position', (WidgetTester tester) async { final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; final List<PointerHoverEvent> hover = <PointerHoverEvent>[]; final List<PointerExitEvent> exit = <PointerExitEvent>[]; @@ -1031,7 +1032,7 @@ void main() { expect(exit.single.delta, Offset.zero); }); - testWidgets('detects pointer enter with closure arguments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('detects pointer enter with closure arguments', (WidgetTester tester) async { await tester.pumpWidget(const _HoverClientWithClosures()); expect(find.text('not hovering'), findsOneWidget); @@ -1048,7 +1049,7 @@ void main() { expect(find.text('HOVERING'), findsOneWidget); }); - testWidgets('MouseRegion paints child once and only once when MouseRegion is inactive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MouseRegion paints child once and only once when MouseRegion is inactive', (WidgetTester tester) async { int paintCount = 0; await tester.pumpWidget( Directionality( @@ -1066,7 +1067,7 @@ void main() { expect(paintCount, 1); }); - testWidgets('MouseRegion paints child once and only once when MouseRegion is active', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MouseRegion paints child once and only once when MouseRegion is active', (WidgetTester tester) async { int paintCount = 0; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); @@ -1088,7 +1089,7 @@ void main() { expect(paintCount, 1); }); - testWidgets('A MouseRegion mounted under the pointer should take effect in the next postframe', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A MouseRegion mounted under the pointer should take effect in the next postframe', (WidgetTester tester) async { bool hovered = false; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); @@ -1130,7 +1131,7 @@ void main() { expect(tester.binding.hasScheduledFrame, isFalse); }); - testWidgets('A MouseRegion unmounted under the pointer should not trigger state change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A MouseRegion unmounted under the pointer should not trigger state change', (WidgetTester tester) async { bool hovered = true; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); @@ -1173,7 +1174,7 @@ void main() { expect(tester.binding.hasScheduledFrame, isFalse); }); - testWidgets('A MouseRegion moved into the mouse should take effect in the next postframe', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A MouseRegion moved into the mouse should take effect in the next postframe', (WidgetTester tester) async { bool hovered = false; final List<bool> logHovered = <bool>[]; bool moved = false; @@ -1303,7 +1304,7 @@ void main() { ); } - testWidgets('a transparent one should allow MouseRegions behind it to receive pointers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a transparent one should allow MouseRegions behind it to receive pointers', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget(tripleRegions( opaqueC: false, @@ -1350,7 +1351,7 @@ void main() { expect(logs, <String>['exitC', 'exitB', 'exitA']); }); - testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget(tripleRegions( opaqueC: true, @@ -1397,7 +1398,7 @@ void main() { expect(logs, <String>['exitC', 'exitA']); }); - testWidgets('opaque should default to true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('opaque should default to true', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget(tripleRegions( addLog: (String log) => logs.add(log), @@ -1420,7 +1421,7 @@ void main() { }); }); - testWidgets('an empty opaque MouseRegion is effective', (WidgetTester tester) async { + testWidgetsWithLeakTracking('an empty opaque MouseRegion is effective', (WidgetTester tester) async { bool bottomRegionIsHovered = false; await tester.pumpWidget( Directionality( @@ -1455,7 +1456,7 @@ void main() { expect(bottomRegionIsHovered, isFalse); }); - testWidgets("Changing MouseRegion's callbacks is effective and doesn't repaint", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Changing MouseRegion's callbacks is effective and doesn't repaint", (WidgetTester tester) async { final List<String> logs = <String>[]; const Key key = ValueKey<int>(1); @@ -1519,7 +1520,7 @@ void main() { expect(logs, <String>['paint']); }); - testWidgets('Changing MouseRegion.opaque is effective and repaints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing MouseRegion.opaque is effective and repaints', (WidgetTester tester) async { final List<String> logs = <String>[]; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); @@ -1563,7 +1564,7 @@ void main() { expect(logs, <String>['paint', 'hover-enter']); }); - testWidgets('Changing MouseRegion.cursor is effective and repaints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing MouseRegion.cursor is effective and repaints', (WidgetTester tester) async { final List<String> logPaints = <String>[]; final List<String> logEnters = <String>[]; @@ -1610,7 +1611,7 @@ void main() { logEnters.clear(); }); - testWidgets('Changing whether MouseRegion.cursor is null is effective and repaints', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing whether MouseRegion.cursor is null is effective and repaints', (WidgetTester tester) async { final List<String> logEnters = <String>[]; final List<String> logPaints = <String>[]; @@ -1682,7 +1683,7 @@ void main() { logEnters.clear(); }); - testWidgets('Does not trigger side effects during a reparent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not trigger side effects during a reparent', (WidgetTester tester) async { final List<String> logEnters = <String>[]; final List<String> logExits = <String>[]; final List<String> logCursors = <String>[]; @@ -1766,9 +1767,13 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); }); - testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async { + testWidgetsWithLeakTracking("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - RenderMouseRegion().debugFillProperties(builder); + + final RenderMouseRegion renderMouseRegion = RenderMouseRegion(); + addTearDown(renderMouseRegion.dispose); + + renderMouseRegion.debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList(); @@ -1781,16 +1786,23 @@ void main() { ]); }); - testWidgets("RenderMouseRegion's debugFillProperties when full", (WidgetTester tester) async { + testWidgetsWithLeakTracking("RenderMouseRegion's debugFillProperties when full", (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - RenderMouseRegion( + + final RenderErrorBox renderErrorBox = RenderErrorBox(); + addTearDown(renderErrorBox.dispose); + + final RenderMouseRegion renderMouseRegion = RenderMouseRegion( onEnter: (PointerEnterEvent event) {}, onExit: (PointerExitEvent event) {}, onHover: (PointerHoverEvent event) {}, cursor: SystemMouseCursors.click, validForMouseTracker: false, - child: RenderErrorBox(), - ).debugFillProperties(builder); + child: renderErrorBox, + ); + addTearDown(renderMouseRegion.dispose); + + renderMouseRegion.debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList(); @@ -1805,7 +1817,7 @@ void main() { ]); }); - testWidgets('No new frames are scheduled when mouse moves without triggering callbacks', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No new frames are scheduled when mouse moves without triggering callbacks', (WidgetTester tester) async { await tester.pumpWidget(Center( child: MouseRegion( child: const SizedBox( @@ -1825,7 +1837,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/67044 - testWidgets('Handle mouse events should ignore the detached MouseTrackerAnnotation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Handle mouse events should ignore the detached MouseTrackerAnnotation', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Center( child: Draggable<int>( diff --git a/packages/flutter/test/widgets/multi_view_binding_test.dart b/packages/flutter/test/widgets/multi_view_binding_test.dart new file mode 100644 index 0000000000000..d896cfa196f39 --- /dev/null +++ b/packages/flutter/test/widgets/multi_view_binding_test.dart @@ -0,0 +1,40 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + testWidgetsWithLeakTracking('runApp uses deprecated pipelineOwner and renderView', (WidgetTester tester) async { + runApp(const SizedBox()); + final RenderObject renderObject = tester.renderObject(find.byType(SizedBox)); + + RenderObject parent = renderObject; + while (parent.parent != null) { + parent = parent.parent!; + } + expect(parent, isA<RenderView>()); + expect(parent, equals(tester.binding.renderView)); + + expect(renderObject.owner, equals(tester.binding.pipelineOwner)); + }); + + testWidgetsWithLeakTracking('can manually attach RootWidget to build owner', (WidgetTester tester) async { + expect(find.byType(ColoredBox), findsNothing); + + final RootWidget rootWidget = RootWidget( + child: View( + view: tester.view, + child: const ColoredBox(color: Colors.orange), + ), + ); + tester.binding.attachToBuildOwner(rootWidget); + await tester.pump(); + expect(find.byType(ColoredBox), findsOneWidget); + expect(tester.binding.rootElement!.widget, equals(rootWidget)); + expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner)); + }); +} diff --git a/packages/flutter/test/widgets/multi_view_tree_updates_test.dart b/packages/flutter/test/widgets/multi_view_tree_updates_test.dart new file mode 100644 index 0000000000000..c8d6c049b8fa8 --- /dev/null +++ b/packages/flutter/test/widgets/multi_view_tree_updates_test.dart @@ -0,0 +1,222 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + testWidgetsWithLeakTracking('Widgets in view update as expected', (WidgetTester tester) async { + final Widget widget = View( + view: tester.view, + child: const TestWidget(), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: widget, + ); + + expect(find.text('Hello'), findsOneWidget); + expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'Hello'); + + tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'World'; + await tester.pump(); + expect(find.text('Hello'), findsNothing); + expect(find.text('World'), findsOneWidget); + expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World'); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[widget], + ), + ); + expect(find.text('Hello'), findsNothing); + expect(find.text('World'), findsOneWidget); + expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World'); + + tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'FooBar'; + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: widget, + ); + expect(find.text('World'), findsNothing); + expect(find.text('FooBar'), findsOneWidget); + expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'FooBar'); + }); + + testWidgetsWithLeakTracking('Views in ViewCollection update as expected', (WidgetTester tester) async { + Iterable<String> renderParagraphTexts() { + return tester.renderObjectList<RenderParagraph>(find.byType(Text)).map((RenderParagraph r) => r.text.toPlainText()); + } + + final Key key1 = UniqueKey(); + final Key key2 = UniqueKey(); + final Widget view1 = View( + view: tester.view, + child: TestWidget(key: key1), + ); + final Widget view2 = View( + view: FakeView(tester.view), + child: TestWidget(key: key2), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[view1, view2], + ), + ); + + expect(find.text('Hello'), findsNWidgets(2)); + expect(renderParagraphTexts(), <String>['Hello', 'Hello']); + + tester.state<TestWidgetState>(find.byKey(key1)).text = 'Guten'; + tester.state<TestWidgetState>(find.byKey(key2)).text = 'Tag'; + await tester.pump(); + expect(find.text('Hello'), findsNothing); + expect(find.text('Guten'), findsOneWidget); + expect(find.text('Tag'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Guten', 'Tag']); + + tester.state<TestWidgetState>(find.byKey(key2)).text = 'Abend'; + await tester.pump(); + expect(find.text('Tag'), findsNothing); + expect(find.text('Guten'), findsOneWidget); + expect(find.text('Abend'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Guten', 'Abend']); + + tester.state<TestWidgetState>(find.byKey(key2)).text = 'Morgen'; + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[view1, ViewCollection(views: <Widget>[view2])], + ), + ); + expect(find.text('Abend'), findsNothing); + expect(find.text('Guten'), findsOneWidget); + expect(find.text('Morgen'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Guten', 'Morgen']); + }); + + testWidgetsWithLeakTracking('Views in ViewAnchor update as expected', (WidgetTester tester) async { + Iterable<String> renderParagraphTexts() { + return tester.renderObjectList<RenderParagraph>(find.byType(Text)).map((RenderParagraph r) => r.text.toPlainText()); + } + + final Key insideAnchoredViewKey = UniqueKey(); + final Key outsideAnchoredViewKey = UniqueKey(); + final Widget view = View( + view: FakeView(tester.view), + child: TestWidget(key: insideAnchoredViewKey), + ); + + await tester.pumpWidget( + ViewAnchor( + view: view, + child: TestWidget(key: outsideAnchoredViewKey), + ), + ); + + expect(find.text('Hello'), findsNWidgets(2)); + expect(renderParagraphTexts(), <String>['Hello', 'Hello']); + + tester.state<TestWidgetState>(find.byKey(outsideAnchoredViewKey)).text = 'Guten'; + tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Tag'; + await tester.pump(); + expect(find.text('Hello'), findsNothing); + expect(find.text('Guten'), findsOneWidget); + expect(find.text('Tag'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Guten', 'Tag']); + + tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Abend'; + await tester.pump(); + expect(find.text('Tag'), findsNothing); + expect(find.text('Guten'), findsOneWidget); + expect(find.text('Abend'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Guten', 'Abend']); + + tester.state<TestWidgetState>(find.byKey(outsideAnchoredViewKey)).text = 'Schönen'; + await tester.pump(); + expect(find.text('Guten'), findsNothing); + expect(find.text('Schönen'), findsOneWidget); + expect(find.text('Abend'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Schönen', 'Abend']); + + tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Tag'; + await tester.pumpWidget( + ViewAnchor( + view: ViewCollection(views: <Widget>[view]), + child: TestWidget(key: outsideAnchoredViewKey), + ), + ); + await tester.pump(); + expect(find.text('Abend'), findsNothing); + expect(find.text('Schönen'), findsOneWidget); + expect(find.text('Tag'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Schönen', 'Tag']); + + tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Morgen'; + await tester.pumpWidget( + SizedBox( + child: ViewAnchor( + view: ViewCollection(views: <Widget>[view]), + child: TestWidget(key: outsideAnchoredViewKey), + ), + ), + ); + await tester.pump(); + expect(find.text('Schönen'), findsNothing); // The `outsideAnchoredViewKey` is not a global key, its state is lost in the move above. + expect(find.text('Tag'), findsNothing); + expect(find.text('Hello'), findsOneWidget); + expect(find.text('Morgen'), findsOneWidget); + expect(renderParagraphTexts(), <String>['Hello', 'Morgen']); + }); +} + +class TestWidget extends StatefulWidget { + const TestWidget({super.key}); + + @override + State<TestWidget> createState() => TestWidgetState(); +} + +class TestWidgetState extends State<TestWidget> { + String get text => _text; + String _text = 'Hello'; + set text(String value) { + if (_text == value) { + return; + } + setState(() { + _text = value; + }); + } + + @override + Widget build(BuildContext context) { + return Text(text, textDirection: TextDirection.ltr); + } +} + +Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { + tester.binding.attachRootWidget(widget); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} + +class FakeView extends TestFlutterView{ + FakeView(FlutterView view, { this.viewId = 100 }) : super( + view: view, + platformDispatcher: view.platformDispatcher as TestPlatformDispatcher, + display: view.display as TestDisplay, + ); + + @override + final int viewId; +} diff --git a/packages/flutter/test/widgets/multichild_test.dart b/packages/flutter/test/widgets/multichild_test.dart index 78dc38fe9d08a..f52afc5ef99cd 100644 --- a/packages/flutter/test/widgets/multichild_test.dart +++ b/packages/flutter/test/widgets/multichild_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -32,7 +33,7 @@ void checkTree(WidgetTester tester, List<BoxDecoration> expectedDecorations) { } void main() { - testWidgets('MultiChildRenderObjectElement control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MultiChildRenderObjectElement control test', (WidgetTester tester) async { await tester.pumpWidget( const Stack( @@ -117,7 +118,7 @@ void main() { }); - testWidgets('MultiChildRenderObjectElement with stateless widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MultiChildRenderObjectElement with stateless widgets', (WidgetTester tester) async { await tester.pumpWidget( const Stack( @@ -243,7 +244,7 @@ void main() { checkTree(tester, <BoxDecoration>[]); }); - testWidgets('MultiChildRenderObjectElement with stateful widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MultiChildRenderObjectElement with stateful widgets', (WidgetTester tester) async { await tester.pumpWidget( const Stack( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/multichildobject_with_keys_test.dart b/packages/flutter/test/widgets/multichildobject_with_keys_test.dart index d00260a7740a3..07d04fa3895a8 100644 --- a/packages/flutter/test/widgets/multichildobject_with_keys_test.dart +++ b/packages/flutter/test/widgets/multichildobject_with_keys_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Render and element tree stay in sync when keyed children move around', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Render and element tree stay in sync when keyed children move around', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/48855. await tester.pumpWidget( @@ -59,7 +60,7 @@ void main() { ); }); - testWidgets('Building a new MultiChildRenderObjectElement with children having duplicated keys throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Building a new MultiChildRenderObjectElement with children having duplicated keys throws', (WidgetTester tester) async { const ValueKey<int> duplicatedKey = ValueKey<int>(1); await tester.pumpWidget(const Column( @@ -79,7 +80,7 @@ void main() { ); }); - testWidgets('Updating a MultiChildRenderObjectElement to have children with duplicated keys throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updating a MultiChildRenderObjectElement to have children with duplicated keys throws', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/81541 const ValueKey<int> key1 = ValueKey<int>(1); diff --git a/packages/flutter/test/widgets/navigator_and_layers_test.dart b/packages/flutter/test/widgets/navigator_and_layers_test.dart index 1283a386438af..496b69fc359f6 100644 --- a/packages/flutter/test/widgets/navigator_and_layers_test.dart +++ b/packages/flutter/test/widgets/navigator_and_layers_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -26,7 +27,7 @@ class TestCustomPainter extends CustomPainter { } void main() { - testWidgets('Do we paint when coming back from a navigation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do we paint when coming back from a navigation', (WidgetTester tester) async { final List<String> log = <String>[]; log.add('0'); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/navigator_replacement_test.dart b/packages/flutter/test/widgets/navigator_replacement_test.dart index d43df8a88cff7..a1c762c0d6065 100644 --- a/packages/flutter/test/widgets/navigator_replacement_test.dart +++ b/packages/flutter/test/widgets/navigator_replacement_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'observer_tester.dart'; void main() { - testWidgets('Back during pushReplacement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Back during pushReplacement', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: const Material(child: Text('home')), routes: <String, WidgetBuilder>{ @@ -42,7 +43,7 @@ void main() { }); group('pushAndRemoveUntil', () { - testWidgets('notifies appropriately', (WidgetTester tester) async { + testWidgetsWithLeakTracking('notifies appropriately', (WidgetTester tester) async { final TestObserver observer = TestObserver(); final Widget myApp = MaterialApp( home: const Material(child: Text('home')), @@ -110,7 +111,7 @@ void main() { ])); }); - testWidgets('triggers page transition animation for pushed route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('triggers page transition animation for pushed route', (WidgetTester tester) async { final Widget myApp = MaterialApp( home: const Material(child: Text('home')), routes: <String, WidgetBuilder>{ @@ -139,7 +140,7 @@ void main() { expect(find.text('b'), findsOneWidget); }); - testWidgets('Hero transition triggers when preceding route contains hero, and predicate route does not', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero transition triggers when preceding route contains hero, and predicate route does not', (WidgetTester tester) async { const String kHeroTag = 'hero'; final Widget myApp = MaterialApp( initialRoute: '/', @@ -184,7 +185,7 @@ void main() { expect(find.text('b'), isOnstage); }); - testWidgets('Hero transition does not trigger when preceding route does not contain hero, but predicate route does', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hero transition does not trigger when preceding route does not contain hero, but predicate route does', (WidgetTester tester) async { const String kHeroTag = 'hero'; final Widget myApp = MaterialApp( theme: ThemeData( diff --git a/packages/flutter/test/widgets/navigator_restoration_test.dart b/packages/flutter/test/widgets/navigator_restoration_test.dart index ecbed9bfb7658..e91d41aa1f4b3 100644 --- a/packages/flutter/test/widgets/navigator_restoration_test.dart +++ b/packages/flutter/test/widgets/navigator_restoration_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Restoration Smoke Test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Restoration Smoke Test', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -29,7 +30,7 @@ void main() { expect(findRoute('home', count: 2), findsOneWidget); }); - testWidgets('restorablePushNamed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushNamed', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -66,7 +67,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }); - testWidgets('restorablePushReplacementNamed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushReplacementNamed', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -99,7 +100,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }); - testWidgets('restorablePopAndPushNamed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePopAndPushNamed', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -132,7 +133,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }); - testWidgets('restorablePushNamedAndRemoveUntil', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushNamedAndRemoveUntil', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -165,7 +166,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }); - testWidgets('restorablePush', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePush', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -202,7 +203,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('restorablePush adds route on all platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePush adds route on all platforms', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -212,7 +213,7 @@ void main() { expect(findRoute('Foo'), findsOneWidget); }); - testWidgets('restorablePushReplacement', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushReplacement', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -245,7 +246,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('restorablePushReplacement adds route on all platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushReplacement adds route on all platforms', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -255,7 +256,7 @@ void main() { expect(findRoute('Foo'), findsOneWidget); }); - testWidgets('restorablePushAndRemoveUntil', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushAndRemoveUntil', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -288,7 +289,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('restorablePushAndRemoveUntil adds route on all platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorablePushAndRemoveUntil adds route on all platforms', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -298,7 +299,7 @@ void main() { expect(findRoute('Foo'), findsOneWidget); }); - testWidgets('restorableReplace', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorableReplace', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -334,7 +335,7 @@ void main() { expect(findRoute('Bar'), findsNothing); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('restorableReplace adds route on all platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorableReplace adds route on all platforms', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -346,7 +347,7 @@ void main() { expect(findRoute('Foo'), findsOneWidget); }); - testWidgets('restorableReplaceRouteBelow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorableReplaceRouteBelow', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -392,7 +393,7 @@ void main() { expect(findRoute('Anchor', count: 2), findsOneWidget); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('restorableReplaceRouteBelow adds route on all platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restorableReplaceRouteBelow adds route on all platforms', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home', count: 0), findsOneWidget); @@ -412,7 +413,7 @@ void main() { expect(findRoute('Foo', skipOffstage: false), findsOneWidget); }); - testWidgets('restoring a popped route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restoring a popped route', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -440,7 +441,7 @@ void main() { expect(findRoute('Foo', count: 2), findsOneWidget); }); - testWidgets('popped routes are not restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('popped routes are not restored', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -465,7 +466,7 @@ void main() { expect(findRoute('home', skipOffstage: false), findsOneWidget); }); - testWidgets('routes that are in the process of push are restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('routes that are in the process of push are restored', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -494,7 +495,7 @@ void main() { expect(route2.isActive, isTrue); }); - testWidgets('routes that are in the process of pop are not restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('routes that are in the process of pop are not restored', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -531,7 +532,7 @@ void main() { expect(notifyCount, 1); }); - testWidgets('routes are restored in the right order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('routes are restored in the right order', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1'); @@ -569,7 +570,7 @@ void main() { expect(findRoute('home'), findsOneWidget); }); - testWidgets('all routes up to first unrestorable are restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('all routes up to first unrestorable are restored', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1'); @@ -599,7 +600,7 @@ void main() { expect(findRoute('home', skipOffstage: false), findsOneWidget); }); - testWidgets('removing unrestorable routes restores all of them', (WidgetTester tester) async { + testWidgetsWithLeakTracking('removing unrestorable routes restores all of them', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1'); @@ -633,7 +634,7 @@ void main() { expect(findRoute('home', skipOffstage: false), findsOneWidget); }); - testWidgets('RestorableRouteFuture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableRouteFuture', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -673,7 +674,7 @@ void main() { expect(restoredRouteFuture.enabled, isFalse); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 - testWidgets('RestorableRouteFuture in unrestorable context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableRouteFuture in unrestorable context', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); expect(findRoute('home'), findsOneWidget); @@ -704,7 +705,7 @@ void main() { expect(findRoute('home'), findsOneWidget); }); - testWidgets('Illegal arguments throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Illegal arguments throw', (WidgetTester tester) async { await tester.pumpWidget(const TestWidget()); tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar'); await tester.pumpAndSettle(); @@ -787,7 +788,7 @@ void main() { ); }); - testWidgets('Moving scopes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving scopes', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root', child: TestWidget( @@ -840,7 +841,7 @@ void main() { expect(findRoute('home', count: 0), findsOneWidget); }); - testWidgets('Restoring pages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Restoring pages', (WidgetTester tester) async { await tester.pumpWidget(const PagedTestWidget()); expect(findRoute('home', count: 0), findsOneWidget); await tapRouteCounter('home', tester); @@ -882,7 +883,7 @@ void main() { expect(findRoute('bar', count: 0), findsOneWidget); }); - testWidgets('Unrestorable pages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unrestorable pages', (WidgetTester tester) async { await tester.pumpWidget(const PagedTestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -936,7 +937,7 @@ void main() { expect(findRoute('home', count: 1), findsOneWidget); }); - testWidgets('removed page is not restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('removed page is not restored', (WidgetTester tester) async { await tester.pumpWidget(const PagedTestWidget()); await tapRouteCounter('home', tester); expect(findRoute('home', count: 1), findsOneWidget); @@ -976,7 +977,7 @@ void main() { expect(findRoute('p1', count: 0), findsOneWidget); }); - testWidgets('Helpful assert thrown all routes in onGenerateInitialRoutes are not restorable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Helpful assert thrown all routes in onGenerateInitialRoutes are not restorable', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( restorationScopeId: 'material_app', diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 26d1463639259..8044583a7b276 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -6,9 +6,13 @@ import 'dart:ui' show FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; +import 'navigator_utils.dart'; import 'observer_tester.dart'; import 'semantics_tester.dart'; @@ -624,6 +628,41 @@ void main() { expect(observations[2].previous, '/A'); }); + testWidgetsWithLeakTracking('$Route dispatches memory events', (WidgetTester tester) async { + Future<void> createAndDisposeRoute() async { + final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); + await tester.pumpWidget( + MaterialApp( + navigatorKey: nav, + home: const Scaffold( + body: Text('home'), + ) + ) + ); + + nav.currentState!.push(MaterialPageRoute<void>(builder: (_) => const Placeholder())); // This should create a route + await tester.pumpAndSettle(); + + nav.currentState!.pop(); + await tester.pumpAndSettle(); // this should dispose the route. + } + + final List<ObjectEvent> events = <ObjectEvent>[]; + void listener(ObjectEvent event) { + if (event.object.runtimeType == MaterialPageRoute<void>) { + events.add(event); + } + } + MemoryAllocations.instance.addListener(listener); + + await createAndDisposeRoute(); + expect(events, hasLength(2)); + expect(events.first, isA<ObjectCreated>()); + expect(events.last, isA<ObjectDisposed>()); + + MemoryAllocations.instance.removeListener(listener); + }); + testWidgets('Route didAdd and dispose in same frame work', (WidgetTester tester) async { // Regression Test for https://github.com/flutter/flutter/issues/61346. Widget buildNavigator() { @@ -2871,6 +2910,90 @@ void main() { ); }); + testWidgets('Can pop route with local history entries using page api', (WidgetTester tester) async { + List<Page<void>> myPages = const <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + MaterialPage<void>(child: Text('page2')), + ]; + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + expect(find.text('page2'), findsOneWidget); + final ModalRoute<void> route = ModalRoute.of(tester.element(find.text('page2')))!; + bool entryRemoved = false; + route.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () => entryRemoved = true)); + expect(route.willHandlePopInternally, true); + + myPages = const <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + ]; + + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + expect(find.text('page1'), findsOneWidget); + expect(entryRemoved, isTrue); + }); + + testWidgets('ModalRoute must comply with willHandlePopInternally when there is a PopScope', (WidgetTester tester) async { + const List<Page<void>> myPages = <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + MaterialPage<void>( + child: PopScope( + canPop: false, + child: Text('page2'), + ), + ), + ]; + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + final ModalRoute<void> route = ModalRoute.of(tester.element(find.text('page2')))!; + // PopScope only prevents user trigger action, e.g. Navigator.maybePop. + // The page can still be popped by the system if it needs to. + expect(route.willHandlePopInternally, false); + expect(route.didPop(null), true); + }); + testWidgets('can push and pop pages using page api', (WidgetTester tester) async { late Animation<double> secondaryAnimationOfRouteOne; late Animation<double> primaryAnimationOfRouteOne; @@ -4153,6 +4276,720 @@ void main() { expect(const RouteSettings().toString(), 'RouteSettings(none, null)'); }); }); + + group('Android Predictive Back', () { + bool? lastFrameworkHandlesBack; + setUp(() async { + lastFrameworkHandlesBack = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA<bool>()); + lastFrameworkHandlesBack = methodCall.arguments as bool; + } + return; + }); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/lifecycle', + const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), + (ByteData? data) {}, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + testWidgets('a single route is already defaulted to false', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Text('home'), + ) + ) + ); + + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + testWidgets('navigating around a single Navigator with .pop', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Go to one'), + ), + ], + ), + '/one': (BuildContext context) => _LinksPage( + title: 'Page one', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one/one'); + }, + child: const Text('Go to one/one'), + ), + ], + ), + '/one/one': (BuildContext context) => const _LinksPage( + title: 'Page one - one', + ), + }, + ), + ); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go to one/one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one - one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + testWidgets('navigating around a single Navigator with system back', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Go to one'), + ), + ], + ), + '/one': (BuildContext context) => _LinksPage( + title: 'Page one', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one/one'); + }, + child: const Text('Go to one/one'), + ), + ], + ), + '/one/one': (BuildContext context) => const _LinksPage( + title: 'Page one - one', + ), + }, + ), + ); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go to one/one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one - one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + testWidgets('a single Navigator with a PopScope that defaults to enabled', (WidgetTester tester) async { + bool canPop = true; + late StateSetter setState; + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + canPop: canPop, + ), + }, + ); + }, + ), + ); + + expect(lastFrameworkHandlesBack, isFalse); + + setState(() { + canPop = false; + }); + await tester.pump(); + + expect(lastFrameworkHandlesBack, isTrue); + + setState(() { + canPop = true; + }); + await tester.pump(); + + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + testWidgets('a single Navigator with a PopScope that defaults to disabled', (WidgetTester tester) async { + bool canPop = false; + late StateSetter setState; + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + canPop: canPop, + ), + }, + ); + }, + ), + ); + + expect(lastFrameworkHandlesBack, isTrue); + + setState(() { + canPop = true; + }); + await tester.pump(); + + expect(lastFrameworkHandlesBack, isFalse); + + setState(() { + canPop = false; + }); + await tester.pump(); + + expect(lastFrameworkHandlesBack, isTrue); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + // Test both system back gestures and Navigator.pop. + for (final _BackType backType in _BackType.values) { + testWidgets('navigating around nested Navigators', (WidgetTester tester) async { + final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); + final GlobalKey<NavigatorState> nestedNav = GlobalKey<NavigatorState>(); + Future<void> goBack() async { + switch (backType) { + case _BackType.systemBack: + return simulateSystemBack(); + case _BackType.navigatorPop: + if (nestedNav.currentState != null) { + if (nestedNav.currentState!.mounted && nestedNav.currentState!.canPop()) { + return nestedNav.currentState?.pop(); + } + } + return nav.currentState?.pop(); + } + } + await tester.pumpWidget( + MaterialApp( + navigatorKey: nav, + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Go to one'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/nested'); + }, + child: const Text('Go to nested'), + ), + ], + ), + '/one': (BuildContext context) => _LinksPage( + title: 'Page one', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one/one'); + }, + child: const Text('Go to one/one'), + ), + ], + ), + '/nested': (BuildContext context) => _NestedNavigatorsPage( + navigatorKey: nestedNav, + ), + }, + ), + ); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await goBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to nested')); + await tester.pumpAndSettle(); + + expect(find.text('Nested - home'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go to nested/one')); + await tester.pumpAndSettle(); + + expect(find.text('Nested - page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await goBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested - home'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await goBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + } + + testWidgets('nested Navigators with a nested PopScope', (WidgetTester tester) async { + bool canPop = true; + late StateSetter setState; + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) => _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Go to one'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/nested'); + }, + child: const Text('Go to nested'), + ), + ], + ), + '/one': (BuildContext context) => _LinksPage( + title: 'Page one', + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one/one'); + }, + child: const Text('Go to one/one'), + ), + ], + ), + '/nested': (BuildContext context) => _NestedNavigatorsPage( + popScopePageEnabled: canPop, + ), + }, + ); + }, + ), + ); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to nested')); + await tester.pumpAndSettle(); + + expect(find.text('Nested - home'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go to nested/popscope')); + await tester.pumpAndSettle(); + + expect(find.text('Nested - PopScope'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + // Going back works because canPop is true. + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested - home'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await tester.tap(find.text('Go to nested/popscope')); + await tester.pumpAndSettle(); + + expect(find.text('Nested - PopScope'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + setState(() { + canPop = false; + }); + await tester.pumpAndSettle(); + + expect(lastFrameworkHandlesBack, isTrue); + + // Now going back doesn't work because canPop is false, but it still + // has no effect on the system navigator due to all of the other routes. + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested - PopScope'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + setState(() { + canPop = true; + }); + await tester.pump(); + + expect(lastFrameworkHandlesBack, isTrue); + + // And going back works again after switching canPop back to true. + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Nested - home'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + group('Navigator page API', () { + testWidgets('starting with one route as usual', (WidgetTester tester) async { + late StateSetter builderSetState; + final List<_Page> pages = <_Page>[_Page.home]; + bool canPop() => pages.length <= 1; + + await tester.pumpWidget( + MaterialApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + builderSetState = setState; + return PopScope( + canPop: canPop(), + onPopInvoked: (bool success) { + if (success || pages.last == _Page.noPop) { + return; + } + setState(() { + pages.removeLast(); + }); + }, + child: Navigator( + onPopPage: (Route<void> route, void result) { + if (!route.didPop(null)) { + return false; + } + setState(() { + pages.removeLast(); + }); + return true; + }, + pages: pages.map((_Page page) { + switch (page) { + case _Page.home: + return MaterialPage<void>( + child: _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + setState(() { + pages.add(_Page.one); + }); + }, + child: const Text('Go to _Page.one'), + ), + TextButton( + onPressed: () { + setState(() { + pages.add(_Page.noPop); + }); + }, + child: const Text('Go to _Page.noPop'), + ), + ], + ), + ); + case _Page.one: + return const MaterialPage<void>( + child: _LinksPage( + title: 'Page one', + ), + ); + case _Page.noPop: + return const MaterialPage<void>( + child: _LinksPage( + title: 'Cannot pop page', + canPop: false, + ), + ); + } + }).toList(), + ), + ); + }, + ), + ), + ); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to _Page.one')); + await tester.pumpAndSettle(); + + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + + await tester.tap(find.text('Go to _Page.noPop')); + await tester.pumpAndSettle(); + + expect(find.text('Cannot pop page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Cannot pop page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + // Circumvent "Cannot pop page" by directly modifying pages. + builderSetState(() { + pages.removeLast(); + }); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + + testWidgets('starting with existing route history', (WidgetTester tester) async { + final List<_Page> pages = <_Page>[_Page.home, _Page.one]; + bool canPop() => pages.length <= 1; + + await tester.pumpWidget( + MaterialApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return PopScope( + canPop: canPop(), + onPopInvoked: (bool success) { + if (success || pages.last == _Page.noPop) { + return; + } + setState(() { + pages.removeLast(); + }); + }, + child: Navigator( + onPopPage: (Route<void> route, void result) { + if (!route.didPop(null)) { + return false; + } + setState(() { + pages.removeLast(); + }); + return true; + }, + pages: pages.map((_Page page) { + switch (page) { + case _Page.home: + return MaterialPage<void>( + child: _LinksPage( + title: 'Home page', + buttons: <Widget>[ + TextButton( + onPressed: () { + setState(() { + pages.add(_Page.one); + }); + }, + child: const Text('Go to _Page.one'), + ), + TextButton( + onPressed: () { + setState(() { + pages.add(_Page.noPop); + }); + }, + child: const Text('Go to _Page.noPop'), + ), + ], + ), + ); + case _Page.one: + return const MaterialPage<void>( + child: _LinksPage( + title: 'Page one', + ), + ); + case _Page.noPop: + return const MaterialPage<void>( + child: _LinksPage( + title: 'Cannot pop page', + canPop: false, + ), + ); + } + }).toList(), + ), + ); + }, + ), + ), + ); + + expect(find.text('Home page'), findsNothing); + expect(find.text('Page one'), findsOneWidget); + expect(lastFrameworkHandlesBack, isTrue); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('Home page'), findsOneWidget); + expect(find.text('Page one'), findsNothing); + expect(lastFrameworkHandlesBack, isFalse); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), + skip: isBrowser, // [intended] only non-web Android supports predictive back. + ); + }); + }); } typedef AnnouncementCallBack = void Function(Route<dynamic>?); @@ -4435,3 +5272,153 @@ class TestDependencies extends StatelessWidget { ); } } + +enum _BackType { + systemBack, + navigatorPop, +} + +enum _Page { + home, + one, + noPop, +} + +class _LinksPage extends StatelessWidget { + const _LinksPage ({ + this.buttons = const <Widget>[], + this.canPop, + required this.title, + this.onBack, + }); + + final List<Widget> buttons; + final bool? canPop; + final VoidCallback? onBack; + final String title; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text(title), + ...buttons, + if (Navigator.of(context).canPop()) + TextButton( + onPressed: onBack ?? () { + Navigator.of(context).pop(); + }, + child: const Text('Go back'), + ), + if (canPop != null) + PopScope( + canPop: canPop!, + child: const SizedBox.shrink(), + ), + ], + ), + ), + ); + } +} + +class _NestedNavigatorsPage extends StatefulWidget { + const _NestedNavigatorsPage({ + this.popScopePageEnabled, + this.navigatorKey, + }); + + /// Whether the PopScope on the /popscope page is enabled. + /// + /// If null, then no PopScope is built at all. + final bool? popScopePageEnabled; + + final GlobalKey<NavigatorState>? navigatorKey; + + @override + State<_NestedNavigatorsPage> createState() => _NestedNavigatorsPageState(); +} + +class _NestedNavigatorsPageState extends State<_NestedNavigatorsPage> { + late final GlobalKey<NavigatorState> _navigatorKey; + + @override + void initState() { + super.initState(); + _navigatorKey = widget.navigatorKey ?? GlobalKey<NavigatorState>(); + } + + @override + Widget build(BuildContext context) { + final BuildContext rootContext = context; + return NavigatorPopHandler( + onPop: () { + if (widget.popScopePageEnabled == false) { + return; + } + _navigatorKey.currentState!.pop(); + }, + child: Navigator( + key: _navigatorKey, + initialRoute: '/', + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return _LinksPage( + title: 'Nested - home', + onBack: () { + Navigator.of(rootContext).pop(); + }, + buttons: <Widget>[ + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Go to nested/one'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/popscope'); + }, + child: const Text('Go to nested/popscope'), + ), + TextButton( + onPressed: () { + Navigator.of(rootContext).pop(); + }, + child: const Text('Go back out of nested nav'), + ), + ], + ); + }, + ); + case '/one': + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return const _LinksPage( + title: 'Nested - page one', + ); + }, + ); + case '/popscope': + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return _LinksPage( + canPop: widget.popScopePageEnabled, + title: 'Nested - PopScope', + ); + }, + ); + default: + throw Exception('Invalid route: ${settings.name}'); + } + }, + ), + ); + } +} diff --git a/packages/flutter/test/widgets/navigator_utils.dart b/packages/flutter/test/widgets/navigator_utils.dart new file mode 100644 index 0000000000000..46f1f9b1ac469 --- /dev/null +++ b/packages/flutter/test/widgets/navigator_utils.dart @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Simulates a system back, like a back gesture on Android. +/// +/// Sends the same platform channel message that the engine sends when it +/// receives a system back. +Future<void> simulateSystemBack() { + return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/navigation', + const JSONMessageCodec().encodeMessage(<String, dynamic>{ + 'method': 'popRoute', + }), + (ByteData? _) {}, + ); +} diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index c4b6e271aa8b5..6527110b86e86 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; @@ -109,7 +110,7 @@ Widget buildTest({ } void main() { - testWidgets('ScrollDirection test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollDirection test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/107101 final List<ScrollDirection> receivedResult = <ScrollDirection>[]; const List<ScrollDirection> expectedReverseResult = <ScrollDirection>[ScrollDirection.reverse, ScrollDirection.idle]; @@ -211,7 +212,7 @@ void main() { expect(context.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView overscroll and release and hold', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(find.text('aaa2'), findsOneWidget); await tester.pump(const Duration(milliseconds: 250)); @@ -234,7 +235,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView overscroll and release and hold', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(find.text('aaa2'), findsOneWidget); await tester.pump(const Duration(milliseconds: 250)); @@ -259,7 +260,7 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('NestedScrollView overscroll and release', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView overscroll and release', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(find.text('aaa2'), findsOneWidget); await tester.pump(const Duration(milliseconds: 500)); @@ -275,7 +276,7 @@ void main() { expect(find.text('aaa2'), findsOneWidget); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('NestedScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView', (WidgetTester tester) async { await tester.pumpWidget(buildTest()); expect(find.text('aaa2'), findsOneWidget); expect(find.text('aaa3'), findsNothing); @@ -342,10 +343,11 @@ void main() { ); }); - testWidgets('NestedScrollView with a ScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView with a ScrollController', (WidgetTester tester) async { final ScrollController controller = ScrollController( initialScrollOffset: 50.0, ); + addTearDown(controller.dispose); late double scrollOffset; controller.addListener(() { @@ -419,8 +421,9 @@ void main() { expect(find.text('ddd1'), findsOneWidget); }); - testWidgets('Three NestedScrollViews with one ScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Three NestedScrollViews with one ScrollController', (WidgetTester tester) async { final TrackingScrollController controller = TrackingScrollController(); + addTearDown(controller.dispose); expect(controller.mostRecentlyUpdatedPosition, isNull); expect(controller.initialScrollOffset, 0.0); @@ -482,7 +485,7 @@ void main() { ); }); - testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollViews with custom physics', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Localizations( @@ -520,7 +523,7 @@ void main() { expect(point1.dy, greaterThan(point2.dy)); }); - testWidgets('NestedScrollViews respect NeverScrollableScrollPhysics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollViews respect NeverScrollableScrollPhysics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/113753 await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -561,7 +564,7 @@ void main() { expect(point1, point2); }); - testWidgets('NestedScrollView and internal scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView and internal scrolling', (WidgetTester tester) async { debugDisableShadows = false; const List<String> tabs = <String>['Hello', 'World']; int buildCount = 0; @@ -843,7 +846,7 @@ void main() { debugDisableShadows = true; }); - testWidgets('NestedScrollView and bouncing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView and bouncing', (WidgetTester tester) async { // This verifies that overscroll bouncing works correctly on iOS. For // example, this checks that if you pull to overscroll, friction is applied; // it also makes sure that if you scroll back the other way, the scroll @@ -946,7 +949,7 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); group('NestedScrollViewState exposes inner and outer controllers', () { - testWidgets('Scrolling by less than the outer extent does not scroll the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling by less than the outer extent does not scroll the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey, @@ -978,7 +981,7 @@ void main() { expect(globalKey.currentState!.innerController.offset, 0.0); }); - testWidgets('Scrolling by exactly the outer extent does not scroll the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling by exactly the outer extent does not scroll the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey, @@ -1010,7 +1013,7 @@ void main() { expect(globalKey.currentState!.innerController.offset, 0.0); }); - testWidgets('Scrolling by greater than the outer extent scrolls the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling by greater than the outer extent scrolls the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey, @@ -1046,7 +1049,7 @@ void main() { ); }); - testWidgets('Inertia-cancel event does not modify either position.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inertia-cancel event does not modify either position.', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey, @@ -1094,7 +1097,7 @@ void main() { ); }); - testWidgets('scrolling by less than the expanded outer extent does not scroll the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolling by less than the expanded outer extent does not scroll the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest(key: globalKey)); @@ -1123,7 +1126,7 @@ void main() { expect(globalKey.currentState!.innerController.offset, 0.0); }); - testWidgets('scrolling by exactly the expanded outer extent does not scroll the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolling by exactly the expanded outer extent does not scroll the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest(key: globalKey)); @@ -1152,7 +1155,7 @@ void main() { expect(globalKey.currentState!.innerController.offset, 0.0); }); - testWidgets('scrolling by greater than the expanded outer extent scrolls the inner body', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolling by greater than the expanded outer extent scrolls the inner body', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); await tester.pumpWidget(buildTest(key: globalKey)); @@ -1182,11 +1185,12 @@ void main() { expect(globalKey.currentState!.innerController.offset, 50.0); }); - testWidgets( + testWidgetsWithLeakTracking( 'NestedScrollViewState.outerController should correspond to NestedScrollView.controller', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey = GlobalKey(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(buildTest( controller: scrollController, @@ -1213,7 +1217,7 @@ void main() { ); group('manipulating controllers when', () { - testWidgets('outer: not scrolled, inner: not scrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outer: not scrolled, inner: not scrolled', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey1, @@ -1255,7 +1259,7 @@ void main() { expect(globalKey2.currentState!.outerController.position.pixels, 0.0); }); - testWidgets('outer: not scrolled, inner: scrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outer: not scrolled, inner: scrolled', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey1, @@ -1299,7 +1303,7 @@ void main() { expect(globalKey2.currentState!.outerController.position.pixels, 0.0); }); - testWidgets('outer: scrolled, inner: not scrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outer: scrolled, inner: not scrolled', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey1, @@ -1343,7 +1347,7 @@ void main() { expect(globalKey2.currentState!.outerController.position.pixels, 0.0); }); - testWidgets('outer: scrolled, inner: scrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('outer: scrolled, inner: scrolled', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey(); await tester.pumpWidget(buildTest( key: globalKey1, @@ -1392,7 +1396,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/39963. - testWidgets('NestedScrollView with SliverOverlapAbsorber in or out of the first screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView with SliverOverlapAbsorber in or out of the first screen', (WidgetTester tester) async { await tester.pumpWidget(const _TestLayoutExtentIsNegative(1)); await tester.pumpWidget(const _TestLayoutExtentIsNegative(10)); }); @@ -1471,7 +1475,7 @@ void main() { return geometry.paintExtent; } - testWidgets('float', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( floating: true, @@ -1523,7 +1527,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true); }); - testWidgets('float expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float expanded', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( floating: true, @@ -1578,7 +1582,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 200.0, visible: true); }); - testWidgets('float with pointer signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float with pointer signal', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( floating: true, @@ -1635,7 +1639,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true); }); - testWidgets('snap with pointer signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('snap with pointer signal', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( floating: true, @@ -1689,7 +1693,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false); }); - testWidgets('float expanded with pointer signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float expanded with pointer signal', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( floating: true, @@ -1749,7 +1753,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 200.0, visible: true); }); - testWidgets('only snap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('only snap', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); final GlobalKey<NestedScrollViewState> nestedKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( @@ -1884,7 +1888,7 @@ void main() { expect(nestedKey.currentState!.outerController.offset, 56.0); }); - testWidgets('only snap expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('only snap expanded', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); final GlobalKey<NestedScrollViewState> nestedKey = GlobalKey(); await tester.pumpWidget(buildFloatTest( @@ -2020,7 +2024,7 @@ void main() { expect(nestedKey.currentState!.outerController.offset, 200.0); }); - testWidgets('float pinned', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float pinned', (WidgetTester tester) async { // This configuration should have the same behavior of a pinned app bar. // No floating should happen, and the app bar should persist. final GlobalKey appBarKey = GlobalKey(); @@ -2075,7 +2079,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true); }); - testWidgets('float pinned expanded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float pinned expanded', (WidgetTester tester) async { // Only the expanded portion (flexible space) of the app bar should float // in and out. final GlobalKey appBarKey = GlobalKey(); @@ -2134,7 +2138,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 200.0, visible: true); }); - testWidgets('float pinned with pointer signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float pinned with pointer signal', (WidgetTester tester) async { // This configuration should have the same behavior of a pinned app bar. // No floating should happen, and the app bar should persist. final GlobalKey appBarKey = GlobalKey(); @@ -2194,7 +2198,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true); }); - testWidgets('float pinned expanded with pointer signal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('float pinned expanded with pointer signal', (WidgetTester tester) async { // Only the expanded portion (flexible space) of the app bar should float // in and out. final GlobalKey appBarKey = GlobalKey(); @@ -2289,10 +2293,11 @@ void main() { ); } - testWidgets('overscroll, hold for 0 velocity, and release', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overscroll, hold for 0 velocity, and release', (WidgetTester tester) async { // Dragging into an overscroll and holding so that when released, the // ballistic scroll activity has a 0 velocity. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildBallisticTest(controller)); // Last item of the inner scroll view. expect(find.text('Item 49'), findsNothing); @@ -2316,10 +2321,11 @@ void main() { expect(tester.getCenter(find.text('Item 49')).dy, equals(585.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('overscroll, release, and tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overscroll, release, and tap', (WidgetTester tester) async { // Tapping while an inner ballistic scroll activity is in progress will // trigger a secondary ballistic scroll activity with a 0 velocity. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildBallisticTest(controller)); // Last item of the inner scroll view. expect(find.text('Item 49'), findsNothing); @@ -2350,7 +2356,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/63978 - testWidgets('Inner _NestedScrollPosition.applyClampedDragUpdate correctly calculates range when in overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inner _NestedScrollPosition.applyClampedDragUpdate correctly calculates range when in overscroll', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> nestedScrollView = GlobalKey(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -2404,8 +2410,9 @@ void main() { expect(nestedScrollView.currentState!.innerController.position.pixels, 295.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Scroll pointer signal should not cause overscroll.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll pointer signal should not cause overscroll.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildTest(controller: controller)); final Offset scrollEventLocation = tester.getCenter(find.byType(NestedScrollView)); @@ -2426,7 +2433,7 @@ void main() { expect(find.text('ddd1'), findsOneWidget); }); - testWidgets('NestedScrollView basic scroll with pointer signal', (WidgetTester tester) async{ + testWidgetsWithLeakTracking('NestedScrollView basic scroll with pointer signal', (WidgetTester tester) async{ await tester.pumpWidget(buildTest()); expect(find.text('aaa2'), findsOneWidget); expect(find.text('aaa3'), findsNothing); @@ -2466,12 +2473,13 @@ void main() { }); // Related to https://github.com/flutter/flutter/issues/64266 - testWidgets( + testWidgetsWithLeakTracking( 'Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse', (WidgetTester tester) async { ScrollDirection? lastUserScrollingDirection; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildTest(controller: controller)); controller.addListener(() { @@ -2503,7 +2511,7 @@ void main() { ); // Regression test for https://github.com/flutter/flutter/issues/72257 - testWidgets('NestedScrollView works well when rebuilding during scheduleWarmUpFrame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView works well when rebuilding during scheduleWarmUpFrame', (WidgetTester tester) async { bool? isScrolled; final Widget myApp = MaterialApp( home: Scaffold( @@ -2546,8 +2554,9 @@ void main() { }); // Regression test of https://github.com/flutter/flutter/issues/74372 - testWidgets('ScrollPosition can be accessed during `_updatePosition()`', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollPosition can be accessed during `_updatePosition()`', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); late ScrollPosition position; Widget buildFrame({ScrollPhysics? physics}) { @@ -2592,7 +2601,7 @@ void main() { expect(position.pixels, 0.0); }); - testWidgets("NestedScrollView doesn't crash due to precision error", (WidgetTester tester) async { + testWidgetsWithLeakTracking("NestedScrollView doesn't crash due to precision error", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/63825 await tester.pumpWidget(MaterialApp( @@ -2639,7 +2648,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('NestedScrollViewCoordinator.pointerScroll dispatches correct scroll notifications', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollViewCoordinator.pointerScroll dispatches correct scroll notifications', (WidgetTester tester) async { int scrollEnded = 0; int scrollStarted = 0; bool isScrolled = false; @@ -2701,7 +2710,7 @@ void main() { expect(scrollEnded, 2); }); - testWidgets('SliverAppBar.medium collapses in NestedScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.medium collapses in NestedScrollView', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> nestedScrollView = GlobalKey(); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 112; @@ -2783,7 +2792,7 @@ void main() { expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); }); - testWidgets('SliverAppBar.large collapses in NestedScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar.large collapses in NestedScrollView', (WidgetTester tester) async { final GlobalKey<NestedScrollViewState> nestedScrollView = GlobalKey(); const double collapsedAppBarHeight = 64; const double expandedAppBarHeight = 152; @@ -2866,7 +2875,7 @@ void main() { expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); }); - testWidgets('NestedScrollView does not crash when inner scrollable changes while scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView does not crash when inner scrollable changes while scrolling', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/126454. Widget buildApp({required bool nested}) { final Widget innerScrollable = ListView( @@ -2903,7 +2912,7 @@ void main() { await tester.pumpWidget(buildApp(nested: true)); }); - testWidgets('SliverOverlapInjector asserts when there is no SliverOverlapAbsorber', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverOverlapInjector asserts when there is no SliverOverlapAbsorber', (WidgetTester tester) async { Widget buildApp() { return MaterialApp( home: Scaffold( @@ -2997,7 +3006,8 @@ void main() { ) ); } - testWidgets('when headerSliverBuilder is empty', (WidgetTester tester) async { + + testWidgetsWithLeakTracking('when headerSliverBuilder is empty', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/117316 // Regression test for https://github.com/flutter/flutter/issues/46089 // Short body / long body @@ -3015,7 +3025,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('when headerSliverBuilder extent is 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when headerSliverBuilder extent is 0', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/79077 // Short body / long body for (final _BodyLength bodyLength in _BodyLength.values) { @@ -3169,7 +3179,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('With a pinned SliverAppBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('With a pinned SliverAppBar', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110956 // Regression test for https://github.com/flutter/flutter/issues/127282 // Regression test for https://github.com/flutter/flutter/issues/32563 @@ -3200,6 +3210,13 @@ void main() { } }); }); + + testWidgetsWithLeakTracking('$SliverOverlapAbsorberHandle dispatches creation in constructor', (WidgetTester widgetTester) async { + await expectLater( + await memoryEvents(() => SliverOverlapAbsorberHandle().dispose(), SliverOverlapAbsorberHandle), + areCreateAndDispose, + ); + }); } double appBarHeight(WidgetTester tester) => tester.getSize(find.byType(AppBar, skipOffstage: false)).height; diff --git a/packages/flutter/test/widgets/notification_test.dart b/packages/flutter/test/widgets/notification_test.dart index e58d86f2f06e0..ddfa0d23111e8 100644 --- a/packages/flutter/test/widgets/notification_test.dart +++ b/packages/flutter/test/widgets/notification_test.dart @@ -4,15 +4,16 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class MyNotification extends Notification { } void main() { - testWidgets('Notification basics - toString', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Notification basics - toString', (WidgetTester tester) async { expect(MyNotification(), hasOneLineDescription); }); - testWidgets('Notification basics - dispatch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Notification basics - dispatch', (WidgetTester tester) async { final List<dynamic> log = <dynamic>[]; final GlobalKey key = GlobalKey(); await tester.pumpWidget(NotificationListener<MyNotification>( @@ -36,7 +37,7 @@ void main() { expect(log, <dynamic>['b', notification, 'a', notification]); }); - testWidgets('Notification basics - cancel', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Notification basics - cancel', (WidgetTester tester) async { final List<dynamic> log = <dynamic>[]; final GlobalKey key = GlobalKey(); await tester.pumpWidget(NotificationListener<MyNotification>( @@ -60,7 +61,7 @@ void main() { expect(log, <dynamic>['b', notification]); }); - testWidgets('Notification basics - listener null return value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Notification basics - listener null return value', (WidgetTester tester) async { final List<Type> log = <Type>[]; final GlobalKey key = GlobalKey(); await tester.pumpWidget(NotificationListener<MyNotification>( @@ -77,7 +78,7 @@ void main() { expect(log, <Type>[MyNotification]); }); - testWidgets('Notification basics - listener null return value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Notification basics - listener null return value', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); final ScrollMetricsNotification n1 = ScrollMetricsNotification( metrics: FixedScrollMetrics( diff --git a/packages/flutter/test/widgets/obscured_animated_image_test.dart b/packages/flutter/test/widgets/obscured_animated_image_test.dart index 788d8f2d4905f..179df2c1a8e0b 100644 --- a/packages/flutter/test/widgets/obscured_animated_image_test.dart +++ b/packages/flutter/test/widgets/obscured_animated_image_test.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui show Image; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; import '../painting/fake_codec.dart'; @@ -17,7 +18,7 @@ Future<void> main() async { final FakeCodec fakeCodec = await FakeCodec.fromData(Uint8List.fromList(kAnimatedGif)); final FakeImageProvider fakeImageProvider = FakeImageProvider(fakeCodec); - testWidgets('Obscured image does not animate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Obscured image does not animate', (WidgetTester tester) async { final GlobalKey imageKey = GlobalKey(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/opacity_repaint_test.dart b/packages/flutter/test/widgets/opacity_repaint_test.dart index b0aff87cd6753..c858380ef5a11 100644 --- a/packages/flutter/test/widgets/opacity_repaint_test.dart +++ b/packages/flutter/test/widgets/opacity_repaint_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('RenderOpacity avoids repainting and does not drop layer at fully opaque', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderOpacity avoids repainting and does not drop layer at fully opaque', (WidgetTester tester) async { RenderTestObject.paintCount = 0; await tester.pumpWidget( const ColoredBox( @@ -46,7 +47,7 @@ void main() { expect(RenderTestObject.paintCount, 1); }); - testWidgets('RenderOpacity allows opacity layer to be dropped at 0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderOpacity allows opacity layer to be dropped at 0 opacity', (WidgetTester tester) async { RenderTestObject.paintCount = 0; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/opacity_test.dart b/packages/flutter/test/widgets/opacity_test.dart index 804f35a0dbbc8..a638dcfe2029a 100644 --- a/packages/flutter/test/widgets/opacity_test.dart +++ b/packages/flutter/test/widgets/opacity_test.dart @@ -7,15 +7,17 @@ @Tags(<String>['reduced-test-set']) library; +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Opacity', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // Opacity 1.0: Semantics and painting @@ -152,7 +154,7 @@ void main() { semantics.dispose(); }); - testWidgets('offset is correctly handled in Opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('offset is correctly handled in Opacity', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -185,7 +187,7 @@ void main() { ); }); - testWidgets('empty opacity does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('empty opacity does not crash', (WidgetTester tester) async { await tester.pumpWidget( RepaintBoundary(child: Opacity(opacity: 0.5, child: Container())), ); @@ -193,10 +195,11 @@ void main() { // The following line will send the layer to engine and cause crash if an // empty opacity layer is sent. final OffsetLayer offsetLayer = element.renderObject!.debugLayer! as OffsetLayer; - await offsetLayer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0)); + final ui.Image image = await offsetLayer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0)); + image.dispose(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857 - testWidgets('Child shows up in the right spot when opacity is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Child shows up in the right spot when opacity is disabled', (WidgetTester tester) async { debugDisableOpacityLayers = true; final GlobalKey key = GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/overflow_bar_test.dart b/packages/flutter/test/widgets/overflow_bar_test.dart index d8e9520b20cd2..3145df151320a 100644 --- a/packages/flutter/test/widgets/overflow_bar_test.dart +++ b/packages/flutter/test/widgets/overflow_bar_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('OverflowBar documented defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar documented defaults', (WidgetTester tester) async { const OverflowBar bar = OverflowBar(); expect(bar.spacing, 0); expect(bar.alignment, null); @@ -17,7 +18,7 @@ void main() { expect(bar.children, const <Widget>[]); }); - testWidgets('Empty OverflowBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty OverflowBar', (WidgetTester tester) async { const Size size = Size(16, 24); await tester.pumpWidget( @@ -46,7 +47,7 @@ void main() { expect(tester.getSize(find.byType(OverflowBar)), Size.zero); }); - testWidgets('OverflowBar horizontal layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar horizontal layout', (WidgetTester tester) async { final Key child1Key = UniqueKey(); final Key child2Key = UniqueKey(); final Key child3Key = UniqueKey(); @@ -93,7 +94,7 @@ void main() { expect(tester.getRect(find.byKey(child1Key)), const Rect.fromLTRB(10.0 + 96 + 10.0, 8, 10.0 + 10.0 + 144, 56)); }); - testWidgets('OverflowBar vertical layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar vertical layout', (WidgetTester tester) async { final Key child1Key = UniqueKey(); final Key child2Key = UniqueKey(); final Key child3Key = UniqueKey(); @@ -174,7 +175,7 @@ void main() { expect(tester.getRect(find.byKey(child3Key)), const Rect.fromLTRB(100.0/2.0 - 32/2, 112, 100.0/2.0 + 32/2, 144)); }); - testWidgets('OverflowBar intrinsic width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar intrinsic width', (WidgetTester tester) async { Widget buildFrame({ required double width }) { return Directionality( textDirection: TextDirection.ltr, @@ -205,7 +206,7 @@ void main() { expect(tester.getSize(find.byType(OverflowBar)).width, 150); }); - testWidgets('OverflowBar intrinsic height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar intrinsic height', (WidgetTester tester) async { Widget buildFrame({ required double maxWidth }) { return Directionality( textDirection: TextDirection.ltr, @@ -237,7 +238,7 @@ void main() { }); - testWidgets('OverflowBar is wider that its intrinsic width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar is wider that its intrinsic width', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); @@ -273,7 +274,7 @@ void main() { expect(tester.getTopLeft(find.byKey(key2)).dx, 600); }); - testWidgets('OverflowBar with alignment should match Row with mainAxisAlignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBar with alignment should match Row with mainAxisAlignment', (WidgetTester tester) async { final Key key0 = UniqueKey(); final Key key1 = UniqueKey(); final Key key2 = UniqueKey(); diff --git a/packages/flutter/test/widgets/overflow_box_test.dart b/packages/flutter/test/widgets/overflow_box_test.dart index 2cb07611145f4..bc97f50e7f094 100644 --- a/packages/flutter/test/widgets/overflow_box_test.dart +++ b/packages/flutter/test/widgets/overflow_box_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('OverflowBox control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBox control test', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Align( alignment: Alignment.bottomRight, @@ -30,7 +31,7 @@ void main() { expect(box.size, equals(const Size(100.0, 50.0))); }); - testWidgets('OverflowBox implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('OverflowBox implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const OverflowBox( minWidth: 1.0, @@ -50,7 +51,7 @@ void main() { ]); }); - testWidgets('SizedOverflowBox alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedOverflowBox alignment', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.rtl, @@ -73,7 +74,7 @@ void main() { ); }); - testWidgets('SizedOverflowBox alignment (direction-sensitive)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedOverflowBox alignment (direction-sensitive)', (WidgetTester tester) async { final GlobalKey inner = GlobalKey(); await tester.pumpWidget(Directionality( textDirection: TextDirection.rtl, diff --git a/packages/flutter/test/widgets/overlay_portal_test.dart b/packages/flutter/test/widgets/overlay_portal_test.dart index 702a5db64058d..22d624b9c0b76 100644 --- a/packages/flutter/test/widgets/overlay_portal_test.dart +++ b/packages/flutter/test/widgets/overlay_portal_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _ManyRelayoutBoundaries extends StatelessWidget { const _ManyRelayoutBoundaries({ @@ -94,18 +95,20 @@ void main() { _PaintOrder.paintOrder.clear(); }); - testWidgets('The overlay child sees the right inherited widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The overlay child sees the right inherited widgets', (WidgetTester tester) async { int buildCount = 0; TextDirection? directionSeenByOverlayChild; TextDirection textDirection = TextDirection.rtl; late StateSetter setState; + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -141,13 +144,16 @@ void main() { expect(directionSeenByOverlayChild, textDirection); }); - testWidgets('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + final Widget widget = Directionality( key: GlobalKey(debugLabel: 'key'), textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( controller: controller1, @@ -164,14 +170,17 @@ void main() { await tester.pumpWidget(SizedBox(child: widget)); }); - testWidgets('Safe to hide overlay child and remove OverlayPortal in the same frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe to hide overlay child and remove OverlayPortal in the same frame', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/129025. + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + final Widget widget = Directionality( key: GlobalKey(debugLabel: 'key'), textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( controller: controller1, @@ -192,7 +201,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Safe to hide overlay child and reparent OverlayPortal in the same frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe to hide overlay child and reparent OverlayPortal in the same frame', (WidgetTester tester) async { final OverlayPortal overlayPortal = OverlayPortal( key: GlobalKey(debugLabel: 'key'), controller: controller1, @@ -202,12 +211,14 @@ void main() { List<Widget> children = <Widget>[ const SizedBox(), overlayPortal ]; + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); late StateSetter setState; final Widget widget = Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry( + overlayEntry = OverlayStatefulEntry( builder: (BuildContext context, StateSetter setter) { setState = setter; return Column(children: children); @@ -228,13 +239,16 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Safe to hide overlay child and reparent OverlayPortal in the same frame 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe to hide overlay child and reparent OverlayPortal in the same frame 2', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + final Widget widget = Directionality( key: GlobalKey(debugLabel: 'key'), textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( controller: controller1, @@ -255,6 +269,45 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgetsWithLeakTracking('No relayout boundary between OverlayPortal and Overlay', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/133545. + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + final GlobalKey key = GlobalKey(debugLabel: 'key'); + + final Widget widget = Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: <OverlayEntry>[ + overlayEntry = OverlayEntry( + builder: (BuildContext context) { + // The Positioned widget prevents a relayout boundary from being + // introduced between the Overlay and OverlayPortal. + return Positioned( + top: 0, + left: 0, + child: OverlayPortal( + controller: controller1, + overlayChildBuilder: (BuildContext context) => SizedBox(key: key), + child: const SizedBox(), + ), + ); + }, + ), + ], + ), + ); + + controller1.hide(); + await tester.pumpWidget(widget); + + controller1.show(); + await tester.pump(); + expect(find.byKey(key), findsOneWidget); + expect(tester.takeException(), isNull); + verifyTreeIsClean(); + }); + testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async { final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller'); final Widget widget = Directionality( @@ -290,14 +343,17 @@ void main() { ); }); - testWidgets('show/hide works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('show/hide works', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller'); + const Widget target = SizedBox(); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( controller: controller, @@ -328,13 +384,16 @@ void main() { expect(find.byWidget(target), findsOneWidget); }); - testWidgets('overlayChildBuilder is not evaluated until show is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlayChildBuilder is not evaluated until show is called', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller'); + final Widget widget = Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( controller: controller, @@ -351,16 +410,18 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('overlay child can use Positioned', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlay child can use Positioned', (WidgetTester tester) async { double dimensions = 30; late StateSetter setState; + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -397,9 +458,11 @@ void main() { expect(tester.getSize(find.byType(Placeholder)), const Size(50, 50)) ; }); - testWidgets('overlay child can be hit tested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overlay child can be hit tested', (WidgetTester tester) async { double offset = 0; late StateSetter setState; + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); bool isHit = false; await tester.pumpWidget( @@ -407,7 +470,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setter) { @@ -452,13 +515,16 @@ void main() { expect(isHit, true); }); - testWidgets('works in a LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works in a LayoutBuilder', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -479,7 +545,9 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('works in a LayoutBuilder 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works in a LayoutBuilder 2', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); late StateSetter setState; bool shouldShowChild = false; @@ -496,7 +564,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter setter) { + overlayEntry = OverlayStatefulEntry(builder: (BuildContext context, StateSetter setter) { setState = setter; return OverlayPortal( controller: controller1, @@ -516,7 +584,55 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('throws when no Overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('works in a LayoutBuilder 3', (WidgetTester tester) async { + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + late StateSetter setState; + bool shouldShowChild = false; + + Widget layoutBuilder(BuildContext context, BoxConstraints constraints) { + return OverlayPortal( + controller: controller2, + overlayChildBuilder: (BuildContext context) => const SizedBox(), + child: const SizedBox(), + ); + } + controller1.hide(); + controller2.hide(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: <OverlayEntry>[ + overlayEntry = OverlayStatefulEntry(builder: (BuildContext context, StateSetter setter) { + setState = setter; + // The Positioned widget ensures there's no relayout boundary + // between the Overlay and the OverlayPortal. + return Positioned( + top: 0, + left: 0, + child: OverlayPortal( + controller: controller1, + overlayChildBuilder: (BuildContext context) => const SizedBox(), + child: shouldShowChild ? LayoutBuilder(builder: layoutBuilder) : null, + ), + ); + }), + ], + ), + ), + ); + + controller1.show(); + controller2.show(); + setState(() { shouldShowChild = true; }); + + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgetsWithLeakTracking('throws when no Overlay', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -544,10 +660,12 @@ void main() { ); }); - testWidgets('widget is laid out before overlay child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('widget is laid out before overlay child', (WidgetTester tester) async { final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget'); final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); int layoutCount = 0; await tester.pumpWidget( @@ -555,7 +673,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) { return OverlayPortal( @@ -597,12 +715,16 @@ void main() { verifyTreeIsClean(); }); - testWidgets('adding/removing overlay child does not redirty overlay more than once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('adding/removing overlay child does not redirty overlay more than once', (WidgetTester tester) async { final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget'); final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay'); final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); int layoutCount = 0; controller1.hide(); @@ -613,8 +735,8 @@ void main() { key: overlayKey, initialEntries: <OverlayEntry>[ // Overlay.performLayout will call layoutCounter.layout. - OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)), - OverlayEntry( + overlayEntry1 = OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)), + overlayEntry2 = OverlayEntry( builder: (BuildContext context) { return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) { return OverlayPortal( @@ -655,11 +777,13 @@ void main() { verifyTreeIsClean(); }); - testWidgets('Adding/Removing OverlayPortal in LayoutBuilder during layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Adding/Removing OverlayPortal in LayoutBuilder during layout', (WidgetTester tester) async { final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget'); final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay'); controller1.hide(); late StateSetter setState; + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); Size size = Size.zero; final Widget overlayPortal = OverlayPortal( @@ -675,7 +799,7 @@ void main() { child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry = OverlayEntry( builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter stateSetter) { @@ -875,7 +999,7 @@ void main() { }); group('GlobalKey Reparenting', () { - testWidgets('child is laid out before overlay child after OverlayEntry shuffle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child is laid out before overlay child after OverlayEntry shuffle', (WidgetTester tester) async { int layoutCount = 0; final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget'); @@ -900,8 +1024,11 @@ void main() { }), ); }); + addTearDown(() => overlayEntry1..remove()..dispose()); final OverlayEntry overlayEntry2 = OverlayEntry(builder: (BuildContext context) => const Placeholder()); + addTearDown(() => overlayEntry2..remove()..dispose()); final OverlayEntry overlayEntry3 = OverlayEntry(builder: (BuildContext context) => const Placeholder()); + addTearDown(() => overlayEntry3..remove()..dispose()); await tester.pumpWidget( Directionality( @@ -1012,7 +1139,7 @@ void main() { expect(layoutCount2, 1); }); - testWidgets('Swap child and overlayChild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swap child and overlayChild', (WidgetTester tester) async { final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); @@ -1023,12 +1150,15 @@ void main() { final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox); final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + overlayEntry = OverlayEntry(builder: (BuildContext context) { return _ManyRelayoutBoundaries( levels: 50, child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) { @@ -1051,7 +1181,7 @@ void main() { verifyTreeIsClean(); }); - testWidgets('forgetChild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('forgetChild', (WidgetTester tester) async { final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints()); @@ -1063,6 +1193,11 @@ void main() { final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox); final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + controller1.hide(); await tester.pumpWidget( @@ -1070,7 +1205,7 @@ void main() { textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + overlayEntry1 = OverlayEntry(builder: (BuildContext context) { return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) { setState2 = stateSetter; return OverlayPortal( @@ -1080,7 +1215,7 @@ void main() { ); }); }), - OverlayEntry(builder: (BuildContext context) { + overlayEntry2 = OverlayEntry(builder: (BuildContext context) { return _ManyRelayoutBoundaries( levels: 50, child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) { @@ -1161,7 +1296,7 @@ void main() { verifyTreeIsClean(); }); - testWidgets('Paint order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paint order', (WidgetTester tester) async { final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget'); final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget'); @@ -1202,12 +1337,15 @@ void main() { ], ); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + overlayEntry = OverlayEntry(builder: (BuildContext context) { return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) { setState = stateSetter; return widget; @@ -1283,21 +1421,26 @@ void main() { setState2 = null; }); - testWidgets('between OverlayEntry & overlayChild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between OverlayEntry & overlayChild', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry1 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; // WidgetToRenderBoxAdapter is keyed by the render box. return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1); }), - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry2 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState2 = stateSetter; return OverlayPortal( controller: controller1, @@ -1326,20 +1469,25 @@ void main() { expect(counter2.layoutCount, 3); }); - testWidgets('between OverlayEntry & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between OverlayEntry & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry1 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1); }), - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry2 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState2 = stateSetter; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -1372,16 +1520,21 @@ void main() { expect(counter2.layoutCount, 3); }); - testWidgets('between overlayChild & overlayChild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between overlayChild & overlayChild', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry1 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; return OverlayPortal( // WidgetToRenderBoxAdapter is keyed by the render box. @@ -1390,7 +1543,7 @@ void main() { child: const SizedBox(), ); }), - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry2 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState2 = stateSetter; return OverlayPortal( controller: controller2, @@ -1419,16 +1572,21 @@ void main() { expect(counter2.layoutCount, 3); }); - testWidgets('between overlayChild & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between overlayChild & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry1 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -1440,7 +1598,7 @@ void main() { } ); }), - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry2 = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState2 = stateSetter; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -1473,16 +1631,19 @@ void main() { expect(counter2.layoutCount, 3); }); - testWidgets('between child & overlayChild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between child & overlayChild', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; return OverlayPortal( // WidgetToRenderBoxAdapter is keyed by the render box. @@ -1512,16 +1673,19 @@ void main() { expect(counter2.layoutCount, 3); }); - testWidgets('between child & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('between child & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async { final _RenderLayoutCounter counter1 = _RenderLayoutCounter(); final _RenderLayoutCounter counter2 = _RenderLayoutCounter(); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { + overlayEntry = OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) { setState1 = stateSetter; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -1556,19 +1720,25 @@ void main() { }); }); - testWidgets('Safe to move the overlay child to a different Overlay and remove the old Overlay', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Safe to move the overlay child to a different Overlay and remove the old Overlay', (WidgetTester tester) async { controller1.show(); final GlobalKey key = GlobalKey(debugLabel: 'key'); final GlobalKey oldOverlayKey = GlobalKey(debugLabel: 'old overlay'); final GlobalKey newOverlayKey = GlobalKey(debugLabel: 'new overlay'); final GlobalKey overlayChildKey = GlobalKey(debugLabel: 'overlay child key'); + + late final OverlayEntry overlayEntry1; + addTearDown(() => overlayEntry1..remove()..dispose()); + late final OverlayEntry overlayEntry2; + addTearDown(() => overlayEntry2..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: oldOverlayKey, initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry1 = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( key: key, @@ -1593,7 +1763,7 @@ void main() { child: Overlay( key: newOverlayKey, initialEntries: <OverlayEntry>[ - OverlayEntry( + overlayEntry2 = OverlayEntry( builder: (BuildContext context) { return OverlayPortal( key: key, @@ -1616,18 +1786,21 @@ void main() { }); group('Paint order', () { - testWidgets('show bringsToTop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('show bringsToTop', (WidgetTester tester) async { controller1.hide(); const _PaintOrder child1 = _PaintOrder(); const _PaintOrder child2 = _PaintOrder(); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + overlayEntry = OverlayEntry(builder: (BuildContext context) { return Column( children: <Widget>[ OverlayPortal(controller: controller1, overlayChildBuilder: (BuildContext context) => child1), @@ -1680,7 +1853,7 @@ void main() { ); }); - testWidgets('Paint order does not change after global key reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paint order does not change after global key reparenting', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); late StateSetter setState; @@ -1705,12 +1878,15 @@ void main() { child: const SizedBox(), ); + late final OverlayEntry overlayEntry; + addTearDown(() => overlayEntry..remove()..dispose()); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ - OverlayEntry(builder: (BuildContext context) { + overlayEntry = OverlayEntry(builder: (BuildContext context) { return Column( children: <Widget>[ StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) { diff --git a/packages/flutter/test/widgets/overlay_test.dart b/packages/flutter/test/widgets/overlay_test.dart index abb1d07f31e81..66663e061adb6 100644 --- a/packages/flutter/test/widgets/overlay_test.dart +++ b/packages/flutter/test/widgets/overlay_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; void main() { @@ -1140,6 +1139,72 @@ void main() { ); }); + testWidgets('OverlayEntry throws if inserted to an invalid Overlay', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Overlay(), + ), + ); + final OverlayState overlay = tester.state(find.byType(Overlay)); + final OverlayEntry entry = OverlayEntry(builder: (BuildContext context) => const SizedBox()); + expect( + () => overlay.insert(entry), + returnsNormally, + ); + + // Throws when inserted to the same Overlay. + expect( + () => overlay.insert(entry), + throwsA(isA<FlutterError>().having( + (FlutterError error) => error.toString(), + 'toString()', + allOf( + contains('The specified entry is already present in the target Overlay.'), + contains('The OverlayEntry was'), + contains('The Overlay the OverlayEntry was trying to insert to was'), + ), + )), + ); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: SizedBox(child: Overlay()), + ), + ); + + // Throws if inserted to an already disposed Overlay. + expect( + () => overlay.insert(entry), + throwsA(isA<FlutterError>().having( + (FlutterError error) => error.toString(), + 'toString()', + allOf( + contains('Attempted to insert an OverlayEntry to an already disposed Overlay.'), + contains('The OverlayEntry was'), + contains('The Overlay the OverlayEntry was trying to insert to was'), + ), + )), + ); + + final OverlayState newOverlay = tester.state(find.byType(Overlay)); + // Throws when inserted to a different Overlay without calling remove. + expect( + () => newOverlay.insert(entry), + throwsA(isA<FlutterError>().having( + (FlutterError error) => error.toString(), + 'toString()', + allOf( + contains('The specified entry is already present in a different Overlay.'), + contains('The OverlayEntry was'), + contains('The Overlay the OverlayEntry was trying to insert to was'), + contains("The OverlayEntry's current Overlay was"), + ), + )), + ); + }); + group('OverlayEntry listenable', () { final GlobalKey overlayKey = GlobalKey(); final Widget emptyOverlay = Directionality( diff --git a/packages/flutter/test/widgets/overscroll_indicator_test.dart b/packages/flutter/test/widgets/overscroll_indicator_test.dart index 3f4116d1070ad..4caaa3a9714cc 100644 --- a/packages/flutter/test/widgets/overscroll_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_indicator_test.dart @@ -6,8 +6,7 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final Matcher doesNotOverscroll = isNot(paints..circle()); @@ -21,7 +20,7 @@ Future<void> slowDrag(WidgetTester tester, Offset start, Offset offset) async { } void main() { - testWidgets('Overscroll indicator color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll indicator color', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -57,7 +56,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('Nested scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested scrollable', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -88,7 +87,7 @@ void main() { expect(innerPainter, paints..circle()); }); - testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -130,7 +129,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -168,7 +167,7 @@ void main() { }); group("Flipping direction of scrollable doesn't change overscroll behavior", () { - testWidgets('down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('down', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -188,7 +187,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('up', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -210,7 +209,7 @@ void main() { }); }); - testWidgets('Overscroll in both directions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll in both directions', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -233,7 +232,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('Overscroll ignored from alternate axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll ignored from alternate axis', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -262,7 +261,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('Overscroll horizontally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overscroll horizontally', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -293,7 +292,7 @@ void main() { expect(painter, doesNotOverscroll); }); - testWidgets('Nested overscrolls do not throw exceptions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested overscrolls do not throw exceptions', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( @@ -315,7 +314,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Changing settings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing settings', (WidgetTester tester) async { RenderObject painter; await tester.pumpWidget( @@ -361,7 +360,7 @@ void main() { expect(painter, isNot(paints..circle()..circle())); }); - testWidgets('CustomScrollView overscroll indicator works if there is sliver before center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView overscroll indicator works if there is sliver before center', (WidgetTester tester) async { final Key centerKey = UniqueKey(); await tester.pumpWidget( Directionality( @@ -400,7 +399,7 @@ void main() { expect(painter, paints..save()..translate(y: 0.0)..scale()..circle()); }); - testWidgets('CustomScrollView overscroll indicator works well with [CustomScrollView.center] and [OverscrollIndicatorNotification.paintOffset]', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView overscroll indicator works well with [CustomScrollView.center] and [OverscrollIndicatorNotification.paintOffset]', (WidgetTester tester) async { final Key centerKey = UniqueKey(); await tester.pumpWidget( Directionality( @@ -447,7 +446,7 @@ void main() { expect(painter, paints..save()..translate(y: 50.0)..scale()..circle()); }); - testWidgets('The OverscrollIndicator should not overflow the scrollable view edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The OverscrollIndicator should not overflow the scrollable view edge', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/64149 await tester.pumpWidget( Directionality( @@ -510,7 +509,7 @@ void main() { }); group('[OverscrollIndicatorNotification.paintOffset] test', () { - testWidgets('Leading', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Leading', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -540,7 +539,7 @@ void main() { expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle()); }); - testWidgets('Trailing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Trailing', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart index 047b5ce1b2bdd..3f48544f7e40a 100644 --- a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart @@ -10,6 +10,7 @@ library; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { Widget buildTest( @@ -76,10 +77,12 @@ void main() { ); } - testWidgets('Stretch overscroll will do nothing when axes do not match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll will do nothing when axes do not match', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -135,11 +138,13 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); }); - testWidgets('Stretch overscroll vertically', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll vertically', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); @@ -212,11 +217,13 @@ void main() { expect(box3.localToGlobal(Offset.zero).dy, 350.0); }); - testWidgets('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, reverse: true), ); @@ -245,11 +252,13 @@ void main() { ); }); - testWidgets('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest( box1Key, @@ -285,11 +294,13 @@ void main() { ); }); - testWidgets('Stretch overscroll works in reverse - horizontal - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll works in reverse - horizontal - RTL', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest( box1Key, @@ -371,11 +382,13 @@ void main() { expect(box3.localToGlobal(Offset.zero).dx, 500.0); }); - testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll horizontally', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal) ); @@ -448,11 +461,13 @@ void main() { expect(box3.localToGlobal(Offset.zero).dx, 500.0); }); - testWidgets('Stretch overscroll horizontally RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll horizontally RTL', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest( box1Key, @@ -488,12 +503,14 @@ void main() { ); }); - testWidgets('Disallow stretching overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disallow stretching overscroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); - double indicatorNotification =0; + addTearDown(controller.dispose); + + double indicatorNotification = 0; await tester.pumpWidget( NotificationListener<OverscrollIndicatorNotification>( onNotification: (OverscrollIndicatorNotification notification) { @@ -530,7 +547,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Stretch does not overflow bounds of container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch does not overflow bounds of container', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/90197 await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -587,7 +604,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Clip behavior is updated as needed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Clip behavior is updated as needed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/97867 await tester.pumpWidget( Directionality( @@ -647,7 +664,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('clipBehavior parameter updates overscroll clipping behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clipBehavior parameter updates overscroll clipping behavior', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103491 Widget buildFrame(Clip clipBehavior) { @@ -711,7 +728,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Stretch limit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch limit', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99264 await tester.pumpWidget( Directionality( @@ -762,7 +779,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99264 await tester.pumpWidget( Directionality( @@ -832,11 +849,13 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Stretch overscroll vertically, change direction mid scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll vertically, change direction mid scroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest( box1Key, @@ -901,11 +920,13 @@ void main() { expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); }); - testWidgets('Stretch overscroll horizontally, change direction mid scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stretch overscroll horizontally, change direction mid scroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest( box1Key, @@ -971,11 +992,13 @@ void main() { expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); }); - testWidgets('Fling toward the trailing edge causes stretch toward the leading edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fling toward the trailing edge causes stretch toward the leading edge', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); @@ -1011,11 +1034,13 @@ void main() { expect(box3.localToGlobal(Offset.zero).dy, 350.0); }); - testWidgets('Fling toward the leading edge causes stretch toward the trailing edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fling toward the leading edge causes stretch toward the trailing edge', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller), ); @@ -1063,11 +1088,13 @@ void main() { expect(box3.localToGlobal(Offset.zero).dy, 500.0); }); - testWidgets('changing scroll direction during recede animation will not change the stretch direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('changing scroll direction during recede animation will not change the stretch direction', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); final GlobalKey box2Key = GlobalKey(); final GlobalKey box3Key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( buildTest(box1Key, box2Key, box3Key, controller, boxHeight: 205.0), ); @@ -1124,4 +1151,31 @@ void main() { await gesture.up(); }); + + testWidgetsWithLeakTracking('Stretch overscroll only uses image filter during stretch effect', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + ) + ); + + expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); + + expect(tester.layers, contains(isA<ImageFilterLayer>())); + }); } diff --git a/packages/flutter/test/widgets/page_forward_transitions_test.dart b/packages/flutter/test/widgets/page_forward_transitions_test.dart index 9ffc019cc670b..da523ac96f40a 100644 --- a/packages/flutter/test/widgets/page_forward_transitions_test.dart +++ b/packages/flutter/test/widgets/page_forward_transitions_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestTransition extends AnimatedWidget { const TestTransition({ @@ -57,7 +58,7 @@ void main() { const Duration kTwoTenthsOfTheTransitionDuration = Duration(milliseconds: 30); const Duration kFourTenthsOfTheTransitionDuration = Duration(milliseconds: 60); - testWidgets('Check onstage/offstage handling around transitions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check onstage/offstage handling around transitions', (WidgetTester tester) async { final GlobalKey insideKey = GlobalKey(); @@ -196,7 +197,7 @@ void main() { }); - testWidgets('Check onstage/offstage handling of barriers around transitions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check onstage/offstage handling of barriers around transitions', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( onGenerateRoute: (RouteSettings settings) { diff --git a/packages/flutter/test/widgets/page_route_builder_test.dart b/packages/flutter/test/widgets/page_route_builder_test.dart index 3e6090fa80195..7e90c9a4e273e 100644 --- a/packages/flutter/test/widgets/page_route_builder_test.dart +++ b/packages/flutter/test/widgets/page_route_builder_test.dart @@ -9,6 +9,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestPage extends StatelessWidget { const TestPage({ super.key, this.useMaterial3 }); @@ -93,7 +94,7 @@ class ModalPage extends StatelessWidget { } void main() { - testWidgets('Material2 - Barriers show when using PageRouteBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Barriers show when using PageRouteBuilder', (WidgetTester tester) async { await tester.pumpWidget(const TestPage(useMaterial3: false)); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); @@ -103,7 +104,7 @@ void main() { ); }); - testWidgets('Material3 - Barriers show when using PageRouteBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - Barriers show when using PageRouteBuilder', (WidgetTester tester) async { await tester.pumpWidget(const TestPage(useMaterial3: true)); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); diff --git a/packages/flutter/test/widgets/page_storage_test.dart b/packages/flutter/test/widgets/page_storage_test.dart index db74aa58b32d2..ec8d39002de18 100644 --- a/packages/flutter/test/widgets/page_storage_test.dart +++ b/packages/flutter/test/widgets/page_storage_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('PageStorage read and write', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageStorage read and write', (WidgetTester tester) async { const Key builderKey = PageStorageKey<String>('builderKey'); late StateSetter setState; int storedValue = 0; @@ -37,7 +38,7 @@ void main() { expect(PageStorage.of(builderElement).readState(builderElement), equals(storedValue)); }); - testWidgets('PageStorage read and write by identifier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageStorage read and write by identifier', (WidgetTester tester) async { late StateSetter setState; int storedValue = 0; diff --git a/packages/flutter/test/widgets/page_transitions_test.dart b/packages/flutter/test/widgets/page_transitions_test.dart index 55729ea2395a7..6b1a973ea408b 100644 --- a/packages/flutter/test/widgets/page_transitions_test.dart +++ b/packages/flutter/test/widgets/page_transitions_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestOverlayRoute extends OverlayRoute<void> { TestOverlayRoute({ super.settings }); @@ -47,7 +48,7 @@ class PersistentBottomSheetTestState extends State<PersistentBottomSheetTest> { } void main() { - testWidgets('Check onstage/offstage handling around transitions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check onstage/offstage handling around transitions', (WidgetTester tester) async { final GlobalKey containerKey1 = GlobalKey(); final GlobalKey containerKey2 = GlobalKey(); final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ @@ -129,7 +130,7 @@ void main() { expect(Navigator.canPop(containerKey1.currentContext!), isFalse); }); - testWidgets('Check back gesture disables Heroes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check back gesture disables Heroes', (WidgetTester tester) async { final GlobalKey containerKey1 = GlobalKey(); final GlobalKey containerKey2 = GlobalKey(); const String kHeroTag = 'hero'; @@ -198,7 +199,7 @@ void main() { expect(settingsOffset.dy, 100.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets("Check back gesture doesn't start during transitions", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Check back gesture doesn't start during transitions", (WidgetTester tester) async { final GlobalKey containerKey1 = GlobalKey(); final GlobalKey containerKey2 = GlobalKey(); final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ @@ -242,7 +243,7 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); // Tests bug https://github.com/flutter/flutter/issues/6451 - testWidgets('Check back gesture with a persistent bottom sheet showing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Check back gesture with a persistent bottom sheet showing', (WidgetTester tester) async { final GlobalKey containerKey1 = GlobalKey(); final GlobalKey containerKey2 = GlobalKey(); final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ @@ -295,7 +296,7 @@ void main() { expect(sheet.setStateCalled, isFalse); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Test completed future', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Test completed future', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (_) => const Center(child: Text('home')), '/next': (_) => const Center(child: Text('next')), diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 9b94146ee55d8..41405b00e229e 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; import 'semantics_tester.dart'; @@ -14,7 +15,7 @@ import 'states.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -49,11 +50,10 @@ void main() { expect(finderCalled, true); }); - testWidgets('PageView resize from zero-size viewport should not lose state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView resize from zero-size viewport should not lose state', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/88956 - final PageController controller = PageController( - initialPage: 1, - ); + final PageController controller = PageController(initialPage: 1); + addTearDown(controller.dispose); Widget build(Size size) { return Directionality( @@ -94,11 +94,10 @@ void main() { expect(find.text('Iowa'), findsOneWidget); }); - testWidgets('Change the page through the controller when zero-size viewport', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Change the page through the controller when zero-size viewport', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/88956 - final PageController controller = PageController( - initialPage: 1, - ); + final PageController controller = PageController(initialPage: 1); + addTearDown(controller.dispose); Widget build(Size size) { return Directionality( @@ -134,11 +133,10 @@ void main() { expect(find.text('Illinois'), findsOneWidget); }); - testWidgets('_PagePosition.applyViewportDimension should not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_PagePosition.applyViewportDimension should not throw', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/101007 - final PageController controller = PageController( - initialPage: 1, - ); + final PageController controller = PageController(initialPage: 1); + addTearDown(controller.dispose); // Set the starting viewportDimension to 0.0 await tester.binding.setSurfaceSize(Size.zero); @@ -173,13 +171,14 @@ void main() { await tester.binding.setSurfaceSize(null); }); - testWidgets('PageController cannot return page while unattached', + testWidgetsWithLeakTracking('PageController cannot return page while unattached', (WidgetTester tester) async { final PageController controller = PageController(); + addTearDown(controller.dispose); expect(() => controller.page, throwsAssertionError); }); - testWidgets('PageView control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( @@ -246,7 +245,7 @@ void main() { expect(find.text('Arizona'), findsNothing); }); - testWidgets('PageView does not squish when overscrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView does not squish when overscrolled', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: PageView( children: List<Widget>.generate(10, (int i) { @@ -284,8 +283,9 @@ void main() { expect(sizeOf(0), equals(const Size(800.0, 600.0))); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('PageController control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageController control test', (WidgetTester tester) async { final PageController controller = PageController(initialPage: 4); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -330,7 +330,7 @@ void main() { expect(find.text('California'), findsOneWidget); }); - testWidgets('PageController page stability', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageController page stability', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Center( @@ -382,8 +382,10 @@ void main() { expect(find.text('Arizona'), findsOneWidget); }); - testWidgets('PageController nextPage and previousPage return Futures that resolve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageController nextPage and previousPage return Futures that resolve', (WidgetTester tester) async { final PageController controller = PageController(); + addTearDown(controller.dispose); + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( @@ -414,7 +416,7 @@ void main() { expect(previousPageCompleted, true); }); - testWidgets('PageView in zero-size container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView in zero-size container', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Center( @@ -444,7 +446,7 @@ void main() { expect(find.text('Alabama'), findsOneWidget); }); - testWidgets('Page changes at halfway point', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Page changes at halfway point', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -494,9 +496,10 @@ void main() { expect(find.text('Alaska'), findsOneWidget); }); - testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async { final List<int> log = <int>[]; final PageController controller = PageController(viewportFraction: 0.9); + addTearDown(controller.dispose); Widget build(PageController controller, { Size? size }) { final Widget pageView = Directionality( @@ -548,8 +551,9 @@ void main() { expect(find.text('Arizona'), findsNothing); }); - testWidgets('PageView viewportFraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView viewportFraction', (WidgetTester tester) async { PageController controller = PageController(viewportFraction: 7/8); + addTearDown(controller.dispose); Widget build(PageController controller) { return Directionality( @@ -583,6 +587,7 @@ void main() { expect(tester.getTopLeft(find.text('Idaho')), const Offset(750.0, 0.0)); controller = PageController(viewportFraction: 39/40); + addTearDown(controller.dispose); await tester.pumpWidget(build(controller)); @@ -591,7 +596,7 @@ void main() { expect(tester.getTopLeft(find.text('Idaho')), const Offset(790.0, 0.0)); }); - testWidgets('Page snapping disable and reenable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Page snapping disable and reenable', (WidgetTester tester) async { final List<int> log = <int>[]; Widget build({ required bool pageSnapping }) { @@ -654,8 +659,9 @@ void main() { expect(find.text('Arkansas'), findsNothing); }); - testWidgets('PageView small viewportFraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView small viewportFraction', (WidgetTester tester) async { final PageController controller = PageController(viewportFraction: 1/8); + addTearDown(controller.dispose); Widget build(PageController controller) { return Directionality( @@ -698,8 +704,9 @@ void main() { expect(tester.getTopLeft(find.text('Iowa')), const Offset(750.0, 0.0)); }); - testWidgets('PageView large viewportFraction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView large viewportFraction', (WidgetTester tester) async { final PageController controller = PageController(viewportFraction: 5/4); + addTearDown(controller.dispose); Widget build(PageController controller) { return Directionality( @@ -731,7 +738,7 @@ void main() { expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0)); }); - testWidgets( + testWidgetsWithLeakTracking( 'Updating PageView large viewportFraction', (WidgetTester tester) async { Widget build(PageController controller) { @@ -754,12 +761,14 @@ void main() { } final PageController oldController = PageController(viewportFraction: 5/4); + addTearDown(oldController.dispose); await tester.pumpWidget(build(oldController)); expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0)); expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0)); final PageController newController = PageController(viewportFraction: 4); + addTearDown(newController.dispose); await tester.pumpWidget(build(newController)); newController.jumpToPage(10); await tester.pump(); @@ -768,11 +777,12 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'PageView large viewportFraction can scroll to the last page and snap', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/45096. final PageController controller = PageController(viewportFraction: 5/4); + addTearDown(controller.dispose); Widget build(PageController controller) { return Directionality( @@ -805,11 +815,12 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'All visible pages are able to receive touch events', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/23873. final PageController controller = PageController(viewportFraction: 1/4); + addTearDown(controller.dispose); late int tappedIndex; Widget build() { @@ -853,12 +864,13 @@ void main() { }, ); - testWidgets('the current item remains centered on constraint change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the current item remains centered on constraint change', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/50505. final PageController controller = PageController( initialPage: kStates.length - 1, viewportFraction: 0.5, ); + addTearDown(controller.dispose); Widget build(Size size) { return Directionality( @@ -895,10 +907,11 @@ void main() { verifyCentered(); }); - testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView does not report page changed on overscroll', (WidgetTester tester) async { final PageController controller = PageController( initialPage: kStates.length - 1, ); + addTearDown(controller.dispose); int changeIndex = 0; Widget build() { return Directionality( @@ -921,8 +934,9 @@ void main() { expect(changeIndex, 0); }); - testWidgets('PageView can restore page', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView can restore page', (WidgetTester tester) async { final PageController controller = PageController(); + addTearDown(controller.dispose); expect( () => controller.page, throwsA(isAssertionError.having( @@ -983,6 +997,7 @@ void main() { expect(controller.page, 2); final PageController controller2 = PageController(keepPage: false); + addTearDown(controller2.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageStorage( @@ -1001,10 +1016,11 @@ void main() { expect(controller2.page, 0); }); - testWidgets('PageView exposes semantics of children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView exposes semantics of children', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final PageController controller = PageController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( @@ -1040,7 +1056,7 @@ void main() { semantics.dispose(); }); - testWidgets('PageMetrics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageMetrics', (WidgetTester tester) async { final PageMetrics page = PageMetrics( minScrollExtent: 100.0, maxScrollExtent: 200.0, @@ -1057,8 +1073,9 @@ void main() { expect(page2.page, 4.0); }); - testWidgets('Page controller can handle rounding issue', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Page controller can handle rounding issue', (WidgetTester tester) async { final PageController pageController = PageController(); + addTearDown(pageController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -1077,10 +1094,11 @@ void main() { expect(pageController.page, 1); }); - testWidgets('PageView can participate in a11y scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView can participate in a11y scrolling', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final PageController controller = PageController(); + addTearDown(controller.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( @@ -1155,7 +1173,7 @@ void main() { expect(context.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('PageView.padEnds tests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView.padEnds tests', (WidgetTester tester) async { Finder viewportFinder() => find.byType(SliverFillViewport, skipOffstage: false); // PageView() defaults to true. @@ -1177,10 +1195,11 @@ void main() { expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false); }); - testWidgets('PageView - precision error inside RenderSliverFixedExtentBoxAdaptor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView - precision error inside RenderSliverFixedExtentBoxAdaptor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/95101 - final PageController controller = PageController(initialPage: 152); + addTearDown(controller.dispose); + await tester.pumpWidget( Center( child: SizedBox( @@ -1204,9 +1223,10 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('PageView content should not be stretched on precision error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView content should not be stretched on precision error', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/126561. final PageController controller = PageController(); + addTearDown(controller.dispose); const double pixel6EmulatorWidth = 411.42857142857144; diff --git a/packages/flutter/test/widgets/pageable_list_test.dart b/packages/flutter/test/widgets/pageable_list_test.dart index 3cab09a14c5ff..0dd30a18da5eb 100644 --- a/packages/flutter/test/widgets/pageable_list_test.dart +++ b/packages/flutter/test/widgets/pageable_list_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Size pageSize = const Size(600.0, 300.0); const List<int> defaultPages = <int>[0, 1, 2, 3, 4, 5]; @@ -59,7 +60,7 @@ Future<void> pageRight(WidgetTester tester) { } void main() { - testWidgets('PageView default control', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView default control', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -70,7 +71,7 @@ void main() { ); }); - testWidgets('PageView control test (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView control test (LTR)', (WidgetTester tester) async { currentPage = null; await tester.pumpWidget(buildFrame(textDirection: TextDirection.ltr)); expect(currentPage, isNull); @@ -98,7 +99,7 @@ void main() { expect(currentPage, equals(0)); }); - testWidgets('PageView with reverse (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView with reverse (LTR)', (WidgetTester tester) async { currentPage = null; await tester.pumpWidget(buildFrame(reverse: true, textDirection: TextDirection.ltr)); await pageRight(tester); @@ -132,7 +133,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('PageView control test (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView control test (RTL)', (WidgetTester tester) async { currentPage = null; await tester.pumpWidget(buildFrame(textDirection: TextDirection.rtl)); await pageRight(tester); @@ -166,7 +167,7 @@ void main() { expect(find.text('5'), findsNothing); }); - testWidgets('PageView with reverse (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView with reverse (RTL)', (WidgetTester tester) async { currentPage = null; await tester.pumpWidget(buildFrame(reverse: true, textDirection: TextDirection.rtl)); expect(currentPage, isNull); diff --git a/packages/flutter/test/widgets/parent_data_test.dart b/packages/flutter/test/widgets/parent_data_test.dart index d546947c81f9c..d8934ba572a96 100644 --- a/packages/flutter/test/widgets/parent_data_test.dart +++ b/packages/flutter/test/widgets/parent_data_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; @@ -49,7 +50,7 @@ void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) { final TestParentData kNonPositioned = TestParentData(); void main() { - testWidgets('ParentDataWidget control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ParentDataWidget control test', (WidgetTester tester) async { await tester.pumpWidget( const Stack( textDirection: TextDirection.ltr, @@ -249,12 +250,99 @@ void main() { checkTree(tester, <TestParentData>[]); }); - testWidgets('ParentDataWidget conflicting data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ParentData overwrite with custom ParentDataWidget subclasses', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + CustomPositionedWidget( + bottom: 8.0, + child: Positioned( + top: 6.0, + left: 7.0, + child: DecoratedBox(decoration: kBoxDecorationB), + ), + ), + ], + ), + ), + ); + + dynamic exception = tester.takeException(); + expect(exception, isFlutterError); + expect( + exception.toString(), + startsWith( + 'Incorrect use of ParentDataWidget.\n' + 'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n' + '- Positioned(left: 7.0, top: 6.0), which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + '- CustomPositionedWidget, which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + 'A RenderObject can receive parent data from multiple ' + 'ParentDataWidgets, but the Type of ParentData must be unique to ' + 'prevent one overwriting another.\n' + 'Usually, this indicates that one or more of the offending ParentDataWidgets listed ' + "above isn't placed inside a dedicated compatible ancestor widget that it isn't " + 'sharing with another ParentDataWidget of the same type.\n' + 'Otherwise, separating aspects of ParentData to prevent conflicts can ' + 'be done using mixins, mixing them all in on the full ParentData ' + 'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n' + 'The ownership chain for the RenderObject that received the parent data was:\n' + ' DecoratedBox ← Positioned ← CustomPositionedWidget ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test. + ), + ); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + SubclassPositioned( + bottom: 8.0, + child: Positioned( + top: 6.0, + left: 7.0, + child: DecoratedBox(decoration: kBoxDecorationB), + ), + ), + ], + ), + ), + ); + + exception = tester.takeException(); + expect(exception, isFlutterError); + expect( + exception.toString(), + startsWith( + 'Incorrect use of ParentDataWidget.\n' + 'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n' + '- Positioned(left: 7.0, top: 6.0), which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + '- SubclassPositioned(bottom: 8.0), which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + 'A RenderObject can receive parent data from multiple ' + 'ParentDataWidgets, but the Type of ParentData must be unique to ' + 'prevent one overwriting another.\n' + 'Usually, this indicates that one or more of the offending ParentDataWidgets listed ' + "above isn't placed inside a dedicated compatible ancestor widget that it isn't " + 'sharing with another ParentDataWidget of the same type.\n' + 'Otherwise, separating aspects of ParentData to prevent conflicts can ' + 'be done using mixins, mixing them all in on the full ParentData ' + 'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n' + 'The ownership chain for the RenderObject that received the parent data was:\n' + ' DecoratedBox ← Positioned ← SubclassPositioned ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test. + ), + ); + }); + + testWidgetsWithLeakTracking('ParentDataWidget conflicting data', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Stack( - textDirection: TextDirection.ltr, children: <Widget>[ Positioned( top: 5.0, @@ -276,19 +364,26 @@ void main() { exception.toString(), startsWith( 'Incorrect use of ParentDataWidget.\n' - 'The following ParentDataWidgets are providing parent data to the same RenderObject:\n' - '- Positioned(left: 7.0, top: 6.0) (typically placed directly inside a Stack widget)\n' - '- Positioned(top: 5.0, bottom: 8.0) (typically placed directly inside a Stack widget)\n' - 'However, a RenderObject can only receive parent data from at most one ParentDataWidget.\n' - 'Usually, this indicates that at least one of the offending ParentDataWidgets listed ' - 'above is not placed directly inside a compatible ancestor widget.\n' + 'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n' + '- Positioned(left: 7.0, top: 6.0), which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + '- Positioned(top: 5.0, bottom: 8.0), which writes ParentData of type ' + 'StackParentData, (typically placed directly inside a Stack widget)\n' + 'A RenderObject can receive parent data from multiple ' + 'ParentDataWidgets, but the Type of ParentData must be unique to ' + 'prevent one overwriting another.\n' + 'Usually, this indicates that one or more of the offending ParentDataWidgets listed ' + "above isn't placed inside a dedicated compatible ancestor widget that it isn't " + 'sharing with another ParentDataWidget of the same type.\n' + 'Otherwise, separating aspects of ParentData to prevent conflicts can ' + 'be done using mixins, mixing them all in on the full ParentData ' + 'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n' 'The ownership chain for the RenderObject that received the parent data was:\n' ' DecoratedBox ← Positioned ← Positioned ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test. ), ); await tester.pumpWidget(const Stack(textDirection: TextDirection.ltr)); - checkTree(tester, <TestParentData>[]); await tester.pumpWidget( @@ -307,6 +402,7 @@ void main() { ), ), ); + exception = tester.takeException(); expect(exception, isFlutterError); expect( @@ -327,11 +423,10 @@ void main() { await tester.pumpWidget( const Stack(textDirection: TextDirection.ltr), ); - checkTree(tester, <TestParentData>[]); }); - testWidgets('ParentDataWidget interacts with global keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ParentDataWidget interacts with global keys', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -389,7 +484,7 @@ void main() { ]); }); - testWidgets('Parent data invalid ancestor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Parent data invalid ancestor', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Row( @@ -424,7 +519,7 @@ void main() { ); }); - testWidgets('ParentDataWidget can be used with different ancestor RenderObjectWidgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ParentDataWidget can be used with different ancestor RenderObjectWidgets', (WidgetTester tester) async { await tester.pumpWidget( OneAncestorWidget( child: Container(), @@ -457,6 +552,46 @@ void main() { }); } +class SubclassPositioned extends Positioned { + const SubclassPositioned({ + super.key, + super.left, + super.top, + super.right, + super.bottom, + super.width, + super.height, + required super.child, + }); + + @override + void applyParentData(RenderObject renderObject) { + assert(renderObject.parentData is StackParentData); + final StackParentData parentData = renderObject.parentData! as StackParentData; + parentData.bottom = bottom; + } +} + +class CustomPositionedWidget extends ParentDataWidget<StackParentData> { + const CustomPositionedWidget({ + super.key, + required this.bottom, + required super.child, + }); + + final double bottom; + + @override + void applyParentData(RenderObject renderObject) { + assert(renderObject.parentData is StackParentData); + final StackParentData parentData = renderObject.parentData! as StackParentData; + parentData.bottom = bottom; + } + + @override + Type get debugTypicalAncestorWidgetClass => Stack; +} + class TestParentDataWidget extends ParentDataWidget<DummyParentData> { const TestParentDataWidget({ super.key, diff --git a/packages/flutter/test/widgets/performance_overlay_test.dart b/packages/flutter/test/widgets/performance_overlay_test.dart index 8c337f1182a8a..9af1f11012fc5 100644 --- a/packages/flutter/test/widgets/performance_overlay_test.dart +++ b/packages/flutter/test/widgets/performance_overlay_test.dart @@ -5,14 +5,15 @@ import 'package:flutter/src/rendering/performance_overlay.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Performance overlay smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Performance overlay smoke test', (WidgetTester tester) async { await tester.pumpWidget(const PerformanceOverlay()); await tester.pumpWidget(PerformanceOverlay.allEnabled()); }); - testWidgets('update widget field checkerboardRasterCacheImages', + testWidgetsWithLeakTracking('update widget field checkerboardRasterCacheImages', (WidgetTester tester) async { await tester.pumpWidget(const PerformanceOverlay()); await tester.pumpWidget( @@ -25,7 +26,7 @@ void main() { true); }); - testWidgets('update widget field checkerboardOffscreenLayers', + testWidgetsWithLeakTracking('update widget field checkerboardOffscreenLayers', (WidgetTester tester) async { await tester.pumpWidget(const PerformanceOverlay()); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/physical_model_test.dart b/packages/flutter/test/widgets/physical_model_test.dart index 7d3146b9c1b76..066d1c7573259 100644 --- a/packages/flutter/test/widgets/physical_model_test.dart +++ b/packages/flutter/test/widgets/physical_model_test.dart @@ -10,9 +10,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('PhysicalModel updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalModel updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: PhysicalModel(color: Colors.red)), ); @@ -28,7 +29,7 @@ void main() { expect(renderPhysicalModel.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('PhysicalShape updates clipBehavior in updateRenderObject', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalShape updates clipBehavior in updateRenderObject', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: PhysicalShape(color: Colors.red, clipper: ShapeBorderClipper(shape: CircleBorder()))), ); @@ -44,7 +45,7 @@ void main() { expect(renderPhysicalShape.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('PhysicalModel - clips when overflows and elevation is 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PhysicalModel - clips when overflows and elevation is 0', (WidgetTester tester) async { const Key key = Key('test'); await tester.pumpWidget( Theme( diff --git a/packages/flutter/test/widgets/placeholder_test.dart b/packages/flutter/test/widgets/placeholder_test.dart index dcbdfdf8223b9..28aadb0dbf213 100644 --- a/packages/flutter/test/widgets/placeholder_test.dart +++ b/packages/flutter/test/widgets/placeholder_test.dart @@ -4,11 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Placeholder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Placeholder', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); expect(tester.renderObject<RenderBox>(find.byType(Placeholder)).size, const Size(800.0, 600.0)); await tester.pumpWidget(const Center(child: Placeholder())); @@ -21,21 +20,21 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(Placeholder)).size, const Size(200.0, 300.0)); }); - testWidgets('Placeholder color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Placeholder color', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); expect(tester.renderObject(find.byType(Placeholder)), paints..path(color: const Color(0xFF455A64))); await tester.pumpWidget(const Placeholder(color: Color(0xFF00FF00))); expect(tester.renderObject(find.byType(Placeholder)), paints..path(color: const Color(0xFF00FF00))); }); - testWidgets('Placeholder stroke width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Placeholder stroke width', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); expect(tester.renderObject(find.byType(Placeholder)), paints..path(strokeWidth: 2.0)); await tester.pumpWidget(const Placeholder(strokeWidth: 10.0)); expect(tester.renderObject(find.byType(Placeholder)), paints..path(strokeWidth: 10.0)); }); - testWidgets('Placeholder child widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Placeholder child widget', (WidgetTester tester) async { await tester.pumpWidget(const Placeholder()); expect(find.text('Label'), findsNothing); await tester.pumpWidget(const MaterialApp(home: Placeholder(child: Text('Label')))); diff --git a/packages/flutter/test/widgets/platform_menu_bar_test.dart b/packages/flutter/test/widgets/platform_menu_bar_test.dart index 8828115a91c39..c72e85130e4a1 100644 --- a/packages/flutter/test/widgets/platform_menu_bar_test.dart +++ b/packages/flutter/test/widgets/platform_menu_bar_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -47,7 +48,7 @@ void main() { group('PlatformMenuBar', () { group('basic menu structure is transmitted to platform', () { - testWidgets('using onSelected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using onSelected', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -78,7 +79,7 @@ void main() { equals(expectedStructure), ); }); - testWidgets('using onSelectedIntent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using onSelectedIntent', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -126,7 +127,7 @@ void main() { ); expect(tester.takeException(), isA<AssertionError>()); }); - testWidgets('diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('diagnostics', (WidgetTester tester) async { const PlatformMenuItem item = PlatformMenuItem( label: 'label2', shortcut: SingleActivator(LogicalKeyboardKey.keyA), @@ -158,7 +159,7 @@ void main() { }); }); group('MenuBarItem', () { - testWidgets('diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('diagnostics', (WidgetTester tester) async { const PlatformMenuItem childItem = PlatformMenuItem( label: 'label', ); @@ -182,7 +183,7 @@ void main() { }); group('ShortcutSerialization', () { - testWidgets('character constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('character constructor', (WidgetTester tester) async { final ShortcutSerialization serialization = ShortcutSerialization.character('?'); expect(serialization.toChannelRepresentation(), equals(<String, Object?>{ 'shortcutCharacter': '?', @@ -195,7 +196,7 @@ void main() { })); }); - testWidgets('modifier constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modifier constructor', (WidgetTester tester) async { final ShortcutSerialization serialization = ShortcutSerialization.modifier(LogicalKeyboardKey.home); expect(serialization.toChannelRepresentation(), equals(<String, Object?>{ 'shortcutTrigger': LogicalKeyboardKey.home.keyId, diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index 7b4bc720370ec..548e9c76afeed 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -14,12 +14,13 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../services/fake_platform_views.dart'; void main() { group('AndroidView', () { - testWidgets('Create Android view', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Create Android view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -47,7 +48,7 @@ void main() { ); }); - testWidgets('Create Android view with params', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Create Android view with params', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -91,7 +92,7 @@ void main() { ); }); - testWidgets('Zero sized Android view is not created', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Zero sized Android view is not created', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -109,7 +110,7 @@ void main() { ); }); - testWidgets('Resize Android view', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Resize Android view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -167,7 +168,7 @@ void main() { ); }); - testWidgets('Change Android view type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Change Android view type', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -205,7 +206,7 @@ void main() { ); }); - testWidgets('Dispose Android view', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dispose Android view', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); await tester.pumpWidget( @@ -233,7 +234,7 @@ void main() { ); }); - testWidgets('Android view survives widget tree change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view survives widget tree change', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -271,7 +272,7 @@ void main() { ); }); - testWidgets('Android view gets touch events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view gets touch events', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -298,7 +299,7 @@ void main() { ); }); - testWidgets('Android view transparent hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view transparent hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -343,7 +344,7 @@ void main() { ); }); - testWidgets('Android view translucent hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view translucent hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -390,7 +391,7 @@ void main() { ); }); - testWidgets('Android view opaque hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view opaque hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -436,7 +437,7 @@ void main() { ); }); - testWidgets("Android view touch events are in virtual display's coordinate system", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Android view touch events are in virtual display's coordinate system", (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -466,7 +467,7 @@ void main() { ); }); - testWidgets('Android view directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view directionality', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('maps'); @@ -515,7 +516,7 @@ void main() { ); }); - testWidgets('Android view ambient directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view ambient directionality', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('maps'); @@ -570,7 +571,7 @@ void main() { ); }); - testWidgets('Android view can lose gesture arenas', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view can lose gesture arenas', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -605,7 +606,7 @@ void main() { ); }); - testWidgets('Android view drag gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view drag gesture recognizer', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -651,7 +652,7 @@ void main() { ); }); - testWidgets('Android view long press gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view long press gesture recognizer', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -694,7 +695,7 @@ void main() { ); }); - testWidgets('Android view tap gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view tap gesture recognizer', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -737,7 +738,7 @@ void main() { ); }); - testWidgets('Android view can claim gesture after all pointers are up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view can claim gesture after all pointers are up', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -777,7 +778,7 @@ void main() { ); }); - testWidgets('Android view rebuilt during gesture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view rebuilt during gesture', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -824,7 +825,7 @@ void main() { ); }); - testWidgets('Android view with eager gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Android view with eager gesture recognizer', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -866,7 +867,7 @@ void main() { // This test makes sure it doesn't crash. // https://github.com/flutter/flutter/issues/21514 - testWidgets( + testWidgetsWithLeakTracking( 'RenderAndroidView reconstructed with same gestureRecognizers does not crash', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); @@ -888,7 +889,7 @@ void main() { }, ); - testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -922,7 +923,7 @@ void main() { expect(factoryInvocationCount, 1); }); - testWidgets('AndroidView has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidView has correct semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); expect(currentViewId, greaterThanOrEqualTo(0)); @@ -977,7 +978,7 @@ void main() { handle.dispose(); }); - testWidgets('AndroidView can take input focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidView can take input focus', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1029,7 +1030,7 @@ void main() { expect(androidViewFocusNode.hasFocus, isTrue); }); - testWidgets('AndroidView sets a platform view text input client when focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidView sets a platform view text input client when focused', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1078,7 +1079,7 @@ void main() { expect(lastPlatformViewTextClient['platformViewId'], currentViewId + 1); }); - testWidgets('AndroidView clears platform focus when unfocused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidView clears platform focus when unfocused', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1123,7 +1124,7 @@ void main() { expect(viewsController.lastClearedFocusViewId, currentViewId + 1); }); - testWidgets('can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can set and update clipBehavior', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1169,7 +1170,7 @@ void main() { } }); - testWidgets('clip is handled correctly during resizing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('clip is handled correctly during resizing', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/67343 final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); @@ -1211,7 +1212,7 @@ void main() { expect(clipRectLayer.clipRect, const Rect.fromLTWH(0.0, 0.0, 50.0, 50.0)); }); - testWidgets('offset is sent to the platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('offset is sent to the platform', (WidgetTester tester) async { final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1237,7 +1238,7 @@ void main() { controller = FakeAndroidViewController(0); }); - testWidgets('AndroidViewSurface sets pointTransformer of view controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidViewSurface sets pointTransformer of view controller', (WidgetTester tester) async { final AndroidViewSurface surface = AndroidViewSurface( controller: controller, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -1247,7 +1248,7 @@ void main() { expect(controller.pointTransformer, isNotNull); }); - testWidgets('AndroidViewSurface defaults to texture-based rendering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidViewSurface defaults to texture-based rendering', (WidgetTester tester) async { final AndroidViewSurface surface = AndroidViewSurface( controller: controller, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -1260,7 +1261,7 @@ void main() { ), findsOneWidget); }); - testWidgets('AndroidViewSurface uses view-based rendering when initially required', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidViewSurface uses view-based rendering when initially required', (WidgetTester tester) async { controller.requiresViewComposition = true; final AndroidViewSurface surface = AndroidViewSurface( controller: controller, @@ -1274,7 +1275,7 @@ void main() { ), findsOneWidget); }); - testWidgets('AndroidViewSurface can switch to view-based rendering after creation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AndroidViewSurface can switch to view-based rendering after creation', (WidgetTester tester) async { final AndroidViewSurface surface = AndroidViewSurface( controller: controller, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -1306,7 +1307,7 @@ void main() { }); group('UiKitView', () { - testWidgets('Create UIView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Create UIView', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1329,7 +1330,7 @@ void main() { ); }); - testWidgets('Change UIView view type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Change UIView view type', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1362,7 +1363,7 @@ void main() { ); }); - testWidgets('Dispose UIView ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dispose UIView ', (WidgetTester tester) async { final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); await tester.pumpWidget( @@ -1390,7 +1391,7 @@ void main() { ); }); - testWidgets('Dispose UIView before creation completed ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dispose UIView before creation completed ', (WidgetTester tester) async { final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); viewsController.creationDelay = Completer<void>(); @@ -1421,7 +1422,7 @@ void main() { ); }); - testWidgets('UIView survives widget tree change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UIView survives widget tree change', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1454,7 +1455,7 @@ void main() { ); }); - testWidgets('Create UIView with params', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Create UIView with params', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1492,7 +1493,7 @@ void main() { ); }); - testWidgets('UiKitView accepts gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView accepts gestures', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1520,7 +1521,7 @@ void main() { expect(viewsController.gesturesAccepted[currentViewId + 1], 1); }); - testWidgets('UiKitView transparent hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView transparent hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1565,7 +1566,7 @@ void main() { expect(numPointerDownsOnParent, 1); }); - testWidgets('UiKitView translucent hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView translucent hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1610,7 +1611,7 @@ void main() { expect(numPointerDownsOnParent, 1); }); - testWidgets('UiKitView opaque hit test behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView opaque hit test behavior', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1653,7 +1654,7 @@ void main() { expect(numPointerDownsOnParent, 0); }); - testWidgets('UiKitView can lose gesture arenas', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView can lose gesture arenas', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1691,7 +1692,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 1); }); - testWidgets('UiKitView tap gesture recognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView tap gesture recognizers', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1735,7 +1736,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView long press gesture recognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView long press gesture recognizers', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1777,7 +1778,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView drag gesture recognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView drag gesture recognizers', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1819,7 +1820,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView can claim gesture after all pointers are up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView can claim gesture after all pointers are up', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1859,7 +1860,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView rebuilt during gesture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView rebuilt during gesture', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1904,7 +1905,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView with eager gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView with eager gesture recognizer', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1944,7 +1945,7 @@ void main() { expect(viewsController.gesturesRejected[currentViewId + 1], 0); }); - testWidgets('UiKitView rejects gestures absorbed by siblings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView rejects gestures absorbed by siblings', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -1974,7 +1975,7 @@ void main() { expect(viewsController.gesturesAccepted[currentViewId + 1], 0); }); - testWidgets( + testWidgetsWithLeakTracking( 'UiKitView rejects gestures absorbed by siblings if the touch is outside of the platform view bounds but inside platform view frame', (WidgetTester tester) async { // UiKitView is positioned at (left=0, top=100, right=300, bottom=600). @@ -2024,7 +2025,7 @@ void main() { }, ); - testWidgets('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async { final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -2058,7 +2059,7 @@ void main() { expect(factoryInvocationCount, 1); }); - testWidgets('UiKitView can take input focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView can take input focus', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -2111,7 +2112,7 @@ void main() { expect(uiKitViewFocusNode.hasFocus, isTrue); }); - testWidgets('UiKitView sends TextInput.setPlatformViewClient when focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView sends TextInput.setPlatformViewClient when focused', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); @@ -2150,7 +2151,7 @@ void main() { expect(channelArguments['platformViewId'], currentViewId + 1); }); - testWidgets('FocusNode is disposed on UIView dispose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('FocusNode is disposed on UIView dispose', (WidgetTester tester) async { final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); viewsController.registerViewType('webview'); @@ -2178,7 +2179,7 @@ void main() { expect(() => ChangeNotifier.debugAssertNotDisposed(node), throwsAssertionError); }); - testWidgets('UiKitView has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UiKitView has correct semantics', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); expect(currentViewId, greaterThanOrEqualTo(0)); @@ -2224,6 +2225,927 @@ void main() { }); }); + group('AppKitView', () { + testWidgetsWithLeakTracking('Create AppView', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals(<FakeAppKitView>[ + FakeAppKitView(currentViewId + 1, 'webview'), + ]), + ); + }); + + testWidgetsWithLeakTracking('Change AppKitView view type', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'maps', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals(<FakeAppKitView>[ + FakeAppKitView(currentViewId + 2, 'maps'), + ]), + ); + }); + + testWidgetsWithLeakTracking('Dispose AppKitView ', (WidgetTester tester) async { + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgetsWithLeakTracking('Dispose AppKitView before creation completed ', (WidgetTester tester) async { + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + viewsController.creationDelay = Completer<void>(); + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + + viewsController.creationDelay!.complete(); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgetsWithLeakTracking('AppKitView survives widget tree change', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals(<FakeAppKitView>[ + FakeAppKitView(currentViewId + 1, 'webview'), + ]), + ); + }); + + testWidgetsWithLeakTracking('Create AppKitView with params', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + creationParams: 'creation parameters', + creationParamsCodec: StringCodec(), + ), + ), + ), + ); + + final FakeAppKitView fakeView = viewsController.views.first; + final Uint8List rawCreationParams = fakeView.creationParams!; + final ByteData byteData = ByteData.view( + rawCreationParams.buffer, + rawCreationParams.offsetInBytes, + rawCreationParams.lengthInBytes, + ); + final dynamic actualParams = const StringCodec().decodeMessage(byteData); + + expect(actualParams, 'creation parameters'); + expect( + viewsController.views, + unorderedEquals(<FakeAppKitView>[ + FakeAppKitView(currentViewId + 1, 'webview', fakeView.creationParams), + ]), + ); + }); + + // TODO(schectman): De-skip the following tests once macOS gesture recognizers are present. + // https://github.com/flutter/flutter/issues/128519 + testWidgetsWithLeakTracking('AppKitView accepts gestures', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 0); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + }, skip: true); // https://github.com/flutter/flutter/issues/128519 + + testWidgetsWithLeakTracking('AppKitView transparent hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + const Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.transparent, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 0); + + expect(numPointerDownsOnParent, 1); + }, skip: true); // https://github.com/flutter/flutter/issues/128519 + + testWidgetsWithLeakTracking('AppKitView translucent hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + const Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.translucent, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + + expect(numPointerDownsOnParent, 1); + }, skip: true); // https://github.com/flutter/flutter/issues/128519 + + testWidgetsWithLeakTracking('AppKitView opaque hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + const Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(numPointerDownsOnParent, 0); + }, skip: true); // https://github.com/flutter/flutter/issues/128519 + + testWidgetsWithLeakTracking('UiKitView can lose gesture arenas', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + bool verticalDragAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.all(10.0), + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + child: const SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, true); + expect(viewsController.gesturesAccepted[currentViewId + 1], 0); + expect(viewsController.gesturesRejected[currentViewId + 1], 1); + }); + + testWidgetsWithLeakTracking('UiKitView tap gesture recognizers', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + bool gestureAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + gestureAcceptedByParent = true; + }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<VerticalDragGestureRecognizer>( + () { + return VerticalDragGestureRecognizer(); + }, + ), + }, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + await gesture.up(); + + expect(gestureAcceptedByParent, false); + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView long press gesture recognizers', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + bool gestureAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onLongPress: () { + gestureAcceptedByParent = true; + }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<LongPressGestureRecognizer>( + () { + return LongPressGestureRecognizer(); + }, + ), + }, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + await tester.longPressAt(const Offset(50.0, 50.0)); + + expect(gestureAcceptedByParent, false); + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView drag gesture recognizers', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + bool verticalDragAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<TapGestureRecognizer>( + () { + return TapGestureRecognizer(); + }, + ), + }, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + await tester.tapAt(const Offset(50.0, 50.0)); + + expect(verticalDragAcceptedByParent, false); + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView can claim gesture after all pointers are up', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + bool verticalDragAcceptedByParent = false; + // The long press recognizer rejects the gesture after the AndroidView gets the pointer up event. + // This test makes sure that the Android view can win the gesture after it got the pointer up event. + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + onLongPress: () { }, + child: const SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, false); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView rebuilt during gesture', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + const Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + + await tester.pumpWidget( + const Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ); + + await gesture.up(); + + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView with eager gesture recognizer', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView( + viewType: 'webview', + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<OneSequenceGestureRecognizer>( + () => EagerGestureRecognizer(), + ), + }, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + await tester.startGesture(const Offset(50.0, 50.0)); + + // Normally (without the eager gesture recognizer) after just the pointer down event + // no gesture arena member will claim the arena (so no motion events will be dispatched to + // the Android view). Here we assert that with the eager recognizer in the gesture team the + // pointer down event is immediately dispatched. + expect(viewsController.gesturesAccepted[currentViewId + 1], 1); + expect(viewsController.gesturesRejected[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking('UiKitView rejects gestures absorbed by siblings', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + Stack( + alignment: Alignment.topLeft, + children: <Widget>[ + const UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + Container( + color: const Color.fromARGB(255, 255, 255, 255), + width: 100, + height: 100, + ), + ], + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(viewsController.gesturesRejected[currentViewId + 1], 1); + expect(viewsController.gesturesAccepted[currentViewId + 1], 0); + }); + + testWidgetsWithLeakTracking( + 'UiKitView rejects gestures absorbed by siblings if the touch is outside of the platform view bounds but inside platform view frame', + (WidgetTester tester) async { + // UiKitView is positioned at (left=0, top=100, right=300, bottom=600). + // Opaque container is on top of the UiKitView positioned at (left=0, top=500, right=300, bottom=600). + // Touch on (550, 150) is expected to be absorbed by the container. + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + SizedBox( + width: 300, + height: 600, + child: Stack( + alignment: Alignment.topLeft, + children: <Widget>[ + Transform.translate( + offset: const Offset(0, 100), + child: const SizedBox( + width: 300, + height: 500, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + Transform.translate( + offset: const Offset(0, 500), + child: Container( + color: const Color.fromARGB(255, 255, 255, 255), + width: 300, + height: 100, + ), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final TestGesture gesture = await tester.startGesture(const Offset(150, 550)); + await gesture.up(); + + expect(viewsController.gesturesRejected[currentViewId + 1], 1); + expect(viewsController.gesturesAccepted[currentViewId + 1], 0); + }, + ); + + testWidgetsWithLeakTracking('UiKitView rebuilt with same gestureRecognizers', (WidgetTester tester) async { + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + int factoryInvocationCount = 0; + EagerGestureRecognizer constructRecognizer() { + factoryInvocationCount += 1; + return EagerGestureRecognizer(); + } + + await tester.pumpWidget( + UiKitView( + viewType: 'webview', + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<EagerGestureRecognizer>(constructRecognizer), + }, + layoutDirection: TextDirection.ltr, + ), + ); + + await tester.pumpWidget( + UiKitView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.translucent, + gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{ + Factory<EagerGestureRecognizer>(constructRecognizer), + }, + layoutDirection: TextDirection.ltr, + ), + ); + + expect(factoryInvocationCount, 1); + }); + + testWidgetsWithLeakTracking('AppKitView can take input focus', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + final GlobalKey containerKey = GlobalKey(); + await tester.pumpWidget( + Center( + child: Column( + children: <Widget>[ + const SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + Focus( + debugLabel: 'container', + child: Container(key: containerKey), + ), + ], + ), + ), + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final Focus uiKitViewFocusWidget = tester.widget( + find.descendant( + of: find.byType(AppKitView), + matching: find.byType(Focus), + ), + ); + final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!; + final Element containerElement = tester.element(find.byKey(containerKey)); + final FocusNode containerFocusNode = Focus.of(containerElement); + + containerFocusNode.requestFocus(); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isTrue); + expect(uiKitViewFocusNode.hasFocus, isFalse); + + viewsController.invokeViewFocused(currentViewId + 1); + + await tester.pump(); + + expect(containerFocusNode.hasFocus, isFalse); + expect(uiKitViewFocusNode.hasFocus, isTrue); + }); + + testWidgetsWithLeakTracking('AppKitView sends TextInput.setPlatformViewClient when focused', (WidgetTester tester) async { + + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr) + ); + + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final Focus uiKitViewFocusWidget = tester.widget( + find.descendant( + of: find.byType(AppKitView), + matching: find.byType(Focus), + ), + ); + final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!; + + late Map<String, dynamic> channelArguments; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall call) { + if (call.method == 'TextInput.setPlatformViewClient') { + channelArguments = call.arguments as Map<String, dynamic>; + } + return null; + }); + + expect(uiKitViewFocusNode.hasFocus, false); + + uiKitViewFocusNode.requestFocus(); + await tester.pump(); + + expect(uiKitViewFocusNode.hasFocus, true); + expect(channelArguments['platformViewId'], currentViewId + 1); + }); + + testWidgetsWithLeakTracking('FocusNode is disposed on UIView dispose', (WidgetTester tester) async { + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + // casting to dynamic is required since the state class is private. + // ignore: avoid_dynamic_calls, invalid_assignment + final FocusNode node = (tester.state(find.byType(AppKitView)) as dynamic).focusNode; + expect(() => ChangeNotifier.debugAssertNotDisposed(node), isNot(throwsAssertionError)); + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + expect(() => ChangeNotifier.debugAssertNotDisposed(node), throwsAssertionError); + }); + + testWidgetsWithLeakTracking('AppKitView has correct semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + expect(currentViewId, greaterThanOrEqualTo(0)); + final FakeMacosPlatformViewsController viewsController = FakeMacosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + Semantics( + container: true, + child: const Align( + alignment: Alignment.bottomRight, + child: SizedBox( + width: 200.0, + height: 100.0, + child: AppKitView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + // First frame is before the platform view was created so the render object + // is not yet in the tree. + await tester.pump(); + + final SemanticsNode semantics = tester.getSemantics( + find.descendant( + of: find.byType(AppKitView), + matching: find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_AppKitPlatformView', + ), + ), + ); + + expect(semantics.platformViewId, currentViewId + 1); + expect(semantics.rect, const Rect.fromLTWH(0, 0, 200, 100)); + // A 200x100 rect positioned at bottom right of a 800x600 box. + expect(semantics.transform, Matrix4.translationValues(600, 500, 0)); + expect(semantics.childrenCount, 0); + + handle.dispose(); + }); + }); + group('Common PlatformView', () { late FakePlatformViewController controller; @@ -2231,7 +3153,7 @@ void main() { controller = FakePlatformViewController(0); }); - testWidgets('PlatformViewSurface should create platform view layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface should create platform view layer', (WidgetTester tester) async { final PlatformViewSurface surface = PlatformViewSurface( controller: controller, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -2241,7 +3163,7 @@ void main() { expect(() => tester.layers.whereType<PlatformViewLayer>().first, returnsNormally); }); - testWidgets('PlatformViewSurface can lose gesture arenas', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface can lose gesture arenas', (WidgetTester tester) async { bool verticalDragAcceptedByParent = false; await tester.pumpWidget( Align( @@ -2277,7 +3199,7 @@ void main() { ); }); - testWidgets('PlatformViewSurface gesture recognizers dispatch events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface gesture recognizers dispatch events', (WidgetTester tester) async { bool verticalDragAcceptedByParent = false; await tester.pumpWidget( Align( @@ -2316,7 +3238,7 @@ void main() { ); }); - testWidgets('PlatformViewSurface can claim gesture after all pointers are up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface can claim gesture after all pointers are up', (WidgetTester tester) async { bool verticalDragAcceptedByParent = false; // The long press recognizer rejects the gesture after the PlatformViewSurface gets the pointer up event. // This test makes sure that the PlatformViewSurface can win the gesture after it got the pointer up event. @@ -2351,7 +3273,7 @@ void main() { ); }); - testWidgets('PlatformViewSurface rebuilt during gesture', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface rebuilt during gesture', (WidgetTester tester) async { await tester.pumpWidget( Align( alignment: Alignment.topLeft, @@ -2393,7 +3315,7 @@ void main() { ); }); - testWidgets('PlatformViewSurface with eager gesture recognizer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface with eager gesture recognizer', (WidgetTester tester) async { await tester.pumpWidget( Align( alignment: Alignment.topLeft, @@ -2428,7 +3350,7 @@ void main() { ); }); - testWidgets('PlatformViewRenderBox reconstructed with same gestureRecognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewRenderBox reconstructed with same gestureRecognizers', (WidgetTester tester) async { int factoryInvocationCount = 0; EagerGestureRecognizer constructRecognizer() { ++factoryInvocationCount; @@ -2452,7 +3374,7 @@ void main() { expect(factoryInvocationCount, 2); }); - testWidgets('PlatformViewSurface rebuilt with same gestureRecognizers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewSurface rebuilt with same gestureRecognizers', (WidgetTester tester) async { int factoryInvocationCount = 0; EagerGestureRecognizer constructRecognizer() { ++factoryInvocationCount; @@ -2485,7 +3407,7 @@ void main() { expect(factoryInvocationCount, 1); }); - testWidgets( + testWidgetsWithLeakTracking( 'PlatformViewLink Widget init, should create a placeholder widget before onPlatformViewCreated and a PlatformViewSurface after', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); @@ -2529,7 +3451,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'PlatformViewLink widget should not trigger creation with an empty size', (WidgetTester tester) async { late PlatformViewController controller; @@ -2571,7 +3493,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'PlatformViewLink calls create when needed for Android texture display modes', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); @@ -2626,7 +3548,7 @@ void main() { }, ); - testWidgets('PlatformViewLink includes offset in create call when using texture layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink includes offset in create call when using texture layer', (WidgetTester tester) async { addTearDown(tester.view.reset); late FakeAndroidViewController controller; @@ -2670,7 +3592,7 @@ void main() { expect(controller.createPosition, const Offset(150, 75)); }); - testWidgets( + testWidgetsWithLeakTracking( 'PlatformViewLink does not double-call create for Android Hybrid Composition', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); @@ -2720,7 +3642,7 @@ void main() { }, ); - testWidgets('PlatformViewLink Widget dispose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink Widget dispose', (WidgetTester tester) async { late FakePlatformViewController disposedController; final PlatformViewLink platformViewLink = PlatformViewLink( viewType: 'webview', @@ -2745,7 +3667,7 @@ void main() { expect(disposedController.disposed, true); }); - testWidgets('PlatformViewLink handles onPlatformViewCreated when disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink handles onPlatformViewCreated when disposed', (WidgetTester tester) async { late PlatformViewCreationParams creationParams; late FakePlatformViewController controller; final PlatformViewLink platformViewLink = PlatformViewLink( @@ -2771,7 +3693,7 @@ void main() { expect(() => creationParams.onPlatformViewCreated(creationParams.id), returnsNormally); }); - testWidgets('PlatformViewLink widget survives widget tree change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink widget survives widget tree change', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final List<int> ids = <int>[]; @@ -2826,7 +3748,7 @@ void main() { ); }); - testWidgets('PlatformViewLink re-initializes when view type changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink re-initializes when view type changes', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final List<int> ids = <int>[]; final List<int> surfaceViewIds = <int>[]; @@ -2898,7 +3820,7 @@ void main() { ); }); - testWidgets('PlatformViewLink can take any widget to return in the SurfaceFactory', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink can take any widget to return in the SurfaceFactory', (WidgetTester tester) async { final PlatformViewLink platformViewLink = PlatformViewLink( viewType: 'webview', onCreatePlatformView: (PlatformViewCreationParams params) { @@ -2915,7 +3837,7 @@ void main() { expect(() => tester.allWidgets.whereType<Container>().first, returnsNormally); }); - testWidgets('PlatformViewLink manages the focus properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink manages the focus properly', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); late FakePlatformViewController controller; late ValueChanged<bool> focusChanged; @@ -2980,7 +3902,7 @@ void main() { expect(controller.focusCleared, true); }); - testWidgets('PlatformViewLink sets a platform view text input client when focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformViewLink sets a platform view text input client when focused', (WidgetTester tester) async { late FakePlatformViewController controller; late int viewId; @@ -3030,7 +3952,7 @@ void main() { }); }); - testWidgets('Platform views respect hitTestBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Platform views respect hitTestBehavior', (WidgetTester tester) async { final FakePlatformViewController controller = FakePlatformViewController(0); final List<String> logs = <String>[]; @@ -3169,7 +4091,7 @@ void main() { expect(controller.dispatchedPointerEvents[0], isA<PointerHoverEvent>()); }); - testWidgets('HtmlElementView can be instantiated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('HtmlElementView can be instantiated', (WidgetTester tester) async { late final Widget htmlElementView; expect(() { htmlElementView = const HtmlElementView(viewType: 'webview'); @@ -3189,7 +4111,7 @@ void main() { // This file runs on non-web platforms, so we expect `HtmlElementView` to // fail. final dynamic exception = tester.takeException(); - expect(exception, isAssertionError); + expect(exception, isUnimplementedError); expect(exception.toString(), contains('HtmlElementView is only available on Flutter Web')); }); } diff --git a/packages/flutter/test/widgets/pop_scope_test.dart b/packages/flutter/test/widgets/pop_scope_test.dart new file mode 100644 index 0000000000000..23f7569ee98f4 --- /dev/null +++ b/packages/flutter/test/widgets/pop_scope_test.dart @@ -0,0 +1,363 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +import 'navigator_utils.dart'; + +void main() { + bool? lastFrameworkHandlesBack; + setUp(() async { + lastFrameworkHandlesBack = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA<bool>()); + lastFrameworkHandlesBack = methodCall.arguments as bool; + } + return; + }); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/lifecycle', + const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), + (ByteData? data) {}, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + testWidgetsWithLeakTracking('toggling canPop on root route allows/prevents backs', (WidgetTester tester) async { + bool canPop = false; + late StateSetter setState; + late BuildContext context; + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext buildContext) => Scaffold( + body: StatefulBuilder( + builder: (BuildContext buildContext, StateSetter stateSetter) { + context = buildContext; + setState = stateSetter; + return PopScope( + canPop: canPop, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text('Home/PopScope Page'), + ], + ), + ), + ); + }, + ), + ), + }, + ), + ); + + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); + + setState(() { + canPop = true; + }); + await tester.pump(); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); + }, + variant: TargetPlatformVariant.all(), + ); + + testWidgetsWithLeakTracking('toggling canPop on secondary route allows/prevents backs', (WidgetTester tester) async { + final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); + bool canPop = true; + late StateSetter setState; + late BuildContext homeContext; + late BuildContext oneContext; + late bool lastPopSuccess; + await tester.pumpWidget( + MaterialApp( + navigatorKey: nav, + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext context) { + homeContext = context; + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + const Text('Home Page'), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed('/one'); + }, + child: const Text('Next'), + ), + ], + ), + ), + ); + }, + '/one': (BuildContext context) => Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter stateSetter) { + oneContext = context; + setState = stateSetter; + return PopScope( + canPop: canPop, + onPopInvoked: (bool didPop) { + lastPopSuccess = didPop; + }, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text('PopScope Page'), + ], + ), + ), + ); + }, + ), + ), + }, + ), + ); + + expect(find.text('Home Page'), findsOneWidget); + expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); + + await tester.tap(find.text('Next')); + await tester.pumpAndSettle(); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + // When canPop is true, can use pop to go back. + nav.currentState!.maybePop(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, true); + expect(find.text('Home Page'), findsOneWidget); + expect(find.text('PopScope Page'), findsNothing); + expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + await tester.tap(find.text('Next')); + await tester.pumpAndSettle(); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + // When canPop is true, can use system back to go back. + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, true); + expect(find.text('Home Page'), findsOneWidget); + expect(find.text('PopScope Page'), findsNothing); + expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + await tester.tap(find.text('Next')); + await tester.pumpAndSettle(); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + setState(() { + canPop = false; + }); + await tester.pump(); + + // When canPop is false, can't use pop to go back. + nav.currentState!.maybePop(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, false); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop); + + // When canPop is false, can't use system back to go back. + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, false); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop); + + // Toggle canPop back to true and back works again. + setState(() { + canPop = true; + }); + await tester.pump(); + + nav.currentState!.maybePop(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, true); + expect(find.text('Home Page'), findsOneWidget); + expect(find.text('PopScope Page'), findsNothing); + expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + await tester.tap(find.text('Next')); + await tester.pumpAndSettle(); + expect(find.text('PopScope Page'), findsOneWidget); + expect(find.text('Home Page'), findsNothing); + expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(lastPopSuccess, true); + expect(find.text('Home Page'), findsOneWidget); + expect(find.text('PopScope Page'), findsNothing); + expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + }, + variant: TargetPlatformVariant.all(), + ); + + testWidgetsWithLeakTracking('removing PopScope from the tree removes its effect on navigation', (WidgetTester tester) async { + bool usePopScope = true; + late StateSetter setState; + late BuildContext context; + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + routes: <String, WidgetBuilder>{ + '/': (BuildContext buildContext) => Scaffold( + body: StatefulBuilder( + builder: (BuildContext buildContext, StateSetter stateSetter) { + context = buildContext; + setState = stateSetter; + const Widget child = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text('Home/PopScope Page'), + ], + ), + ); + if (!usePopScope) { + return child; + } + return const PopScope( + canPop: false, + child: child, + ); + }, + ), + ), + }, + ), + ); + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); + + setState(() { + usePopScope = false; + }); + await tester.pump(); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); + }, + variant: TargetPlatformVariant.all(), + ); + + testWidgetsWithLeakTracking('identical PopScopes', (WidgetTester tester) async { + bool usePopScope1 = true; + bool usePopScope2 = true; + late StateSetter setState; + late BuildContext context; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext buildContext, StateSetter stateSetter) { + context = buildContext; + setState = stateSetter; + return Column( + children: <Widget>[ + if (usePopScope1) + const PopScope( + canPop: false, + child: Text('hello'), + ), + if (usePopScope2) + const PopScope( + canPop: false, + child: Text('hello'), + ), + ], + ); + }, + ), + ), + ), + ); + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); + + // Despite being in the widget tree twice, the ModalRoute has only ever + // registered one PopScopeInterface for it. Removing one makes it think that + // both have been removed. + setState(() { + usePopScope1 = false; + }); + await tester.pump(); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); + + setState(() { + usePopScope2 = false; + }); + await tester.pump(); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); + }, + variant: TargetPlatformVariant.all(), + ); +} diff --git a/packages/flutter/test/widgets/positioned_test.dart b/packages/flutter/test/widgets/positioned_test.dart index 834bd845f0af3..33a8331bda32d 100644 --- a/packages/flutter/test/widgets/positioned_test.dart +++ b/packages/flutter/test/widgets/positioned_test.dart @@ -7,9 +7,10 @@ import 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Positioned constructors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Positioned constructors', (WidgetTester tester) async { final Widget child = Container(); final Positioned a = Positioned( left: 101.0, @@ -56,7 +57,7 @@ void main() { expect(c.height, null); }); - testWidgets('Can animate position data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can animate position data', (WidgetTester tester) async { final RelativeRectTween rect = RelativeRectTween( begin: RelativeRect.fromRect( const Rect.fromLTRB(10.0, 20.0, 20.0, 30.0), diff --git a/packages/flutter/test/widgets/range_maintaining_scroll_physics_test.dart b/packages/flutter/test/widgets/range_maintaining_scroll_physics_test.dart index cc55aecf4729e..4b3eea78f7666 100644 --- a/packages/flutter/test/widgets/range_maintaining_scroll_physics_test.dart +++ b/packages/flutter/test/widgets/range_maintaining_scroll_physics_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class ExpandingBox extends StatefulWidget { const ExpandingBox({ super.key, required this.collapsedSize, required this.expandedSize }); @@ -53,7 +54,7 @@ class _ExpandingBoxState extends State<ExpandingBox> with AutomaticKeepAliveClie } void main() { - testWidgets('shrink listview', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shrink listview', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: ListView.builder( itemBuilder: (BuildContext context, int index) => index == 0 @@ -98,7 +99,7 @@ void main() { expect(position.pixels, 100.0); }); - testWidgets('shrink listview while dragging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shrink listview while dragging', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: ListView.builder( itemBuilder: (BuildContext context, int index) => index == 0 @@ -157,7 +158,7 @@ void main() { expect(position.pixels, 50.0); }); - testWidgets('shrink listview while ballistic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shrink listview while ballistic', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: GestureDetector( onTap: () { assert(false); }, @@ -220,7 +221,7 @@ void main() { expect(position.pixels, 0.0); }); - testWidgets('expanding page views', (WidgetTester tester) async { + testWidgetsWithLeakTracking('expanding page views', (WidgetTester tester) async { await tester.pumpWidget(const Padding(padding: EdgeInsets.only(right: 200.0), child: TabBarDemo())); await tester.tap(find.text('bike')); await tester.pump(); @@ -231,7 +232,7 @@ void main() { expect(bike2.center, bike1.shift(const Offset(100.0, 0.0)).center); }); - testWidgets('changing the size of the viewport when overscrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('changing the size of the viewport when overscrolled', (WidgetTester tester) async { Widget build(double height) { return Directionality( textDirection: TextDirection.rtl, @@ -265,7 +266,7 @@ void main() { expect(oldPosition, newPosition); }); - testWidgets('inserting and removing an item when overscrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inserting and removing an item when overscrolled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/62890 const double itemExtent = 100.0; diff --git a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart index 64f927b5f95c9..4bb44325bd967 100644 --- a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart +++ b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart @@ -5,19 +5,22 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Can dispose without keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can dispose without keyboard', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget(RawKeyboardListener(focusNode: focusNode, child: Container())); await tester.pumpWidget(RawKeyboardListener(focusNode: focusNode, child: Container())); await tester.pumpWidget(Container()); }); - testWidgets('Fuchsia key event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fuchsia key event', (WidgetTester tester) async { final List<RawKeyEvent> events = <RawKeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( RawKeyboardListener( @@ -43,13 +46,13 @@ void main() { expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue); await tester.pumpWidget(Container()); - focusNode.dispose(); }, skip: isBrowser); // [intended] This is a Fuchsia-specific test. - testWidgets('Web key event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Web key event', (WidgetTester tester) async { final List<RawKeyEvent> events = <RawKeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( RawKeyboardListener( @@ -74,13 +77,13 @@ void main() { expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue); await tester.pumpWidget(Container()); - focusNode.dispose(); }); - testWidgets('Defunct listeners do not receive events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defunct listeners do not receive events', (WidgetTester tester) async { final List<RawKeyEvent> events = <RawKeyEvent>[]; final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( RawKeyboardListener( @@ -108,6 +111,5 @@ void main() { expect(events.length, 0); await tester.pumpWidget(Container()); - focusNode.dispose(); }); } diff --git a/packages/flutter/test/widgets/reassemble_test.dart b/packages/flutter/test/widgets/reassemble_test.dart index 32eff20023226..2145a34cbe6f1 100644 --- a/packages/flutter/test/widgets/reassemble_test.dart +++ b/packages/flutter/test/widgets/reassemble_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('reassemble does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reassemble does not crash', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Text('Hello World'), )); diff --git a/packages/flutter/test/widgets/render_object_element_test.dart b/packages/flutter/test/widgets/render_object_element_test.dart index a485d3fed6950..95189e03bba9d 100644 --- a/packages/flutter/test/widgets/render_object_element_test.dart +++ b/packages/flutter/test/widgets/render_object_element_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; @immutable class Pair<T> { @@ -239,7 +240,7 @@ class RenderSwapper extends RenderBox { BoxParentData parentDataFor(RenderObject renderObject) => renderObject.parentData! as BoxParentData; void main() { - testWidgets('RenderObjectElement *RenderObjectChild methods get called with correct arguments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObjectElement *RenderObjectChild methods get called with correct arguments', (WidgetTester tester) async { const Key redKey = ValueKey<String>('red'); const Key blueKey = ValueKey<String>('blue'); Widget widget() { diff --git a/packages/flutter/test/widgets/render_object_widget_test.dart b/packages/flutter/test/widgets/render_object_widget_test.dart index 39241cb18666a..7aac4ea187672 100644 --- a/packages/flutter/test/widgets/render_object_widget_test.dart +++ b/packages/flutter/test/widgets/render_object_widget_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/recording_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final BoxDecoration kBoxDecorationA = BoxDecoration(border: nonconst(null)); final BoxDecoration kBoxDecorationB = BoxDecoration(border: nonconst(null)); @@ -77,7 +76,7 @@ class TestNonVisitingRenderObject extends RenderBox with RenderObjectWithChildMi } void main() { - testWidgets('RenderObjectWidget smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObjectWidget smoke test', (WidgetTester tester) async { await tester.pumpWidget(DecoratedBox(decoration: kBoxDecorationA)); SingleChildRenderObjectElement element = tester.element(find.byElementType(SingleChildRenderObjectElement)); @@ -96,7 +95,7 @@ void main() { expect(renderObject.position, equals(DecorationPosition.background)); }); - testWidgets('RenderObjectWidget can add and remove children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObjectWidget can add and remove children', (WidgetTester tester) async { void checkFullTree() { final SingleChildRenderObjectElement element = @@ -180,7 +179,7 @@ void main() { childBareTree(); }); - testWidgets('Detached render tree is intact', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Detached render tree is intact', (WidgetTester tester) async { await tester.pumpWidget(DecoratedBox( decoration: kBoxDecorationA, @@ -222,7 +221,7 @@ void main() { expect(grandChild.child, isNull); }); - testWidgets('Can watch inherited widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can watch inherited widgets', (WidgetTester tester) async { final Key boxKey = UniqueKey(); final TestOrientedBox box = TestOrientedBox(key: boxKey); @@ -244,7 +243,7 @@ void main() { expect(decoration.color, equals(const Color(0xFF0000FF))); }); - testWidgets('RenderObject not visiting children provides helpful error message', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObject not visiting children provides helpful error message', (WidgetTester tester) async { await tester.pumpWidget( TestNonVisitingWidget( child: Container(color: const Color(0xFFED1D7F)), diff --git a/packages/flutter/test/widgets/reorderable_list_test.dart b/packages/flutter/test/widgets/reorderable_list_test.dart index 1437c597109fa..f50b0380eed23 100644 --- a/packages/flutter/test/widgets/reorderable_list_test.dart +++ b/packages/flutter/test/widgets/reorderable_list_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('SliverReorderableList works well when having gestureSettings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList works well when having gestureSettings', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103404 const int itemCount = 5; int onReorderCallCount = 0; @@ -67,7 +68,7 @@ void main() { expect(items, orderedEquals(<int>[1, 0, 2, 3, 4])); }); - testWidgets('SliverReorderableList item has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList item has correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const int itemCount = 5; int onReorderCallCount = 0; @@ -127,7 +128,7 @@ void main() { semantics.dispose(); }); - testWidgets('SliverReorderableList custom semantics action has correct label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList custom semantics action has correct label', (WidgetTester tester) async { const int itemCount = 5; final List<int> items = List<int>.generate(itemCount, (int index) => index); // The list has five elements of height 100 @@ -166,7 +167,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/100451 - testWidgets('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; @@ -206,7 +207,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/88191 - testWidgets('Do not crash when dragging with two fingers simultaneously', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when dragging with two fingers simultaneously', (WidgetTester tester) async { final List<int> items = List<int>.generate(3, (int index) => index); void handleReorder(int fromIndex, int toIndex) { if (toIndex > fromIndex) { @@ -252,7 +253,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('negative itemCount should assert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('negative itemCount should assert', (WidgetTester tester) async { final List<int> items = <int>[1, 2, 3]; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( @@ -284,7 +285,7 @@ void main() { expect(tester.takeException(), isA<AssertionError>()); }); - testWidgets('zero itemCount should not build widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('zero itemCount should not build widget', (WidgetTester tester) async { final List<int> items = <int>[1, 2, 3]; await tester.pumpWidget(MaterialApp( home: StatefulBuilder( @@ -331,7 +332,7 @@ void main() { expect(find.text('after'), findsOneWidget); }); - testWidgets('SliverReorderableList, drag and drop, fixed height items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList, drag and drop, fixed height items', (WidgetTester tester) async { final List<int> items = List<int>.generate(8, (int index) => index); Future<void> pressDragRelease(Offset start, Offset delta) async { @@ -397,7 +398,7 @@ void main() { expect(items, orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7])); }); - testWidgets('SliverReorderableList, items inherit DefaultTextStyle, IconTheme', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList, items inherit DefaultTextStyle, IconTheme', (WidgetTester tester) async { const Color textColor = Color(0xffffffff); const Color iconColor = Color(0xff0000ff); @@ -450,7 +451,7 @@ void main() { expect(getTextStyle().color, textColor); }); - testWidgets('SliverReorderableList - custom proxyDecorator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList - custom proxyDecorator', (WidgetTester tester) async { const ValueKey<String> fadeTransitionKey = ValueKey<String>('reordered-fade'); await tester.pumpWidget( @@ -513,7 +514,7 @@ void main() { expect(getItemFadeTransition(), findsNothing); }); - testWidgets('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -567,7 +568,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/83224. await tester.pumpWidget( MaterialApp( @@ -622,7 +623,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('SliverReorderableList - properly animates the drop in a reversed list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList - properly animates the drop in a reversed list', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110949 final List<int> items = List<int>.generate(8, (int index) => index); @@ -673,7 +674,7 @@ void main() { expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 400)); }); - testWidgets('SliverReorderableList - properly animates the drop at starting position in a reversed list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList - properly animates the drop at starting position in a reversed list', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/84625 final List<int> items = List<int>.generate(8, (int index) => index); @@ -716,7 +717,7 @@ void main() { expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 500)); }); - testWidgets('SliverReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { final List<int> items = List<int>.generate(8, (int index) => index); int? startIndex, endIndex; final Finder item0 = find.textContaining('item 0'); @@ -767,7 +768,7 @@ void main() { expect(endIndex, equals(0)); }); - testWidgets('ReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async { final List<int> items = List<int>.generate(8, (int index) => index); int? startIndex, endIndex; final Finder item0 = find.textContaining('item 0'); @@ -840,7 +841,7 @@ void main() { - testWidgets('ReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; expect(() => ReorderableList( itemBuilder: (BuildContext context, int index) { @@ -860,7 +861,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('SliverReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; expect(() => SliverReorderableList( itemBuilder: (BuildContext context, int index) { @@ -880,7 +881,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( @@ -925,7 +926,7 @@ void main() { expect(item2Height, 30.0); }); - testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( @@ -968,7 +969,7 @@ void main() { }); group('ReorderableDragStartListener', () { - testWidgets('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async { const int itemCount = 5; int onReorderCallCount = 0; final List<int> items = List<int>.generate(itemCount, (int index) => index); @@ -1014,7 +1015,7 @@ void main() { expect(items, orderedEquals(<int>[1, 0, 2, 3, 4])); }); - testWidgets('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async { const int itemCount = 5; int onReorderCallCount = 0; final List<int> items = List<int>.generate(itemCount, (int index) => index); @@ -1063,7 +1064,7 @@ void main() { }); group('ReorderableDelayedDragStartListener', () { - testWidgets('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async { const int itemCount = 5; int onReorderCallCount = 0; final List<int> items = List<int>.generate(itemCount, (int index) => index); @@ -1110,7 +1111,7 @@ void main() { expect(items, orderedEquals(<int>[1, 0, 2, 3, 4])); }); - testWidgets('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async { const int itemCount = 5; int onReorderCallCount = 0; final List<int> items = List<int>.generate(itemCount, (int index) => index); @@ -1158,7 +1159,7 @@ void main() { }); }); - testWidgets('SliverReorderableList properly disposes items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList properly disposes items', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/105010 const int itemCount = 5; final List<int> items = List<int>.generate(itemCount, (int index) => index); @@ -1225,7 +1226,7 @@ void main() { expect(item0, findsNothing); }); - testWidgets('SliverReorderableList auto scrolls speed is configurable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverReorderableList auto scrolls speed is configurable', (WidgetTester tester) async { Future<void> pumpFor({ required Duration duration, Duration interval = const Duration(milliseconds: 50), @@ -1243,6 +1244,7 @@ void main() { Future<double> pumpListAndDrag({required double autoScrollerVelocityScalar}) async { final List<int> items = List<int>.generate(10, (int index) => index); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( MaterialApp( @@ -1309,6 +1311,51 @@ void main() { expect(offsetForFastScroller / offsetForSlowScroller, fastVelocityScalar / slowVelocityScalar); }); + + testWidgetsWithLeakTracking('Null check error when dragging and dropping last element into last index with reverse:true', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/132077 + const int itemCount = 5; + final List<String> items = List<String>.generate(itemCount, (int index) => 'Item ${index+1}'); + + await tester.pumpWidget( + MaterialApp( + home: ReorderableList( + onReorder: (int oldIndex, int newIndex) { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final String item = items.removeAt(oldIndex); + items.insert(newIndex, item); + }, + itemCount: items.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: Key('$index'), + index: index, + child: Material( + child: ListTile( + title: Text(items[index]), + ), + ), + ); + }, + ), + ) + ); + + // Start gesture on last item + final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('Item 5'))); + await tester.pump(kLongPressTimeout); + + // Drag to move up the last item, and drop at the last index + await drag.moveBy(const Offset(0, -50)); + await tester.pump(); + await drag.up(); + await tester.pumpAndSettle(); + + expect(tester.takeException(), null); + }); } class TestList extends StatelessWidget { diff --git a/packages/flutter/test/widgets/reparent_state_harder_test.dart b/packages/flutter/test/widgets/reparent_state_harder_test.dart index d7029bb888192..3deb76318e008 100644 --- a/packages/flutter/test/widgets/reparent_state_harder_test.dart +++ b/packages/flutter/test/widgets/reparent_state_harder_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // This is a regression test for https://github.com/flutter/flutter/issues/5588. @@ -92,7 +93,7 @@ class RekeyableDummyStatefulWidgetWrapperState extends State<RekeyableDummyState } void main() { - testWidgets('Handle GlobalKey reparenting in weird orders', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Handle GlobalKey reparenting in weird orders', (WidgetTester tester) async { // This is a bit of a weird test so let's try to explain it a bit. // diff --git a/packages/flutter/test/widgets/reparent_state_test.dart b/packages/flutter/test/widgets/reparent_state_test.dart index a14505a877b81..ce7ab6bb7f7f7 100644 --- a/packages/flutter/test/widgets/reparent_state_test.dart +++ b/packages/flutter/test/widgets/reparent_state_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class StateMarker extends StatefulWidget { const StateMarker({ super.key, this.child }); @@ -50,7 +51,7 @@ class DeactivateLoggerState extends State<DeactivateLogger> { } void main() { - testWidgets('can reparent state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can reparent state', (WidgetTester tester) async { final GlobalKey left = GlobalKey(); final GlobalKey right = GlobalKey(); @@ -130,7 +131,7 @@ void main() { expect(right.currentState, isNull); }); - testWidgets('can reparent state with multichild widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can reparent state with multichild widgets', (WidgetTester tester) async { final GlobalKey left = GlobalKey(); final GlobalKey right = GlobalKey(); @@ -198,7 +199,7 @@ void main() { expect(right.currentState, isNull); }); - testWidgets('can with scrollable list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can with scrollable list', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(StateMarker(key: key)); @@ -231,7 +232,7 @@ void main() { expect(keyState.marker, equals('marked')); }); - testWidgets('Reparent during update children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reparent during update children', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(Stack( @@ -268,7 +269,7 @@ void main() { expect(keyState.marker, equals('marked')); }); - testWidgets('Reparent to child during update children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reparent to child during update children', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(Stack( @@ -330,7 +331,7 @@ void main() { expect(keyState.marker, equals('marked')); }); - testWidgets('Deactivate implies build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Deactivate implies build', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<String> log = <String>[]; final DeactivateLogger logger = DeactivateLogger(key: key, log: log); @@ -352,7 +353,7 @@ void main() { expect(log, isEmpty); }); - testWidgets('Reparenting with multiple moves', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reparenting with multiple moves', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final GlobalKey key3 = GlobalKey(); diff --git a/packages/flutter/test/widgets/reparent_state_with_layout_builder_test.dart b/packages/flutter/test/widgets/reparent_state_with_layout_builder_test.dart index 9578888c30fdd..567f344e823e6 100644 --- a/packages/flutter/test/widgets/reparent_state_with_layout_builder_test.dart +++ b/packages/flutter/test/widgets/reparent_state_with_layout_builder_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // This is a regression test for https://github.com/flutter/flutter/issues/5840. @@ -65,7 +66,7 @@ class StatefulCreationCounterState extends State<StatefulCreationCounter> { } void main() { - testWidgets('reparent state with layout builder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reparent state with layout builder', (WidgetTester tester) async { expect(StatefulCreationCounterState.creationCount, 0); await tester.pumpWidget(const Bar()); expect(StatefulCreationCounterState.creationCount, 1); @@ -75,7 +76,7 @@ void main() { expect(StatefulCreationCounterState.creationCount, 1); }); - testWidgets('Clean then reparent with dependencies', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Clean then reparent with dependencies', (WidgetTester tester) async { int layoutBuilderBuildCount = 0; late StateSetter keyedSetState; diff --git a/packages/flutter/test/widgets/restorable_property_test.dart b/packages/flutter/test/widgets/restorable_property_test.dart index ef33e0f5ebff0..835380a7ba0e3 100644 --- a/packages/flutter/test/widgets/restorable_property_test.dart +++ b/packages/flutter/test/widgets/restorable_property_test.dart @@ -4,28 +4,68 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('value is not accessible when not registered', (WidgetTester tester) async { - expect(() => RestorableNum<num>(0).value, throwsAssertionError); - expect(() => RestorableDouble(1.0).value, throwsAssertionError); - expect(() => RestorableInt(1).value, throwsAssertionError); - expect(() => RestorableString('hello').value, throwsAssertionError); - expect(() => RestorableBool(true).value, throwsAssertionError); - expect(() => RestorableNumN<num?>(0).value, throwsAssertionError); - expect(() => RestorableDoubleN(1.0).value, throwsAssertionError); - expect(() => RestorableIntN(1).value, throwsAssertionError); - expect(() => RestorableStringN('hello').value, throwsAssertionError); - expect(() => RestorableBoolN(true).value, throwsAssertionError); - expect(() => RestorableTextEditingController().value, throwsAssertionError); - expect(() => RestorableDateTime(DateTime(2020, 4, 3)).value, throwsAssertionError); - expect(() => RestorableDateTimeN(DateTime(2020, 4, 3)).value, throwsAssertionError); - expect(() => RestorableEnumN<TestEnum>(TestEnum.one, values: TestEnum.values).value, throwsAssertionError); - expect(() => RestorableEnum<TestEnum>(TestEnum.one, values: TestEnum.values).value, throwsAssertionError); - expect(() => _TestRestorableValue().value, throwsAssertionError); + testWidgetsWithLeakTracking('value is not accessible when not registered', (WidgetTester tester) async { + final RestorableNum<num> numValue = RestorableNum<num>(0); + addTearDown(numValue.dispose); + expect(() => numValue.value, throwsAssertionError); + final RestorableDouble doubleValue = RestorableDouble(1.0); + addTearDown(doubleValue.dispose); + expect(() => doubleValue.value, throwsAssertionError); + final RestorableInt intValue = RestorableInt(1); + addTearDown(intValue.dispose); + expect(() => intValue.value, throwsAssertionError); + final RestorableString stringValue = RestorableString('hello'); + addTearDown(stringValue.dispose); + expect(() => stringValue.value, throwsAssertionError); + final RestorableBool boolValue = RestorableBool(true); + addTearDown(boolValue.dispose); + expect(() => boolValue.value, throwsAssertionError); + final RestorableNumN<num?> nullableNumValue = RestorableNumN<num?>(0); + addTearDown(nullableNumValue.dispose); + expect(() => nullableNumValue.value, throwsAssertionError); + final RestorableDoubleN nullableDoubleValue = RestorableDoubleN(1.0); + addTearDown(nullableDoubleValue.dispose); + expect(() => nullableDoubleValue.value, throwsAssertionError); + final RestorableIntN nullableIntValue = RestorableIntN(1); + addTearDown(nullableIntValue.dispose); + expect(() => nullableIntValue.value, throwsAssertionError); + final RestorableStringN nullableStringValue = RestorableStringN('hello'); + addTearDown(nullableStringValue.dispose); + expect(() => nullableStringValue.value, throwsAssertionError); + final RestorableBoolN nullableBoolValue = RestorableBoolN(true); + addTearDown(nullableBoolValue.dispose); + expect(() => nullableBoolValue.value, throwsAssertionError); + final RestorableTextEditingController controllerValue = RestorableTextEditingController(); + addTearDown(controllerValue.dispose); + expect(() => controllerValue.value, throwsAssertionError); + final RestorableDateTime dateTimeValue = RestorableDateTime(DateTime(2020, 4, 3)); + addTearDown(dateTimeValue.dispose); + expect(() => dateTimeValue.value, throwsAssertionError); + final RestorableDateTimeN nullableDateTimeValue = RestorableDateTimeN(DateTime(2020, 4, 3)); + addTearDown(nullableDateTimeValue.dispose); + expect(() => nullableDateTimeValue.value, throwsAssertionError); + final RestorableEnumN<TestEnum> nullableEnumValue = RestorableEnumN<TestEnum>(TestEnum.one, values: TestEnum.values); + addTearDown(nullableEnumValue.dispose); + expect(() => nullableEnumValue.value, throwsAssertionError); + final RestorableEnum<TestEnum> enumValue = RestorableEnum<TestEnum>(TestEnum.one, values: TestEnum.values); + addTearDown(enumValue.dispose); + expect(() => enumValue.value, throwsAssertionError); + final _TestRestorableValue objectValue = _TestRestorableValue(); + addTearDown(objectValue.dispose); + expect(() => objectValue.value, throwsAssertionError); }); - testWidgets('work when not in restoration scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('$RestorableProperty dispatches creation in constructor', (WidgetTester widgetTester) async { + await expectLater( + await memoryEvents(() => RestorableDateTimeN(null).dispose(), RestorableDateTimeN), + areCreateAndDispose, + ); + }); + + testWidgetsWithLeakTracking('work when not in restoration scope', (WidgetTester tester) async { await tester.pumpWidget(const _RestorableWidget()); expect(find.text('hello world'), findsOneWidget); @@ -89,7 +129,7 @@ void main() { expect(find.text('guten tag'), findsOneWidget); }); - testWidgets('restart and restore', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restart and restore', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -180,7 +220,7 @@ void main() { expect(find.text('guten tag'), findsOneWidget); }); - testWidgets('restore to older state', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restore to older state', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -278,7 +318,7 @@ void main() { expect(find.text('hello world'), findsOneWidget); }); - testWidgets('call notifiers when value changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('call notifiers when value changes', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -460,7 +500,7 @@ void main() { expect(notifyLog, isEmpty); }); - testWidgets('RestorableValue calls didUpdateValue', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableValue calls didUpdateValue', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -484,7 +524,7 @@ void main() { expect(state.objectValue.didUpdateValueCallCount, 1); }); - testWidgets('RestorableEnum and RestorableEnumN assert if default value is not in enum', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableEnum and RestorableEnumN assert if default value is not in enum', (WidgetTester tester) async { expect(() => RestorableEnum<TestEnum>( TestEnum.four, values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four})), throwsAssertionError); @@ -493,33 +533,37 @@ void main() { values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four})), throwsAssertionError); }); - testWidgets('RestorableEnum and RestorableEnumN assert if unknown values are set', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableEnum and RestorableEnumN assert if unknown values are set', (WidgetTester tester) async { final RestorableEnum<TestEnum> enumMissingValue = RestorableEnum<TestEnum>( TestEnum.one, values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four}), ); + addTearDown(enumMissingValue.dispose); expect(() => enumMissingValue.value = TestEnum.four, throwsAssertionError); final RestorableEnumN<TestEnum> nullableEnumMissingValue = RestorableEnumN<TestEnum>( null, values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four}), ); + addTearDown(nullableEnumMissingValue.dispose); expect(() => nullableEnumMissingValue.value = TestEnum.four, throwsAssertionError); }); - testWidgets('RestorableEnum and RestorableEnumN assert if unknown values are restored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableEnum and RestorableEnumN assert if unknown values are restored', (WidgetTester tester) async { final RestorableEnum<TestEnum> enumMissingValue = RestorableEnum<TestEnum>( TestEnum.one, values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four}), ); + addTearDown(enumMissingValue.dispose); expect(() => enumMissingValue.fromPrimitives('four'), throwsAssertionError); final RestorableEnumN<TestEnum> nullableEnumMissingValue = RestorableEnumN<TestEnum>( null, values: TestEnum.values.toSet().difference(<TestEnum>{TestEnum.four}), ); + addTearDown(nullableEnumMissingValue.dispose); expect(() => nullableEnumMissingValue.fromPrimitives('four'), throwsAssertionError); }); - testWidgets('RestorableN types are properly defined', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorableN types are properly defined', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', child: _RestorableWidget(), @@ -626,6 +670,27 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi registerForRestoration(objectValue, 'object'); } + @override + void dispose() { + numValue.dispose(); + doubleValue.dispose(); + intValue.dispose(); + stringValue.dispose(); + boolValue.dispose(); + dateTimeValue.dispose(); + enumValue.dispose(); + nullableNumValue.dispose(); + nullableDoubleValue.dispose(); + nullableIntValue.dispose(); + nullableStringValue.dispose(); + nullableBoolValue.dispose(); + nullableDateTimeValue.dispose(); + nullableEnumValue.dispose(); + controllerValue.dispose(); + objectValue.dispose(); + super.dispose(); + } + void setProperties(VoidCallback callback) { setState(callback); } diff --git a/packages/flutter/test/widgets/restoration_mixin_test.dart b/packages/flutter/test/widgets/restoration_mixin_test.dart index 8eefb90df0fbf..7fb55feb461aa 100644 --- a/packages/flutter/test/widgets/restoration_mixin_test.dart +++ b/packages/flutter/test/widgets/restoration_mixin_test.dart @@ -4,13 +4,15 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'restoration.dart'; void main() { - testWidgets('claims bucket', (WidgetTester tester) async { + testWidgetsWithLeakTracking('claims bucket', (WidgetTester tester) async { const String id = 'hello world 1234'; final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); expect(rawData, isEmpty); @@ -35,8 +37,9 @@ void main() { expect(state.restoreStateLog.single, isNull); }); - testWidgets('claimed bucket with data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('claimed bucket with data', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); await tester.pumpWidget( @@ -57,8 +60,9 @@ void main() { expect(state.restoreStateLog.single, isNull); }); - testWidgets('renames existing bucket when new ID is provided via widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('renames existing bucket when new ID is provided via widget', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); await tester.pumpWidget( @@ -99,8 +103,9 @@ void main() { expect(state.toggleBucketLog, isEmpty); }); - testWidgets('renames existing bucket when didUpdateRestorationId is called', (WidgetTester tester) async { + testWidgetsWithLeakTracking('renames existing bucket when didUpdateRestorationId is called', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); await tester.pumpWidget( @@ -134,8 +139,9 @@ void main() { expect(state.toggleBucketLog, isEmpty); }); - testWidgets('Disposing widget removes its data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing widget removes its data', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); @@ -162,8 +168,9 @@ void main() { expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse); }); - testWidgets('toggling id between null and non-null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toggling id between null and non-null', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); @@ -222,9 +229,10 @@ void main() { expect(state.toggleBucketLog.single, same(bucket)); }); - testWidgets('move in and out of scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('move in and out of scope', (WidgetTester tester) async { final Key key = GlobalKey(); final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); @@ -284,8 +292,9 @@ void main() { expect(state.toggleBucketLog.single, same(bucket)); }); - testWidgets('moving scope moves its data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moving scope moves its data', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); final Key key = GlobalKey(); @@ -349,7 +358,7 @@ void main() { expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('moving-child'), isTrue); }); - testWidgets('restartAndRestore', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restartAndRestore', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -388,7 +397,7 @@ void main() { expect(state.toggleBucketLog, isEmpty); }); - testWidgets('restore while running', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restore while running', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -427,7 +436,7 @@ void main() { expect(state.toggleBucketLog, isEmpty); }); - testWidgets('can register additional property outside of restoreState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can register additional property outside of restoreState', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -464,7 +473,7 @@ void main() { expect(state.property.log, <String>['fromPrimitives', 'initWithValue']); }); - testWidgets('cannot register same property twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cannot register same property twice', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -494,7 +503,7 @@ void main() { expect(() => state.registerPropertyUnderSameId(), throwsAssertionError); }); - testWidgets('data of disabled property is not stored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('data of disabled property is not stored', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -534,7 +543,7 @@ void main() { expect(state.property.value, 10); // Initialized to default value. }); - testWidgets('Enabling property stores its data again', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Enabling property stores its data again', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -572,7 +581,7 @@ void main() { expect(state.property.value, 40); }); - testWidgets('Unregistering a property removes its data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unregistering a property removes its data', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', @@ -598,7 +607,7 @@ void main() { expect(state.bucket!.read<int>('additional'), 11); }); - testWidgets('Disposing a property unregisters it, but keeps data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing a property unregisters it, but keeps data', (WidgetTester tester) async { await tester.pumpWidget( const RootRestorationScope( restorationId: 'root-child', diff --git a/packages/flutter/test/widgets/restoration_scope_test.dart b/packages/flutter/test/widgets/restoration_scope_test.dart index 74ab521fc544f..aaabc66f6de0c 100644 --- a/packages/flutter/test/widgets/restoration_scope_test.dart +++ b/packages/flutter/test/widgets/restoration_scope_test.dart @@ -4,12 +4,13 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'restoration.dart'; void main() { group('UnmanagedRestorationScope', () { - testWidgets('makes bucket available to descendants', (WidgetTester tester) async { + testWidgetsWithLeakTracking('makes bucket available to descendants', (WidgetTester tester) async { final RestorationBucket bucket1 = RestorationBucket.empty( restorationId: 'foo', debugOwner: 'owner', @@ -39,7 +40,7 @@ void main() { expect(state.bucket, bucket2); }); - testWidgets('null bucket disables restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('null bucket disables restoration', (WidgetTester tester) async { await tester.pumpWidget( const UnmanagedRestorationScope( child: BucketSpy(), @@ -51,7 +52,7 @@ void main() { }); group('RestorationScope', () { - testWidgets('asserts when none is found', (WidgetTester tester) async { + testWidgetsWithLeakTracking('asserts when none is found', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget(WidgetsApp( color: const Color(0xD0FF0000), @@ -97,9 +98,10 @@ void main() { expect(RestorationScope.of(capturedContext), scope.bucket); }); - testWidgets('makes bucket available to descendants', (WidgetTester tester) async { + testWidgetsWithLeakTracking('makes bucket available to descendants', (WidgetTester tester) async { const String id = 'hello world 1234'; final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); expect(rawData, isEmpty); @@ -120,8 +122,9 @@ void main() { expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey(id), isTrue); }); - testWidgets('bucket for descendants contains data claimed from parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('bucket for descendants contains data claimed from parent', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); await tester.pumpWidget( @@ -140,8 +143,9 @@ void main() { expect(state.bucket!.read<int>('foo'), 22); }); - testWidgets('renames existing bucket when new ID is provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('renames existing bucket when new ID is provided', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); await tester.pumpWidget( @@ -178,8 +182,9 @@ void main() { expect(state.bucket, same(bucket)); }); - testWidgets('Disposing a scope removes its data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing a scope removes its data', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); @@ -207,8 +212,9 @@ void main() { expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isFalse); }); - testWidgets('no bucket for descendants when id is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no bucket for descendants when id is null', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{}); await tester.pumpWidget( @@ -251,7 +257,7 @@ void main() { expect(state.bucket, isNull); }); - testWidgets('no bucket for descendants when scope is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no bucket for descendants when scope is null', (WidgetTester tester) async { final Key scopeKey = GlobalKey(); await tester.pumpWidget( @@ -266,6 +272,7 @@ void main() { // Move it under a valid scope. final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{}); await tester.pumpWidget( UnmanagedRestorationScope( @@ -293,7 +300,7 @@ void main() { expect(state.bucket, isNull); }); - testWidgets('no bucket for descendants when scope and id are null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no bucket for descendants when scope and id are null', (WidgetTester tester) async { await tester.pumpWidget( const RestorationScope( restorationId: null, @@ -304,8 +311,9 @@ void main() { expect(state.bucket, isNull); }); - testWidgets('moving scope moves its data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('moving scope moves its data', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); final Key scopeKey = GlobalKey(); diff --git a/packages/flutter/test/widgets/restoration_scopes_moving_test.dart b/packages/flutter/test/widgets/restoration_scopes_moving_test.dart index 1edea3feb8a32..0b91adc024a47 100644 --- a/packages/flutter/test/widgets/restoration_scopes_moving_test.dart +++ b/packages/flutter/test/widgets/restoration_scopes_moving_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('widget moves scopes during restore', (WidgetTester tester) async { + testWidgetsWithLeakTracking('widget moves scopes during restore', (WidgetTester tester) async { await tester.pumpWidget(const RootRestorationScope( restorationId: 'root', child: Directionality( @@ -68,7 +69,7 @@ void main() { expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).toggleCount, 0); }); - testWidgets('restoration is turned on later', (WidgetTester tester) async { + testWidgetsWithLeakTracking('restoration is turned on later', (WidgetTester tester) async { tester.binding.restorationManager.disableRestoration(); await tester.pumpWidget(const RootRestorationScope( restorationId: 'root-child', diff --git a/packages/flutter/test/widgets/rich_text_test.dart b/packages/flutter/test/widgets/rich_text_test.dart index e8c83eea5b6b9..abea136e5ca86 100644 --- a/packages/flutter/test/widgets/rich_text_test.dart +++ b/packages/flutter/test/widgets/rich_text_test.dart @@ -6,9 +6,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('RichText with recognizers without handlers does not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RichText with recognizers without handlers does not throw', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -40,7 +41,7 @@ void main() { )); }); - testWidgets('TextSpan Locale works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSpan Locale works', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -89,7 +90,7 @@ void main() { )); }); - testWidgets('TextSpan spellOut works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSpan spellOut works', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -138,7 +139,7 @@ void main() { )); }); - testWidgets('WidgetSpan calculate correct intrinsic heights', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetSpan calculate correct intrinsic heights', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -170,7 +171,7 @@ void main() { expect(tester.getSize(find.byType(IntrinsicHeight)).height, 3 * 16); }); - testWidgets('RichText implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RichText implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); RichText( text: const TextSpan(text: 'rich text'), @@ -193,12 +194,12 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description, unorderedMatches(<dynamic>[ + expect(description, unorderedMatches(<Matcher>[ contains('textAlign: center'), contains('textDirection: rtl'), contains('softWrap: no wrapping except at line break characters'), contains('overflow: ellipsis'), - contains('textScaleFactor: 1.3'), + contains('textScaler: linear (1.3x)'), contains('maxLines: 1'), contains('textWidthBasis: longestLine'), contains('text: "rich text"'), diff --git a/packages/flutter/test/widgets/root_restoration_scope_test.dart b/packages/flutter/test/widgets/root_restoration_scope_test.dart index 56333dd55665c..26db826c53ade 100644 --- a/packages/flutter/test/widgets/root_restoration_scope_test.dart +++ b/packages/flutter/test/widgets/root_restoration_scope_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'restoration.dart'; @@ -17,8 +18,13 @@ void main() { binding._restorationManager = MockRestorationManager(); }); - testWidgets('does not inject root bucket if inside scope', (WidgetTester tester) async { + tearDown(() { + binding._restorationManager.dispose(); + }); + + testWidgetsWithLeakTracking('does not inject root bucket if inside scope', (WidgetTester tester) async { final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); expect(rawData, isEmpty); @@ -47,7 +53,7 @@ void main() { expect(find.text('Hello'), findsOneWidget); }); - testWidgets('waits for root bucket', (WidgetTester tester) async { + testWidgetsWithLeakTracking('waits for root bucket', (WidgetTester tester) async { final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>(); binding.restorationManager.rootBucket = bucketCompleter.future; @@ -83,7 +89,7 @@ void main() { expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue); }); - testWidgets('no delay when root is available synchronously', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no delay when root is available synchronously', (WidgetTester tester) async { final Map<String, dynamic> rawData = <String, dynamic>{}; final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData); binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root); @@ -109,7 +115,7 @@ void main() { expect((rawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue); }); - testWidgets('does not insert root when restoration id is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not insert root when restoration id is null', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -175,9 +181,10 @@ void main() { expect(state.bucket, isNull); }); - testWidgets('injects root bucket when moved out of scope', (WidgetTester tester) async { + testWidgetsWithLeakTracking('injects root bucket when moved out of scope', (WidgetTester tester) async { final Key rootScopeKey = GlobalKey(); final MockRestorationManager manager = MockRestorationManager(); + addTearDown(manager.dispose); final Map<String, dynamic> inScopeRawData = <String, dynamic>{}; final RestorationBucket inScopeRootBucket = RestorationBucket.root(manager: manager, rawData: inScopeRawData); @@ -257,7 +264,7 @@ void main() { expect((inScopeRawData[childrenMapKey] as Map<Object?, Object?>).containsKey('root-child'), isTrue); }); - testWidgets('injects new root when old one is decommissioned', (WidgetTester tester) async { + testWidgetsWithLeakTracking('injects new root when old one is decommissioned', (WidgetTester tester) async { final Map<String, dynamic> firstRawData = <String, dynamic>{}; final RestorationBucket firstRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: firstRawData); binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(firstRoot); @@ -300,7 +307,7 @@ void main() { expect(state.bucket!.read<int>('foo'), 22); }); - testWidgets('injects null when rootBucket is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('injects null when rootBucket is null', (WidgetTester tester) async { final Completer<RestorationBucket?> completer = Completer<RestorationBucket?>(); binding.restorationManager.rootBucket = completer.future; @@ -337,7 +344,7 @@ void main() { expect(state.bucket, isNotNull); }); - testWidgets('can switch to null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can switch to null', (WidgetTester tester) async { final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null); binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root); diff --git a/packages/flutter/test/widgets/rotated_box_test.dart b/packages/flutter/test/widgets/rotated_box_test.dart index c47bfd09d5d68..9e93264aaea73 100644 --- a/packages/flutter/test/widgets/rotated_box_test.dart +++ b/packages/flutter/test/widgets/rotated_box_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Rotated box control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rotated box control test', (WidgetTester tester) async { final List<String> log = <String>[]; final Key rotatedBoxKey = UniqueKey(); diff --git a/packages/flutter/test/widgets/route_notification_messages_test.dart b/packages/flutter/test/widgets/route_notification_messages_test.dart index 940bf16fba354..c1913d7db5605 100644 --- a/packages/flutter/test/widgets/route_notification_messages_test.dart +++ b/packages/flutter/test/widgets/route_notification_messages_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class OnTapPage extends StatelessWidget { const OnTapPage({super.key, required this.id, required this.onTap}); @@ -32,7 +33,7 @@ class OnTapPage extends StatelessWidget { } void main() { - testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Push and Pop should send platform messages', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage( id: '/', @@ -107,7 +108,7 @@ void main() { ); }); - testWidgets('Navigator does not report route name by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigator does not report route name by default', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async { log.add(methodCall); @@ -141,7 +142,7 @@ void main() { expect(log, hasLength(0)); }); - testWidgets('Replace should send platform messages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Replace should send platform messages', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage( id: '/', @@ -217,7 +218,7 @@ void main() { ); }); - testWidgets('Nameless routes should send platform messages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nameless routes should send platform messages', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async { log.add(methodCall); @@ -261,7 +262,7 @@ void main() { expect(log, isEmpty); }); - testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async { log.add(methodCall); @@ -273,12 +274,14 @@ void main() { uri: Uri.parse('initial'), ), ); + addTearDown(provider.dispose); final SimpleRouterDelegate delegate = SimpleRouterDelegate( reportConfiguration: true, builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, ); + addTearDown(delegate.dispose); await tester.pumpWidget(MaterialApp.router( routeInformationProvider: provider, @@ -313,7 +316,12 @@ void main() { 'replace': false, }), ]); - }); + }, + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/134205 + notDisposedAllowList: <String, int?> {'_RestorableRouteInformation': 1}, + )); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation); @@ -338,7 +346,11 @@ class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeN required this.builder, this.onPopRoute, this.reportConfiguration = false, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } RouteInformation get routeInformation => _routeInformation; late RouteInformation _routeInformation; diff --git a/packages/flutter/test/widgets/router_restoration_test.dart b/packages/flutter/test/widgets/router_restoration_test.dart index a2568f930b718..4d429c893d1b0 100644 --- a/packages/flutter/test/widgets/router_restoration_test.dart +++ b/packages/flutter/test/widgets/router_restoration_test.dart @@ -90,6 +90,12 @@ class _TestRouteInformationParser extends RouteInformationParser<String> { } class _TestRouterDelegate extends RouterDelegate<String> with ChangeNotifier { + _TestRouterDelegate() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + final List<String> newRoutePaths = <String>[]; final List<String> restoredRoutePaths = <String>[]; @@ -128,6 +134,12 @@ class _TestRouterDelegate extends RouterDelegate<String> with ChangeNotifier { } class _TestRouteInformationProvider extends RouteInformationProvider with ChangeNotifier { + _TestRouteInformationProvider() { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } + @override RouteInformation get value => _value; RouteInformation _value = RouteInformation(uri: Uri.parse('/home')); diff --git a/packages/flutter/test/widgets/router_test.dart b/packages/flutter/test/widgets/router_test.dart index f07d59245be12..2f739778d5279 100644 --- a/packages/flutter/test/widgets/router_test.dart +++ b/packages/flutter/test/widgets/router_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async { @@ -867,6 +868,76 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ]); }); + testWidgets('PlatformRouteInformationProvider does not push new entry if query parameters are semantically the same', (WidgetTester tester) async { + final List<MethodCall> log = <MethodCall>[]; + TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.navigation, + (MethodCall methodCall) async { + log.add(methodCall); + return null; + } + ); + final RouteInformation initial = RouteInformation( + uri: Uri.parse('initial?a=ws/abcd'), + ); + final RouteInformationProvider provider = PlatformRouteInformationProvider( + initialRouteInformation: initial + ); + // Make sure engine is updated with initial route + provider.routerReportsNewRouteInformation(initial); + log.clear(); + + provider.routerReportsNewRouteInformation( + RouteInformation( + uri: Uri( + path: 'initial', + queryParameters: <String, String>{'a': 'ws/abcd'}, // This will be escaped. + ), + ), + ); + expect(provider.value.uri.toString(), 'initial?a=ws%2Fabcd'); + // should use `replace: true` + expect(log, <Object>[ + isMethodCall('selectMultiEntryHistory', arguments: null), + isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'uri': 'initial?a=ws%2Fabcd', 'state': null, 'replace': true }), + ]); + log.clear(); + + provider.routerReportsNewRouteInformation( + RouteInformation(uri: Uri.parse('initial?a=1&b=2')), + ); + log.clear(); + + // Change query parameters order + provider.routerReportsNewRouteInformation( + RouteInformation(uri: Uri.parse('initial?b=2&a=1')), + ); + // should use `replace: true` + expect(log, <Object>[ + isMethodCall('selectMultiEntryHistory', arguments: null), + isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'uri': 'initial?b=2&a=1', 'state': null, 'replace': true }), + ]); + log.clear(); + + provider.routerReportsNewRouteInformation( + RouteInformation(uri: Uri.parse('initial?a=1&a=2')), + ); + log.clear(); + + // Change query parameters order for same key + provider.routerReportsNewRouteInformation( + RouteInformation(uri: Uri.parse('initial?a=2&a=1')), + ); + // should use `replace: true` + expect(log, <Object>[ + isMethodCall('selectMultiEntryHistory', arguments: null), + isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'uri': 'initial?a=2&a=1', 'state': null, 'replace': true }), + ]); + log.clear(); + }); + testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async { final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final RouteInformationProvider provider = PlatformRouteInformationProvider( @@ -1512,6 +1583,18 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester expect(info2.location, '/abc?def=ghi&def=jkl#mno'); }); }); + + test('$PlatformRouteInformationProvider dispatches object creation in constructor', () async { + Future<void> createAndDispose() async { + PlatformRouteInformationProvider( + initialRouteInformation: RouteInformation(uri: Uri.parse('http://google.com')), + ).dispose(); + } + await expectLater( + await memoryEvents(createAndDispose, PlatformRouteInformationProvider), + areCreateAndDispose, + ); + }); } Widget buildBoilerPlate(Widget child) { @@ -1563,7 +1646,11 @@ class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeN this.builder, this.onPopRoute, this.reportConfiguration = false, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } RouteInformation? get routeInformation => _routeInformation; RouteInformation? _routeInformation; @@ -1647,7 +1734,11 @@ class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> wit class SimpleRouteInformationProvider extends RouteInformationProvider with ChangeNotifier { SimpleRouteInformationProvider({ this.onRouterReport, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } RouterReportRouterInformation? onRouterReport; @@ -1703,7 +1794,11 @@ class CompleterRouteInformationParser extends RouteInformationParser<RouteInform class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier { SimpleAsyncRouterDelegate({ required this.builder, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + } RouteInformation? get routeInformation => _routeInformation; RouteInformation? _routeInformation; diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 8d802f67e25f6..a309a51368e49 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -89,6 +90,9 @@ class TestRoute extends Route<String?> with LocalHistoryRoute<String?> { @override void dispose() { log('dispose'); + for (final OverlayEntry e in _entries) { + e.dispose(); + } _entries.clear(); routes.remove(this); super.dispose(); @@ -113,12 +117,12 @@ Future<void> runNavigatorTest( } void main() { - testWidgets('Route settings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route settings', (WidgetTester tester) async { const RouteSettings settings = RouteSettings(name: 'A'); expect(settings, hasOneLineDescription); }); - testWidgets('Route settings arguments', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route settings arguments', (WidgetTester tester) async { const RouteSettings settings = RouteSettings(name: 'A'); expect(settings.arguments, isNull); @@ -127,7 +131,7 @@ void main() { expect(settings2.arguments, same(arguments)); }); - testWidgets('Route management - push, replace, pop sequence', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route management - push, replace, pop sequence', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( @@ -214,7 +218,7 @@ void main() { results.clear(); }); - testWidgets('Route management - push, remove, pop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route management - push, remove, pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( @@ -329,7 +333,7 @@ void main() { results.clear(); }); - testWidgets('Route management - push, replace, popUntil', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route management - push, replace, popUntil', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( @@ -406,7 +410,7 @@ void main() { results.clear(); }); - testWidgets('Route localHistory - popUntil', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Route localHistory - popUntil', (WidgetTester tester) async { final TestRoute routeA = TestRoute('A'); routeA.addLocalHistoryEntry(LocalHistoryEntry( onRemove: () { routeA.log('onRemove 0'); }, @@ -543,10 +547,13 @@ void main() { }); }); - testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); + addTearDown(focusNode.dispose); + await tester.pumpWidget( Material( child: MaterialApp( @@ -573,7 +580,7 @@ void main() { }); group('PageRouteBuilder', () { - testWidgets('reverseTransitionDuration defaults to 300ms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverseTransitionDuration defaults to 300ms', (WidgetTester tester) async { // Default PageRouteBuilder reverse transition duration should be 300ms. await tester.pumpWidget( MaterialApp( @@ -624,7 +631,7 @@ void main() { expect(find.text('Open page'), findsOneWidget); }); - testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverseTransitionDuration can be customized', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( @@ -676,7 +683,7 @@ void main() { }); group('TransitionRoute', () { - testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -734,7 +741,7 @@ void main() { expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); - testWidgets('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -789,7 +796,7 @@ void main() { expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); - testWidgets('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -861,7 +868,7 @@ void main() { expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); - testWidgets('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( @@ -930,7 +937,7 @@ void main() { expect(trainHopper2.currentTrain, isNull); // Has been disposed. }); - testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async { + testWidgetsWithLeakTracking('secondary animation is triggered when pop initial route', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); late Animation<double> secondaryAnimationOfRouteOne; late Animation<double> primaryAnimationOfRouteTwo; @@ -968,7 +975,7 @@ void main() { expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); }); - testWidgets('showGeneralDialog handles transparent barrier color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog handles transparent barrier color', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Builder( builder: (BuildContext context) { @@ -1002,7 +1009,7 @@ void main() { expect(find.byType(ModalBarrier), findsNWidgets(1)); }); - testWidgets('showGeneralDialog adds non-dismissible barrier when barrierDismissible is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog adds non-dismissible barrier when barrierDismissible is false', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Builder( builder: (BuildContext context) { @@ -1037,7 +1044,7 @@ void main() { expect(find.byType(ModalBarrier), findsNWidgets(1)); }); - testWidgets('showGeneralDialog uses null as a barrierLabel by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog uses null as a barrierLabel by default', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Builder( builder: (BuildContext context) { @@ -1072,7 +1079,7 @@ void main() { expect(find.byType(ModalBarrier), findsNWidgets(1)); }); - testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog uses root navigator by default', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -1108,7 +1115,7 @@ void main() { expect(nestedObserver.dialogCount, 0); }); - testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); @@ -1145,7 +1152,7 @@ void main() { expect(nestedObserver.dialogCount, 1); }); - testWidgets('showGeneralDialog default argument values', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showGeneralDialog default argument values', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); await tester.pumpWidget(MaterialApp( @@ -1181,7 +1188,7 @@ void main() { }); group('showGeneralDialog avoids overlapping display features', () { - testWidgets('positioning with anchorPoint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1219,7 +1226,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning with Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning with Directionality', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1259,7 +1266,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('positioning by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positioning by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { @@ -1297,7 +1304,7 @@ void main() { }); }); - testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); // Default MaterialPageRoute transition duration should be 300ms. @@ -1349,7 +1356,7 @@ void main() { expect(find.byKey(containerKey), findsNothing); }); - testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('reverseTransitionDuration can be customized', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { @@ -1401,7 +1408,7 @@ void main() { expect(find.byKey(containerKey), findsNothing); }); - testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async { + testWidgetsWithLeakTracking('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -1469,7 +1476,7 @@ void main() { }); group('ModalRoute', () { - testWidgets('default barrierCurve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default barrierCurve', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Builder( @@ -1531,7 +1538,7 @@ void main() { expect(modalBarrierAnimation.value, Colors.black); }); - testWidgets('custom barrierCurve', (WidgetTester tester) async { + testWidgetsWithLeakTracking('custom barrierCurve', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Builder( @@ -1594,7 +1601,7 @@ void main() { expect(modalBarrierAnimation.value, Colors.black); }); - testWidgets('white barrierColor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('white barrierColor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Builder( @@ -1657,7 +1664,7 @@ void main() { expect(modalBarrierAnimation.value, Colors.white); }); - testWidgets('modal route semantics order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('modal route semantics order', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/46625. final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( @@ -1730,7 +1737,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS})); - testWidgets('focus traverse correct when pop multiple page simultaneously', (WidgetTester tester) async { + testWidgetsWithLeakTracking('focus traverse correct when pop multiple page simultaneously', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( @@ -1776,7 +1783,7 @@ void main() { expect(focusNodeOnPageOne.hasFocus, isTrue); }); - testWidgets('focus traversal is correct when popping multiple pages simultaneously - with focused children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('focus traversal is correct when popping multiple pages simultaneously - with focused children', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( @@ -1828,7 +1835,7 @@ void main() { expect(focusNodeOnPageOne.hasFocus, isTrue); }); - testWidgets('child with local history can be disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child with local history can be disposed', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/52478 await tester.pumpWidget(const MaterialApp( home: WidgetWithLocalHistory(), @@ -1849,7 +1856,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('child with no local history can be disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child with no local history can be disposed', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: WidgetWithNoLocalHistory(), )); @@ -1869,7 +1876,7 @@ void main() { }); }); - testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, @@ -1891,7 +1898,7 @@ void main() { expect(find.text('dialog1'), findsNothing); }); - testWidgets('can not be dismissed with escape keyboard shortcut if barrier not dismissible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can not be dismissed with escape keyboard shortcut if barrier not dismissible', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, @@ -1914,7 +1921,7 @@ void main() { expect(find.text('dialog1'), findsOneWidget); }); - testWidgets('ModalRoute.of works for void routes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ModalRoute.of works for void routes', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, @@ -1936,7 +1943,7 @@ void main() { expect(parentRoute, isA<MaterialPageRoute<void>>()); }); - testWidgets('RawDialogRoute is state restorable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawDialogRoute is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( restorationScopeId: 'app', @@ -2078,7 +2085,7 @@ class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> { if (_barrierCurve == null) { return super.barrierCurve; } - return _barrierCurve!; + return _barrierCurve; } final Curve? _barrierCurve; diff --git a/packages/flutter/test/widgets/row_test.dart b/packages/flutter/test/widgets/row_test.dart index 884dfbf288b37..da7ee24981e8b 100644 --- a/packages/flutter/test/widgets/row_test.dart +++ b/packages/flutter/test/widgets/row_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class OrderPainter extends CustomPainter { const OrderPainter(this.index); @@ -28,7 +29,7 @@ Widget log(int index) => CustomPaint(painter: OrderPainter(index)); void main() { // NO DIRECTION - testWidgets('Row with one Flexible child - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with one Flexible child - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -59,7 +60,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with default main axis parameters - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with default main axis parameters - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -90,7 +91,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with MainAxisAlignment.center - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.center - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -120,7 +121,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with MainAxisAlignment.end - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.end - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -152,7 +153,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with MainAxisAlignment.spaceBetween - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceBetween - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -184,7 +185,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with MainAxisAlignment.spaceAround - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceAround - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -218,7 +219,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row with MainAxisAlignment.spaceEvenly - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceEvenly - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -250,7 +251,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row and MainAxisSize.min - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row and MainAxisSize.min - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('rowKey'); const Key child0Key = Key('child0'); @@ -280,7 +281,7 @@ void main() { expect(OrderPainter.log, <int>[]); }); - testWidgets('Row MainAxisSize.min layout at zero size - no textDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row MainAxisSize.min layout at zero size - no textDirection', (WidgetTester tester) async { OrderPainter.log.clear(); const Key childKey = Key('childKey'); @@ -308,7 +309,7 @@ void main() { // LTR - testWidgets('Row with one Flexible child - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with one Flexible child - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -358,7 +359,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with default main axis parameters - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with default main axis parameters - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -408,7 +409,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.center - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.center - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -450,7 +451,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2]); }); - testWidgets('Row with MainAxisAlignment.end - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.end - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -500,7 +501,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceBetween - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceBetween - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -550,7 +551,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceAround - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceAround - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -608,7 +609,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3, 4]); }); - testWidgets('Row with MainAxisAlignment.spaceEvenly - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceEvenly - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -658,7 +659,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row and MainAxisSize.min - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row and MainAxisSize.min - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('rowKey'); const Key child0Key = Key('child0'); @@ -700,7 +701,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2]); }); - testWidgets('Row MainAxisSize.min layout at zero size - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row MainAxisSize.min layout at zero size - LTR', (WidgetTester tester) async { OrderPainter.log.clear(); const Key childKey = Key('childKey'); @@ -729,7 +730,7 @@ void main() { // RTL - testWidgets('Row with one Flexible child - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with one Flexible child - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -779,7 +780,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with default main axis parameters - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with default main axis parameters - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -829,7 +830,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.center - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.center - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -871,7 +872,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2]); }); - testWidgets('Row with MainAxisAlignment.end - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.end - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -921,7 +922,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceBetween - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceBetween - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -971,7 +972,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row with MainAxisAlignment.spaceAround - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceAround - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -1029,7 +1030,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3, 4]); }); - testWidgets('Row with MainAxisAlignment.spaceEvenly - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row with MainAxisAlignment.spaceEvenly - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('row'); const Key child0Key = Key('child0'); @@ -1079,7 +1080,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2, 3]); }); - testWidgets('Row and MainAxisSize.min - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row and MainAxisSize.min - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key rowKey = Key('rowKey'); const Key child0Key = Key('child0'); @@ -1121,7 +1122,7 @@ void main() { expect(OrderPainter.log, <int>[1, 2]); }); - testWidgets('Row MainAxisSize.min layout at zero size - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Row MainAxisSize.min layout at zero size - RTL', (WidgetTester tester) async { OrderPainter.log.clear(); const Key childKey = Key('childKey'); diff --git a/packages/flutter/test/widgets/rtl_test.dart b/packages/flutter/test/widgets/rtl_test.dart index 077be41cc2644..810adc2d2ba59 100644 --- a/packages/flutter/test/widgets/rtl_test.dart +++ b/packages/flutter/test/widgets/rtl_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Padding RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding RTL', (WidgetTester tester) async { const Widget child = Padding( padding: EdgeInsetsDirectional.only(start: 10.0), child: Placeholder(), @@ -43,7 +44,7 @@ void main() { ); }); - testWidgets('Container padding/margin RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Container padding/margin RTL', (WidgetTester tester) async { final Widget child = Container( padding: const EdgeInsetsDirectional.only(start: 6.0), margin: const EdgeInsetsDirectional.only(end: 20.0, start: 4.0), @@ -63,7 +64,7 @@ void main() { expect(tester.getTopRight(find.byType(Placeholder)), const Offset(790.0, 0.0)); }); - testWidgets('Container padding/margin mixed RTL/absolute', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Container padding/margin mixed RTL/absolute', (WidgetTester tester) async { final Widget child = Container( padding: const EdgeInsets.only(left: 6.0), margin: const EdgeInsetsDirectional.only(end: 20.0, start: 4.0), @@ -83,7 +84,7 @@ void main() { expect(tester.getTopRight(find.byType(Placeholder)), const Offset(796.0, 0.0)); }); - testWidgets('EdgeInsetsDirectional without Directionality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('EdgeInsetsDirectional without Directionality', (WidgetTester tester) async { await tester.pumpWidget(const Padding(padding: EdgeInsetsDirectional.zero)); expect(tester.takeException(), isAssertionError); }); diff --git a/packages/flutter/test/widgets/run_app_test.dart b/packages/flutter/test/widgets/run_app_test.dart index 49c035999a1e4..d48a75f7991c9 100644 --- a/packages/flutter/test/widgets/run_app_test.dart +++ b/packages/flutter/test/widgets/run_app_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('runApp inside onPressed does not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('runApp inside onPressed does not throw', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/safe_area_test.dart b/packages/flutter/test/widgets/safe_area_test.dart index 2960351456eb0..afe4c41b29001 100644 --- a/packages/flutter/test/widgets/safe_area_test.dart +++ b/packages/flutter/test/widgets/safe_area_test.dart @@ -5,10 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('SafeArea', () { - testWidgets('SafeArea - basic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea - basic', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(padding: EdgeInsets.all(20.0)), @@ -22,7 +23,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); }); - testWidgets('SafeArea - with minimums', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea - with minimums', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(padding: EdgeInsets.all(20.0)), @@ -37,7 +38,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 570.0)); }); - testWidgets('SafeArea - nested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea - nested', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(padding: EdgeInsets.all(20.0)), @@ -54,7 +55,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); }); - testWidgets('SafeArea - changing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea - changing', (WidgetTester tester) async { const Widget child = SafeArea( bottom: false, child: SafeArea( @@ -85,7 +86,7 @@ void main() { expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); - testWidgets('SafeArea - properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea - properties', (WidgetTester tester) async { final SafeArea child = SafeArea( right: false, bottom: false, @@ -112,7 +113,7 @@ void main() { ); } - testWidgets('SafeArea alone.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea alone.', (WidgetTester tester) async { final Widget child = boilerplate(const SafeArea( maintainBottomViewPadding: true, child: Column( @@ -147,7 +148,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('SafeArea alone - partial ViewInsets consume Padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea alone - partial ViewInsets consume Padding', (WidgetTester tester) async { final Widget child = boilerplate(const SafeArea( maintainBottomViewPadding: true, child: Column( @@ -180,7 +181,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('SafeArea with nested Scaffold', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea with nested Scaffold', (WidgetTester tester) async { final Widget child = boilerplate(const SafeArea( maintainBottomViewPadding: true, child: Scaffold( @@ -218,7 +219,7 @@ void main() { expect(initialPoint, finalPoint); }); - testWidgets('SafeArea with nested Scaffold - partial ViewInsets consume Padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SafeArea with nested Scaffold - partial ViewInsets consume Padding', (WidgetTester tester) async { final Widget child = boilerplate(const SafeArea( maintainBottomViewPadding: true, child: Scaffold( @@ -258,12 +259,15 @@ void main() { group('SliverSafeArea', () { Widget buildWidget(EdgeInsets mediaPadding, Widget sliver) { + late final ViewportOffset offset; + addTearDown(() => offset.dispose()); + return MediaQuery( data: MediaQueryData(padding: mediaPadding), child: Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(0.0), + offset: offset = ViewportOffset.fixed(0.0), slivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(width: 800.0, height: 100.0, child: Text('before'))), sliver, @@ -285,7 +289,7 @@ void main() { expect(testAnswers, equals(expectedRects)); } - testWidgets('SliverSafeArea - basic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverSafeArea - basic', (WidgetTester tester) async { await tester.pumpWidget( buildWidget( const EdgeInsets.all(20.0), @@ -302,7 +306,7 @@ void main() { ]); }); - testWidgets('SliverSafeArea - basic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverSafeArea - basic', (WidgetTester tester) async { await tester.pumpWidget( buildWidget( const EdgeInsets.all(20.0), @@ -320,7 +324,7 @@ void main() { ]); }); - testWidgets('SliverSafeArea - nested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverSafeArea - nested', (WidgetTester tester) async { await tester.pumpWidget( buildWidget( const EdgeInsets.all(20.0), @@ -340,7 +344,7 @@ void main() { ]); }); - testWidgets('SliverSafeArea - changing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverSafeArea - changing', (WidgetTester tester) async { const Widget sliver = SliverSafeArea( bottom: false, sliver: SliverSafeArea( @@ -379,7 +383,7 @@ void main() { }); }); - testWidgets('SliverSafeArea - properties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverSafeArea - properties', (WidgetTester tester) async { const SliverSafeArea child = SliverSafeArea( right: false, bottom: false, diff --git a/packages/flutter/test/widgets/scroll_activity_test.dart b/packages/flutter/test/widgets/scroll_activity_test.dart index f6dd3235d0f9e..824c12105c809 100644 --- a/packages/flutter/test/widgets/scroll_activity_test.dart +++ b/packages/flutter/test/widgets/scroll_activity_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; List<Widget> children(int n) { return List<Widget>.generate(n, (int i) { @@ -14,8 +15,9 @@ List<Widget> children(int n) { } void main() { - testWidgets('Scrolling with list view changes, leaving the overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling with list view changes, leaving the overscroll', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(30)))); final double thirty = controller.position.maxScrollExtent; controller.jumpTo(thirty); @@ -28,8 +30,9 @@ void main() { expect(controller.position.pixels, thirty + 100.0); // and ends up at the end }); - testWidgets('Scrolling with list view changes, remaining overscrolled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolling with list view changes, remaining overscrolled', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(30)))); final double thirty = controller.position.maxScrollExtent; controller.jumpTo(thirty); @@ -42,7 +45,7 @@ void main() { expect(controller.position.pixels, thirty + 100.0); // and ends up at the end }); - testWidgets('Ability to keep a PageView at the end manually (issue 62209)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ability to keep a PageView at the end manually (issue 62209)', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: PageView62209())); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 100'), findsNothing); @@ -129,8 +132,9 @@ void main() { expect(find.text('Page 9'), findsOneWidget); }); - testWidgets('Pointer is not ignored during trackpad scrolling.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pointer is not ignored during trackpad scrolling.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); int? lastTapped; int? lastHovered; await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart index 51fa9d85ef70e..8dcba52ef928e 100644 --- a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart +++ b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui show Image; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../painting/image_test_utils.dart'; @@ -17,6 +18,10 @@ void main() { testImage = await createTestImage(width: 10, height: 10); }); + tearDownAll(() { + testImage.dispose(); + }); + tearDown(() { imageCache.clear(); }); @@ -29,7 +34,7 @@ void main() { return Scrollable.of(find.byType(TestWidget).evaluate().first).position; } - testWidgets('ScrollAwareImageProvider does not delay if widget is not in scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider does not delay if widget is not in scrollable', (WidgetTester tester) async { final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>(); await tester.pumpWidget(TestWidget(key)); @@ -56,7 +61,7 @@ void main() { expect(imageCache.currentSize, 1); }); - testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is not scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider does not delay if in scrollable that is not scrolling', (WidgetTester tester) async { final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -92,9 +97,10 @@ void main() { expect(findPhysics<RecordingPhysics>(tester).velocities, <double>[0]); }); - testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is scrolling slowly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider does not delay if in scrollable that is scrolling slowly', (WidgetTester tester) async { final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[]; final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -149,9 +155,10 @@ void main() { expect(imageCache.currentSize, 1); }); - testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider delays if in scrollable that is scrolling fast', (WidgetTester tester) async { final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[]; final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -216,9 +223,10 @@ void main() { expect(imageCache.currentSize, 1); }); - testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast and fizzles if disposed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider delays if in scrollable that is scrolling fast and fizzles if disposed', (WidgetTester tester) async { final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[]; final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -285,9 +293,10 @@ void main() { expect(imageCache.currentSize, 0); }); - testWidgets('ScrollAwareImageProvider resolves from ImageCache and does not set completer twice', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider resolves from ImageCache and does not set completer twice', (WidgetTester tester) async { final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( @@ -333,12 +342,13 @@ void main() { expect(stream.completer, null); }); - testWidgets('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async { final int oldSize = imageCache.maximumSize; imageCache.maximumSize = 1; final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>(); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( diff --git a/packages/flutter/test/widgets/scroll_behavior_test.dart b/packages/flutter/test/widgets/scroll_behavior_test.dart index 287a1a82d0898..087f80012ac5a 100644 --- a/packages/flutter/test/widgets/scroll_behavior_test.dart +++ b/packages/flutter/test/widgets/scroll_behavior_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; late GestureVelocityTrackerBuilder lastCreatedBuilder; class TestScrollBehavior extends ScrollBehavior { @@ -33,7 +34,7 @@ class TestScrollBehavior extends ScrollBehavior { } void main() { - testWidgets('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = ScrollBehavior(); late BuildContext capturedContext; @@ -75,14 +76,14 @@ void main() { }, variant: TargetPlatformVariant.all()); // Regression test for https://github.com/flutter/flutter/issues/89681 - testWidgets('_WrappedScrollBehavior shouldNotify test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('_WrappedScrollBehavior shouldNotify test', (WidgetTester tester) async { final ScrollBehavior behavior1 = const ScrollBehavior().copyWith(); final ScrollBehavior behavior2 = const ScrollBehavior().copyWith(); expect(behavior1.shouldNotify(behavior2), false); }); - testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inherited ScrollConfiguration changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'scrollable'); TestScrollBehavior? behavior; late ScrollPositionWithSingleContext position; @@ -131,7 +132,7 @@ void main() { expect(metrics.viewportDimension, equals(600.0)); }); - testWidgets('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -154,34 +155,8 @@ void main() { expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('ScrollBehavior stretch android overscroll indicator', (WidgetTester tester) async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(size: Size(800, 600)), - child: ScrollConfiguration( - behavior: const ScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch), - child: ListView( - children: const <Widget>[ - SizedBox( - height: 1000.0, - width: 1000.0, - child: Text('Test'), - ), - ], - ), - ), - ), - ), - ); - - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - group('ScrollBehavior configuration is maintained over multiple copies', () { - testWidgets('dragDevices', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dragDevices', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); expect(defaultBehavior.dragDevices, <PointerDeviceKind>{ @@ -203,23 +178,7 @@ void main() { expect(twiceCopiedBehavior.dragDevices, PointerDeviceKind.values.toSet()); }); - testWidgets('androidOverscrollIndicator', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/91673 - const ScrollBehavior defaultBehavior = ScrollBehavior(); - expect(defaultBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.glow); - - // Use copyWith to modify androidOverscrollIndicator - final ScrollBehavior onceCopiedBehavior = defaultBehavior.copyWith( - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, - ); - expect(onceCopiedBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.stretch); - - // Copy again. The previously modified value should carry over. - final ScrollBehavior twiceCopiedBehavior = onceCopiedBehavior.copyWith(); - expect(twiceCopiedBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.stretch); - }); - - testWidgets('physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('physics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 late ScrollPhysics defaultPhysics; late ScrollPhysics onceCopiedPhysics; @@ -261,7 +220,7 @@ void main() { expect(twiceCopiedPhysics, const BouncingScrollPhysics()); }); - testWidgets('platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('platform', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 late TargetPlatform defaultPlatform; late TargetPlatform onceCopiedPlatform; @@ -318,7 +277,7 @@ void main() { ); } - testWidgets('scrollbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrollbar', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); await tester.pumpWidget(wrap(defaultBehavior)); @@ -338,7 +297,7 @@ void main() { // For default scrollbars }, variant: TargetPlatformVariant.desktop()); - testWidgets('overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('overscroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); await tester.pumpWidget(wrap(defaultBehavior)); diff --git a/packages/flutter/test/widgets/scroll_controller_test.dart b/packages/flutter/test/widgets/scroll_controller_test.dart index 96eeea898ecd8..3b1682ac524b7 100644 --- a/packages/flutter/test/widgets/scroll_controller_test.dart +++ b/packages/flutter/test/widgets/scroll_controller_test.dart @@ -6,12 +6,14 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'states.dart'; void main() { - testWidgets('ScrollController control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollController control test', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -76,6 +78,7 @@ void main() { expect(realOffset(), equals(controller.offset)); final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget( Directionality( @@ -131,10 +134,11 @@ void main() { expect(realOffset(), equals(controller2.offset)); }); - testWidgets('ScrollController control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollController control test', (WidgetTester tester) async { final ScrollController controller = ScrollController( initialScrollOffset: 209.0, ); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -176,8 +180,9 @@ void main() { expect(realOffset(), equals(controller.offset)); }); - testWidgets('DrivenScrollActivity ending after dispose', (WidgetTester tester) async { + testWidgetsWithLeakTracking('DrivenScrollActivity ending after dispose', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -197,14 +202,17 @@ void main() { await tester.pumpWidget(Container(), const Duration(seconds: 2)); }); - testWidgets('Read operations on ScrollControllers with no positions fail', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Read operations on ScrollControllers with no positions fail', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); expect(() => controller.offset, throwsAssertionError); expect(() => controller.position, throwsAssertionError); }); - testWidgets('Read operations on ScrollControllers with more than one position fail', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Read operations on ScrollControllers with more than one position fail', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -237,14 +245,17 @@ void main() { expect(() => controller.position, throwsAssertionError); }); - testWidgets('Write operations on ScrollControllers with no positions fail', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Write operations on ScrollControllers with no positions fail', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); expect(() => controller.animateTo(1.0, duration: const Duration(seconds: 1), curve: Curves.linear), throwsAssertionError); expect(() => controller.jumpTo(1.0), throwsAssertionError); }); - testWidgets('Write operations on ScrollControllers with more than one position do not throw', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Write operations on ScrollControllers with more than one position do not throw', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -278,7 +289,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scroll controllers notify when the position changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll controllers notify when the position changes', (WidgetTester tester) async { final ScrollController controller = ScrollController(); final List<double> log = <double>[]; @@ -312,7 +323,7 @@ void main() { expect(log, isEmpty); }); - testWidgets('keepScrollOffset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keepScrollOffset', (WidgetTester tester) async { final PageStorageBucket bucket = PageStorageBucket(); Widget buildFrame(ScrollController controller) { @@ -340,6 +351,7 @@ void main() { // The initialScrollOffset is used in this case, because there's no saved // scroll offset. ScrollController controller = ScrollController(initialScrollOffset: 200.0); + addTearDown(controller.dispose); await tester.pumpWidget(buildFrame(controller)); expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 2')), Offset.zero); @@ -350,6 +362,7 @@ void main() { // The initialScrollOffset isn't used in this case, because the scrolloffset // can be restored. controller = ScrollController(initialScrollOffset: 25.0); + addTearDown(controller.dispose); await tester.pumpWidget(buildFrame(controller)); expect(controller.offset, 2000.0); expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 20')), Offset.zero); @@ -359,13 +372,14 @@ void main() { // the initialScrollOffset is used. controller = ScrollController(keepScrollOffset: false, initialScrollOffset: 100.0); + addTearDown(controller.dispose); await tester.pumpWidget(buildFrame(controller)); expect(controller.offset, 100.0); expect(tester.getTopLeft(find.widgetWithText(SizedBox, 'Item 1')), Offset.zero); }); - testWidgets('isScrollingNotifier works with pointer scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isScrollingNotifier works with pointer scroll', (WidgetTester tester) async { Widget buildFrame(ScrollController controller) { return Directionality( textDirection: TextDirection.ltr, @@ -380,6 +394,7 @@ void main() { bool isScrolling = false; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); controller.addListener((){ isScrolling = controller.position.isScrollingNotifier.value; }); @@ -393,4 +408,11 @@ void main() { // should have been true expect(isScrolling, isTrue); }); + + test('$ScrollController dispatches object creation in constructor', () async { + await expectLater( + await memoryEvents(() => ScrollController().dispose(), ScrollController), + areCreateAndDispose, + ); + }); } diff --git a/packages/flutter/test/widgets/scroll_events_test.dart b/packages/flutter/test/widgets/scroll_events_test.dart index 397eb5b9524a8..f7087418f63bc 100644 --- a/packages/flutter/test/widgets/scroll_events_test.dart +++ b/packages/flutter/test/widgets/scroll_events_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Widget _buildScroller({ required List<String> log }) { return NotificationListener<ScrollNotification>( @@ -38,7 +39,7 @@ void main() { scrollable.position.jumpTo(newScrollOffset); } - testWidgets('Scroll event drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll event drag', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -57,7 +58,7 @@ void main() { expect(log, equals(<String>['scroll-start', 'scroll-update', 'scroll-end'])); }); - testWidgets('Scroll animateTo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll animateTo', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -74,7 +75,7 @@ void main() { expect(completer.isCompleted, isTrue); }); - testWidgets('Scroll jumpTo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll jumpTo', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -85,7 +86,7 @@ void main() { expect(log, equals(<String>['scroll-start', 'scroll-update', 'scroll-end'])); }); - testWidgets('Scroll jumpTo during animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll jumpTo during animation', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -110,7 +111,7 @@ void main() { expect(completer.isCompleted, isTrue); }); - testWidgets('Scroll scrollTo during animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll scrollTo during animation', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -134,7 +135,7 @@ void main() { expect(completer.isCompleted, isTrue); }); - testWidgets('fling, fling generates two start/end pairs', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling, fling generates two start/end pairs', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -157,7 +158,7 @@ void main() { expect(log, equals(<String>['scroll-start', 'scroll-end', 'scroll-start', 'scroll-end'])); }); - testWidgets('fling, pause, fling generates two start/end pairs', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling, pause, fling generates two start/end pairs', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); @@ -176,7 +177,7 @@ void main() { expect(log, equals(<String>['scroll-start', 'scroll-end', 'scroll-start', 'scroll-end'])); }); - testWidgets('fling up ends', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling up ends', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(_buildScroller(log: log)); diff --git a/packages/flutter/test/widgets/scroll_interaction_test.dart b/packages/flutter/test/widgets/scroll_interaction_test.dart index f0dd0c3b0a916..06a1735d06e36 100644 --- a/packages/flutter/test/widgets/scroll_interaction_test.dart +++ b/packages/flutter/test/widgets/scroll_interaction_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Scroll flings twice in a row does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll flings twice in a row does not crash', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/scroll_notification_test.dart b/packages/flutter/test/widgets/scroll_notification_test.dart index 0493f8ab25231..d1b1c7f6d71d8 100644 --- a/packages/flutter/test/widgets/scroll_notification_test.dart +++ b/packages/flutter/test/widgets/scroll_notification_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('ScrollMetricsNotification test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollMetricsNotification test', (WidgetTester tester) async { final List<Notification> events = <Notification>[]; Widget buildFrame(double height) { return NotificationListener<Notification>( @@ -62,7 +63,7 @@ void main() { expect(events.length, 0); }); - testWidgets('Scroll notification basics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll notification basics', (WidgetTester tester) async { late ScrollNotification notification; await tester.pumpWidget(NotificationListener<ScrollNotification>( @@ -103,7 +104,7 @@ void main() { expect(end.dragDetails!.velocity, equals(Velocity.zero)); }); - testWidgets('Scroll notification depth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll notification depth', (WidgetTester tester) async { final List<Type> depth0Types = <Type>[]; final List<Type> depth1Types = <Type>[]; final List<int> depth0Values = <int>[]; @@ -158,7 +159,7 @@ void main() { expect(depth1Values, equals(<int>[1, 1, 1, 1, 1])); }); - testWidgets('ScrollNotifications bubble past Scaffold Material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollNotifications bubble past Scaffold Material', (WidgetTester tester) async { final List<Type> notificationTypes = <Type>[]; await tester.pumpWidget( @@ -206,7 +207,7 @@ void main() { expect(notificationTypes, equals(types)); }); - testWidgets('ScrollNotificationObserver', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollNotificationObserver', (WidgetTester tester) async { late ScrollNotificationObserverState observer; ScrollNotification? notification; diff --git a/packages/flutter/test/widgets/scroll_physics_test.dart b/packages/flutter/test/widgets/scroll_physics_test.dart index 6f209bc12bf7e..6185ff15e7111 100644 --- a/packages/flutter/test/widgets/scroll_physics_test.dart +++ b/packages/flutter/test/widgets/scroll_physics_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestScrollPhysics extends ScrollPhysics { const TestScrollPhysics({ @@ -339,7 +340,7 @@ FlutterError } }); - testWidgets('PageScrollPhysics work with NestedScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageScrollPhysics work with NestedScrollView', (WidgetTester tester) async { // Regression test for: https://github.com/flutter/flutter/issues/47850 await tester.pumpWidget(Material( child: Directionality( diff --git a/packages/flutter/test/widgets/scroll_position_test.dart b/packages/flutter/test/widgets/scroll_position_test.dart index 4ada8e22f3fee..8d6f0648329dd 100644 --- a/packages/flutter/test/widgets/scroll_position_test.dart +++ b/packages/flutter/test/widgets/scroll_position_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; ScrollController _controller = ScrollController( initialScrollOffset: 110.0, @@ -140,7 +141,7 @@ Future<void> performTest(WidgetTester tester, bool maintainState) async { } void main() { - testWidgets("ScrollPosition jumpTo() doesn't call notifyListeners twice", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ScrollPosition jumpTo() doesn't call notifyListeners twice", (WidgetTester tester) async { int count = 0; await tester.pumpWidget(MaterialApp( home: ListView.builder( @@ -159,15 +160,22 @@ void main() { expect(count, 1); }); - testWidgets('whether we remember our scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('whether we remember our scroll position', (WidgetTester tester) async { await performTest(tester, true); await performTest(tester, false); }); - testWidgets('scroll alignment is honored by ensureVisible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scroll alignment is honored by ensureVisible', (WidgetTester tester) async { final List<int> items = List<int>.generate(11, (int index) => index).toList(); final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); + addTearDown(() { + for (final FocusNode node in nodes) { + node.dispose(); + } + }); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: ListView( @@ -226,7 +234,7 @@ void main() { expect(controller.position.pixels, equals(0.0)); }); - testWidgets('jumpTo recommends deferred loading', (WidgetTester tester) async { + testWidgetsWithLeakTracking('jumpTo recommends deferred loading', (WidgetTester tester) async { int loadedWithDeferral = 0; int buildCount = 0; const double height = 500; diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart index be22bf40741c6..a957e71560986 100644 --- a/packages/flutter/test/widgets/scroll_view_test.dart +++ b/packages/flutter/test/widgets/scroll_view_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'states.dart'; @@ -68,7 +69,7 @@ Widget primaryScrollControllerBoilerplate({ required Widget child, required Scro } void main() { - testWidgets('ListView control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -110,8 +111,13 @@ void main() { log.clear(); }); - testWidgets('ListView dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView( @@ -143,7 +149,7 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView.builder supports null items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder supports null items', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -163,7 +169,7 @@ void main() { expect(find.text('item'), findsNWidgets(5)); }); - testWidgets('ListView.builder supports null items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder supports null items', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( itemCount: 42, @@ -180,11 +186,14 @@ void main() { expect(find.text('item'), findsNWidgets(5)); }); - testWidgets('PageView supports null items in itemBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView supports null items in itemBuilder', (WidgetTester tester) async { + final PageController controller = PageController(viewportFraction: 1 / 5); + addTearDown(controller.dispose); + await tester.pumpWidget(textFieldBoilerplate( child: PageView.builder( itemCount: 5, - controller: PageController(viewportFraction: 1/5), + controller: controller, itemBuilder: (BuildContext context, int index) { if (index == 2) { return null; @@ -198,7 +207,7 @@ void main() { expect(find.text('item'), findsNWidgets(2)); }); - testWidgets('ListView.separated supports null items in itemBuilder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated supports null items in itemBuilder', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( itemCount: 42, @@ -219,8 +228,13 @@ void main() { expect(find.text('separator'), findsNWidgets(5)); }); - testWidgets('ListView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( @@ -253,8 +267,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('ListView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.custom( @@ -289,8 +308,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('ListView.separated dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( @@ -324,8 +348,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView( @@ -358,8 +387,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( @@ -393,8 +427,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView.count dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.count( @@ -427,8 +466,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView.extent dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.extent dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.extent( @@ -461,8 +505,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('GridView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.custom( @@ -498,8 +547,13 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('ListView dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView( @@ -530,8 +584,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('ListView.builder dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( @@ -563,8 +622,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('ListView.custom dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.custom dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.custom( @@ -598,8 +662,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('ListView.separated dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( @@ -632,8 +701,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('GridView dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView( @@ -665,8 +739,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('GridView.builder dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( @@ -699,8 +778,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('GridView.count dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.count( @@ -732,8 +816,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('GridView.extent dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.extent dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.extent( @@ -765,8 +854,13 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('GridView.custom dismiss keyboard manual test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.custom dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.custom( @@ -801,7 +895,7 @@ void main() { expect(textField.focusNode!.hasFocus, isTrue); }); - testWidgets('ListView restart ballistic activity out of range', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView restart ballistic activity out of range', (WidgetTester tester) async { Widget buildListView(int n) { return Directionality( textDirection: TextDirection.ltr, @@ -831,7 +925,7 @@ void main() { expect(viewport.offset.pixels, equals(2400.0)); }); - testWidgets('CustomScrollView control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -879,8 +973,13 @@ void main() { log.clear(); }); - testWidgets('CustomScrollView dismiss keyboard onDrag test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); await tester.pumpWidget(textFieldBoilerplate( child: CustomScrollView( @@ -918,9 +1017,10 @@ void main() { expect(textField.focusNode!.hasFocus, isFalse); }); - testWidgets('Can jumpTo during drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can jumpTo during drag', (WidgetTester tester) async { final List<Type> log = <Type>[]; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -977,23 +1077,26 @@ void main() { }); test('PrimaryScrollController.automaticallyInheritOnPlatforms defaults to all mobile platforms', (){ - final PrimaryScrollController primaryScrollController = PrimaryScrollController( - controller: ScrollController(), - child: const SizedBox(), - ); - expect( - primaryScrollController.automaticallyInheritForPlatforms, - TargetPlatformVariant.mobile().values, - ); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + final PrimaryScrollController primaryScrollController = PrimaryScrollController( + controller: controller, + child: const SizedBox(), + ); + expect( + primaryScrollController.automaticallyInheritForPlatforms, + TargetPlatformVariant.mobile().values, + ); }); - testWidgets('Vertical CustomScrollViews are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical CustomScrollViews are not primary by default', (WidgetTester tester) async { const CustomScrollView view = CustomScrollView(); expect(view.primary, isNull); }); - testWidgets('Vertical CustomScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical CustomScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const CustomScrollView(), controller: controller, @@ -1001,8 +1104,9 @@ void main() { expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); - testWidgets("Vertical CustomScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Vertical CustomScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const CustomScrollView(), controller: controller, @@ -1010,13 +1114,14 @@ void main() { expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Vertical ListViews are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical ListViews are not primary by default', (WidgetTester tester) async { final ListView view = ListView(); expect(view.primary, isNull); }); - testWidgets('Vertical ListViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical ListViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView(), controller: controller, @@ -1024,8 +1129,9 @@ void main() { expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); - testWidgets("Vertical ListViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Vertical ListViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView(), controller: controller, @@ -1033,13 +1139,14 @@ void main() { expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Vertical GridViews are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical GridViews are not primary by default', (WidgetTester tester) async { final GridView view = GridView.count(crossAxisCount: 1); expect(view.primary, isNull); }); - testWidgets('Vertical GridViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical GridViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count(crossAxisCount: 1), controller: controller, @@ -1047,8 +1154,9 @@ void main() { expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); - testWidgets("Vertical GridViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Vertical GridViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count(crossAxisCount: 1), controller: controller, @@ -1056,79 +1164,98 @@ void main() { expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Horizontal CustomScrollViews are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('Horizontal CustomScrollViews are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: CustomScrollView( scrollDirection: Axis.horizontal, - controller: ScrollController(), + controller: controller2, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('Horizontal ListViews are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('Horizontal ListViews are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView( scrollDirection: Axis.horizontal, - controller: ScrollController(), + controller: controller2, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('Horizontal GridViews are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('Horizontal GridViews are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count( scrollDirection: Axis.horizontal, - controller: ScrollController(), + controller: controller2, crossAxisCount: 1, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('CustomScrollViews with controllers are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('CustomScrollViews with controllers are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: CustomScrollView( - controller: ScrollController(), + controller: controller2, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('ListViews with controllers are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('ListViews with controllers are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView( - controller: ScrollController(), + controller: controller2, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('GridViews with controllers are non-primary by default', (WidgetTester tester) async { - final ScrollController controller = ScrollController(); + testWidgetsWithLeakTracking('GridViews with controllers are non-primary by default', (WidgetTester tester) async { + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count( - controller: ScrollController(), + controller: controller2, crossAxisCount: 1, ), - controller: controller, + controller: controller1, )); - expect(controller.hasClients, isFalse); + expect(controller1.hasClients, isFalse); }); - testWidgets('CustomScrollView sets PrimaryScrollController when primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1142,8 +1269,9 @@ void main() { expect(scrollable.controller, primaryScrollController); }); - testWidgets('ListView sets PrimaryScrollController when primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1157,8 +1285,9 @@ void main() { expect(scrollable.controller, primaryScrollController); }); - testWidgets('GridView sets PrimaryScrollController when primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1172,9 +1301,10 @@ void main() { expect(scrollable.controller, primaryScrollController); }); - testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async { const Key innerKey = Key('inner'); final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1202,27 +1332,27 @@ void main() { expect(innerScrollable.controller, isNull); }); - testWidgets('Primary ListViews are always scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Primary ListViews are always scrollable', (WidgetTester tester) async { final ListView view = ListView(primary: true); expect(view.physics, isA<AlwaysScrollableScrollPhysics>()); }); - testWidgets('Non-primary ListViews are not always scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Non-primary ListViews are not always scrollable', (WidgetTester tester) async { final ListView view = ListView(primary: false); expect(view.physics, isNot(isA<AlwaysScrollableScrollPhysics>())); }); - testWidgets('Defaulting-to-primary ListViews are always scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defaulting-to-primary ListViews are always scrollable', (WidgetTester tester) async { final ListView view = ListView(); expect(view.physics, isA<AlwaysScrollableScrollPhysics>()); }); - testWidgets('Defaulting-to-not-primary ListViews are not always scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Defaulting-to-not-primary ListViews are not always scrollable', (WidgetTester tester) async { final ListView view = ListView(scrollDirection: Axis.horizontal); expect(view.physics, isNot(isA<AlwaysScrollableScrollPhysics>())); }); - testWidgets('primary:true leads to scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('primary:true leads to scrolling', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( @@ -1242,7 +1372,7 @@ void main() { expect(scrolled, isTrue); }); - testWidgets('primary:false leads to no scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('primary:false leads to no scrolling', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( @@ -1262,7 +1392,7 @@ void main() { expect(scrolled, isFalse); }); - testWidgets('physics:AlwaysScrollableScrollPhysics actually overrides primary:false default behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('physics:AlwaysScrollableScrollPhysics actually overrides primary:false default behavior', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( @@ -1283,7 +1413,7 @@ void main() { expect(scrolled, isTrue); }); - testWidgets('physics:ScrollPhysics actually overrides primary:true default behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('physics:ScrollPhysics actually overrides primary:true default behavior', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( @@ -1304,7 +1434,7 @@ void main() { expect(scrolled, isFalse); }); - testWidgets('separatorBuilder must return something', (WidgetTester tester) async { + testWidgetsWithLeakTracking('separatorBuilder must return something', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; Widget buildFrame(Widget firstSeparator) { @@ -1332,7 +1462,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('when itemBuilder throws, creates Error Widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when itemBuilder throws, creates Error Widget', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; Widget buildFrame(bool throwOnFirstItem) { @@ -1363,7 +1493,7 @@ void main() { expect(finder, findsOneWidget); }); - testWidgets('when separatorBuilder throws, creates ErrorWidget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when separatorBuilder throws, creates ErrorWidget', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; const Key key = Key('list'); @@ -1399,14 +1529,14 @@ void main() { expect(finder, findsOneWidget); }); - testWidgets('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView( itemExtent: 100, prototypeItem: const SizedBox(), ), throwsAssertionError); }); - testWidgets('ListView.builder asserts on negative childCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder asserts on negative childCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); @@ -1415,7 +1545,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ListView.builder asserts on negative semanticChildCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder asserts on negative semanticChildCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); @@ -1425,7 +1555,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ListView.builder asserts on nonsensical childCount/semanticChildCount', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder asserts on nonsensical childCount/semanticChildCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); @@ -1435,7 +1565,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); @@ -1445,7 +1575,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('ListView.custom asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.custom asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView.custom( childrenDelegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -1457,7 +1587,7 @@ void main() { ), throwsAssertionError); }); - testWidgets('PrimaryScrollController provides fallback ScrollActions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PrimaryScrollController provides fallback ScrollActions', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CustomScrollView( @@ -1501,7 +1631,7 @@ void main() { ); }); - testWidgets('Fallback ScrollActions handle too many positions with error message', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Fallback ScrollActions handle too many positions with error message', (WidgetTester tester) async { Widget getScrollView() { return SizedBox( width: 400.0, @@ -1550,7 +1680,7 @@ void main() { ); }); - testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( @@ -1588,7 +1718,7 @@ void main() { expect(item2Height, 30.0); }); - testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/scrollable_animations_test.dart b/packages/flutter/test/widgets/scrollable_animations_test.dart index b055f6a635fe8..041d2496561e5 100644 --- a/packages/flutter/test/widgets/scrollable_animations_test.dart +++ b/packages/flutter/test/widgets/scrollable_animations_test.dart @@ -5,10 +5,12 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Does not animate if already at target position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not animate if already at target position', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -27,8 +29,9 @@ void main() { expect(controller.position.pixels, currentPosition); }); - testWidgets('Does not animate if already at target position within tolerance', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not animate if already at target position within tolerance', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -50,8 +53,9 @@ void main() { expect(controller.position.pixels, targetPosition); }); - testWidgets('Animates if going to a position outside of tolerance', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animates if going to a position outside of tolerance', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/scrollable_dispose_test.dart b/packages/flutter/test/widgets/scrollable_dispose_test.dart index 79eaf6bc86196..90eec2250c3fa 100644 --- a/packages/flutter/test/widgets/scrollable_dispose_test.dart +++ b/packages/flutter/test/widgets/scrollable_dispose_test.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; void main() { - testWidgets('simultaneously dispose a widget and end the scroll animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('simultaneously dispose a widget and end the scroll animation', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -26,10 +27,11 @@ void main() { await tester.pump(const Duration(hours: 5)); }); - testWidgets('Disposing a (nested) Scrollable while holding in overscroll does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Disposing a (nested) Scrollable while holding in overscroll does not crash', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/27707. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final Key outerContainer = GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/scrollable_fling_test.dart b/packages/flutter/test/widgets/scrollable_fling_test.dart index 3d521377007b1..29dc51b0933bc 100644 --- a/packages/flutter/test/widgets/scrollable_fling_test.dart +++ b/packages/flutter/test/widgets/scrollable_fling_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const TextStyle testFont = TextStyle( color: Color(0xFF00FF00), @@ -31,7 +32,7 @@ Future<void> pumpTest(WidgetTester tester, TargetPlatform platform) async { const double dragOffset = 213.82; void main() { - testWidgets('Flings on different platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flings on different platforms', (WidgetTester tester) async { double getCurrentOffset() { return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels; } @@ -96,7 +97,7 @@ void main() { expect(linuxResult, equals(androidResult)); }); - testWidgets('fling and tap to stop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling and tap to stop', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( @@ -126,7 +127,7 @@ void main() { expect(log, equals(<String>['tap 21', 'tap 35'])); }); - testWidgets('fling and wait and tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fling and wait and tap', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/scrollable_grid_test.dart b/packages/flutter/test/widgets/scrollable_grid_test.dart index 86c4dfaeb20a1..0f404f4188b93 100644 --- a/packages/flutter/test/widgets/scrollable_grid_test.dart +++ b/packages/flutter/test/widgets/scrollable_grid_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('GridView default control', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView default control', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -20,7 +21,7 @@ void main() { }); // Tests https://github.com/flutter/flutter/issues/5522 - testWidgets('GridView displays correct children with nonzero padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView displays correct children with nonzero padding', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0); final Widget testWidget = Directionality( @@ -76,7 +77,7 @@ void main() { expect(find.text('4'), findsNothing); }); - testWidgets('GridView.count() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count() fixed itemExtent, scroll to end, append, scroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/9506 Widget buildFrame(int itemCount) { return Directionality( diff --git a/packages/flutter/test/widgets/scrollable_helpers_test.dart b/packages/flutter/test/widgets/scrollable_helpers_test.dart index 17b623dc09f61..f693442e5f14e 100644 --- a/packages/flutter/test/widgets/scrollable_helpers_test.dart +++ b/packages/flutter/test/widgets/scrollable_helpers_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; final LogicalKeyboardKey modifierKey = defaultTargetPlatform == TargetPlatform.macOS ? LogicalKeyboardKey.metaLeft @@ -13,8 +14,9 @@ final LogicalKeyboardKey modifierKey = defaultTargetPlatform == TargetPlatform.m void main() { group('ScrollableDetails', (){ - final ScrollController controller = ScrollController(); test('copyWith / == / hashCode', () { + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final ScrollableDetails details = ScrollableDetails( direction: AxisDirection.down, controller: controller, @@ -42,6 +44,8 @@ void main() { }); test('toString', (){ + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); const ScrollableDetails bareDetails = ScrollableDetails( direction: AxisDirection.right, ); @@ -86,8 +90,9 @@ void main() { }); }); - testWidgets("Keyboard scrolling doesn't happen if scroll physics are set to NeverScrollableScrollPhysics", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Keyboard scrolling doesn't happen if scroll physics are set to NeverScrollableScrollPhysics", (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -152,8 +157,9 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -223,8 +229,9 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -283,8 +290,9 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -346,9 +354,11 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final FocusNode focusNode = FocusNode(debugLabel: 'SizedBox'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -420,9 +430,11 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final FocusNode focusNode = FocusNode(debugLabel: 'SizedBox'); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.fuchsia), @@ -479,8 +491,9 @@ void main() { await tester.pumpAndSettle(); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<String> items = List<String>.generate(20, (int index) => 'Item $index'); await tester.pumpWidget( MaterialApp( @@ -550,7 +563,7 @@ void main() { ); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Can scroll using intents only', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can scroll using intents only', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: ListView( diff --git a/packages/flutter/test/widgets/scrollable_in_overlay_test.dart b/packages/flutter/test/widgets/scrollable_in_overlay_test.dart index c28f535b8625b..2207c762bcf7a 100644 --- a/packages/flutter/test/widgets/scrollable_in_overlay_test.dart +++ b/packages/flutter/test/widgets/scrollable_in_overlay_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { test('Can dispose ScrollPosition when hasPixels is false', () { @@ -18,9 +19,10 @@ void main() { position.dispose(); // Should not throw/assert. }); - testWidgets('scrollable in hidden overlay does not crash when unhidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrollable in hidden overlay does not crash when unhidden', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/44269. final TabController controller = TabController(vsync: const TestVSync(), length: 1); + addTearDown(controller.dispose); final OverlayEntry entry1 = OverlayEntry( maintainState: true, diff --git a/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart b/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart index e956f34bd9a09..15bef69c25d0b 100644 --- a/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart +++ b/packages/flutter/test/widgets/scrollable_list_hit_testing_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const List<int> items = <int>[0, 1, 2, 3, 4, 5]; void main() { - testWidgets('Tap item after scroll - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap item after scroll - horizontal', (WidgetTester tester) async { final List<int> tapped = <int>[]; await tester.pumpWidget( Directionality( @@ -51,7 +52,7 @@ void main() { expect(tapped, equals(<int>[2])); }); - testWidgets('Tap item after scroll - vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap item after scroll - vertical', (WidgetTester tester) async { final List<int> tapped = <int>[]; await tester.pumpWidget( Directionality( @@ -94,7 +95,7 @@ void main() { expect(tapped, equals(<int>[1])); // the center of the third item is off-screen so it shouldn't get hit }); - testWidgets('Padding scroll anchor start', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding scroll anchor start', (WidgetTester tester) async { final List<int> tapped = <int>[]; await tester.pumpWidget( @@ -126,7 +127,7 @@ void main() { expect(tapped, equals(<int>[0, 1, 1])); }); - testWidgets('Padding scroll anchor end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Padding scroll anchor end', (WidgetTester tester) async { final List<int> tapped = <int>[]; await tester.pumpWidget( @@ -159,7 +160,7 @@ void main() { expect(tapped, equals(<int>[0, 1, 1])); }); - testWidgets('Tap immediately following clamped overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tap immediately following clamped overscroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/5709 final List<int> tapped = <int>[]; diff --git a/packages/flutter/test/widgets/scrollable_of_test.dart b/packages/flutter/test/widgets/scrollable_of_test.dart index 03ce562419b8c..1aec1979dd200 100644 --- a/packages/flutter/test/widgets/scrollable_of_test.dart +++ b/packages/flutter/test/widgets/scrollable_of_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class ScrollPositionListener extends StatefulWidget { const ScrollPositionListener({ super.key, required this.child, required this.log}); @@ -123,9 +124,10 @@ class TestChildState extends State<TestChild> { } void main() { - testWidgets('Scrollable.of() dependent rebuilds when Scrollable position changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollable.of() dependent rebuilds when Scrollable position changes', (WidgetTester tester) async { late String logValue; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); // Changing the SingleChildScrollView's physics causes the // ScrollController's ScrollPosition to be rebuilt. @@ -163,7 +165,7 @@ void main() { expect(logValue, 'listener 400.0'); }); - testWidgets('Scrollable.of() is possible using ScrollNotification context', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollable.of() is possible using ScrollNotification context', (WidgetTester tester) async { late ScrollNotification notification; await tester.pumpWidget(NotificationListener<ScrollNotification>( @@ -183,9 +185,11 @@ void main() { expect(Scrollable.of(notification.context!), equals(scrollableElement.state)); }); - testWidgets('Static Scrollable methods can target a specific axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Static Scrollable methods can target a specific axis', (WidgetTester tester) async { final TestScrollController horizontalController = TestScrollController(deferLoading: true); + addTearDown(horizontalController.dispose); final TestScrollController verticalController = TestScrollController(deferLoading: false); + addTearDown(verticalController.dispose); late final AxisDirection foundAxisDirection; late final bool foundRecommendation; @@ -218,7 +222,7 @@ void main() { expect(foundRecommendation, isTrue); }); - testWidgets('Axis targeting scrollables establishes the correct dependencies', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Axis targeting scrollables establishes the correct dependencies', (WidgetTester tester) async { final GlobalKey<TestScrollableState> verticalKey = GlobalKey<TestScrollableState>(); final GlobalKey<TestChildState> childKey = GlobalKey<TestChildState>(); @@ -237,12 +241,15 @@ void main() { expect(verticalKey.currentState!.dependenciesChanged, 1); expect(childKey.currentState!.dependenciesChanged, 1); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + // Change the horizontal ScrollView, adding a controller await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( scrollDirection: Axis.horizontal, - controller: ScrollController(), + controller: controller, child: TestScrollable( key: verticalKey, child: TestChild(key: childKey), diff --git a/packages/flutter/test/widgets/scrollable_restoration_test.dart b/packages/flutter/test/widgets/scrollable_restoration_test.dart index fb5e251941d32..2aac5a4148e14 100644 --- a/packages/flutter/test/widgets/scrollable_restoration_test.dart +++ b/packages/flutter/test/widgets/scrollable_restoration_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('CustomScrollView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomScrollView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: CustomScrollView( @@ -33,7 +34,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('ListView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListView( @@ -53,7 +54,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('ListView.builder restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.builder restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListView.builder( @@ -70,7 +71,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('ListView.separated restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.separated restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListView.separated( @@ -89,7 +90,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('ListView.custom restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListView.custom restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListView.custom( @@ -111,7 +112,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('GridView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: GridView( @@ -132,7 +133,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('GridView.builder restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.builder restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: GridView.builder( @@ -150,7 +151,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('GridView.custom restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.custom restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: GridView.custom( @@ -173,7 +174,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('GridView.count restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.count restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: GridView.count( @@ -194,7 +195,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('GridView.extent restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('GridView.extent restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: GridView.extent( @@ -215,7 +216,7 @@ void main() { await restoreScrollAndVerify(tester); }); - testWidgets('SingleChildScrollView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: SingleChildScrollView( @@ -262,7 +263,7 @@ void main() { expect(tester.getTopLeft(find.text('Tile 1')), const Offset(0, -475)); }); - testWidgets('PageView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: PageView( @@ -278,7 +279,7 @@ void main() { await pageViewScrollAndRestore(tester); }); - testWidgets('PageView.builder restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView.builder restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: PageView.builder( @@ -294,7 +295,7 @@ void main() { await pageViewScrollAndRestore(tester); }); - testWidgets('PageView.custom restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PageView.custom restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: PageView.custom( @@ -315,7 +316,7 @@ void main() { await pageViewScrollAndRestore(tester); }); - testWidgets('ListWheelScrollView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListWheelScrollView restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListWheelScrollView( @@ -332,7 +333,7 @@ void main() { await restoreScrollAndVerify(tester, secondOffset: 542); }); - testWidgets('ListWheelScrollView.useDelegate restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListWheelScrollView.useDelegate restoration', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListWheelScrollView.useDelegate( @@ -354,7 +355,7 @@ void main() { await restoreScrollAndVerify(tester, secondOffset: 542); }); - testWidgets('NestedScrollView restoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('NestedScrollView restoration', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: TestHarness( @@ -422,7 +423,7 @@ void main() { expect(find.text('Tile 10'), findsOneWidget); }); - testWidgets('RestorationData is flushed even if no frame is scheduled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RestorationData is flushed even if no frame is scheduled', (WidgetTester tester) async { await tester.pumpWidget( TestHarness( child: ListView( diff --git a/packages/flutter/test/widgets/scrollable_selection_test.dart b/packages/flutter/test/widgets/scrollable_selection_test.dart index 54a07e2f487e2..043b484bff97d 100644 --- a/packages/flutter/test/widgets/scrollable_selection_test.dart +++ b/packages/flutter/test/widgets/scrollable_selection_test.dart @@ -4,10 +4,12 @@ import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'clipboard_utils.dart'; import 'keyboard_utils.dart'; @@ -35,7 +37,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null); }); - testWidgets('mouse can select multiple widgets', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse can select multiple widgets', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -73,7 +75,7 @@ void main() { await gesture.up(); }); - testWidgets('mouse can select multiple widgets - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse can select multiple widgets - horizontal', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -106,8 +108,92 @@ void main() { await gesture.up(); }); - testWidgets('select to scroll forward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse can select multiple widgets on double-click drag', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: SelectionArea( + selectionControls: materialTextSelectionControls, + child: ListView.builder( + itemCount: 100, + itemBuilder: (BuildContext context, int index) { + return Text('Item $index'); + }, + ), + ), + )); + await tester.pumpAndSettle(); + + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: ui.PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.up(); + await tester.pump(); + await gesture.down(textOffsetToPosition(paragraph1, 2)); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + await tester.pump(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 4)); + // Should select the rest of paragraph 1. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 3'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph3, 3)); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); + + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('mouse can select multiple widgets on double-click drag - horizontal', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: SelectionArea( + selectionControls: materialTextSelectionControls, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 100, + itemBuilder: (BuildContext context, int index) { + return Text('Item $index'); + }, + ), + ), + )); + await tester.pumpAndSettle(); + + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: ui.PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph1, 2)); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + await tester.pump(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 5) + const Offset(0, 5)); + // Should select the rest of paragraph 1. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('select to scroll forward', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -155,8 +241,9 @@ void main() { await gesture.up(); }); - testWidgets('select to scroll works for small scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll works for small scrollable', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: SelectionArea( @@ -201,8 +288,9 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('select to scroll backward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll backward', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -249,8 +337,9 @@ void main() { expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 0)); }); - testWidgets('select to scroll forward - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll forward - horizontal', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -297,8 +386,9 @@ void main() { await gesture.up(); }); - testWidgets('select to scroll backward - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll backward - horizontal', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -346,8 +436,9 @@ void main() { await gesture.up(); }); - testWidgets('preserve selection when out of view.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('preserve selection when out of view.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -393,8 +484,9 @@ void main() { expect(paragraph50.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4)); }); - testWidgets('can select all non-Apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can select all non-Apple', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -419,8 +511,9 @@ void main() { expect(find.text('Item 13'), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia })); - testWidgets('can select all - Apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can select all - Apple', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -445,8 +538,9 @@ void main() { expect(find.text('Item 13'), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('select to scroll by dragging selection handles forward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll by dragging selection handles forward', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -467,6 +561,7 @@ void main() { addTearDown(gesture.removePointer); await tester.pump(const Duration(milliseconds: 500)); await gesture.up(); + await tester.pumpAndSettle(); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); @@ -502,8 +597,9 @@ void main() { await gesture.up(); }); - testWidgets('select to scroll by dragging start selection handle stops scroll when released', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll by dragging start selection handle stops scroll when released', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -524,6 +620,7 @@ void main() { addTearDown(gesture.removePointer); await tester.pump(const Duration(milliseconds: 500)); await gesture.up(); + await tester.pumpAndSettle(); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); @@ -556,8 +653,9 @@ void main() { expect(controller.offset, previousOffset); }); - testWidgets('select to scroll by dragging end selection handle stops scroll when released', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select to scroll by dragging end selection handle stops scroll when released', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -578,6 +676,7 @@ void main() { addTearDown(gesture.removePointer); await tester.pump(const Duration(milliseconds: 500)); await gesture.up(); + await tester.pumpAndSettle(); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); @@ -609,9 +708,11 @@ void main() { expect(controller.offset, previousOffset); }); - testWidgets('keyboard selection should auto scroll - vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard selection should auto scroll - vertical', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -672,9 +773,11 @@ void main() { expect(controller.offset, 72.0); }, variant: TargetPlatformVariant.all()); - testWidgets('keyboard selection should auto scroll - vertical reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard selection should auto scroll - vertical reversed', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -736,9 +839,11 @@ void main() { expect(controller.offset, 72.0); }, variant: TargetPlatformVariant.all()); - testWidgets('keyboard selection should auto scroll - horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard selection should auto scroll - horizontal', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -782,9 +887,11 @@ void main() { expect(controller.offset, 352.0); }, variant: TargetPlatformVariant.all()); - testWidgets('keyboard selection should auto scroll - horizontal reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard selection should auto scroll - horizontal reversed', (WidgetTester tester) async { final FocusNode node = FocusNode(); + addTearDown(node.dispose); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: node, @@ -838,8 +945,9 @@ void main() { }, variant: TargetPlatformVariant.all()); group('Complex cases', () { - testWidgets('selection starts outside of the scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection starts outside of the scrollable', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -882,9 +990,11 @@ void main() { expect(controller.offset, 1000.0); }); - testWidgets('nested scrollables keep selection alive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('nested scrollables keep selection alive', (WidgetTester tester) async { final ScrollController outerController = ScrollController(); + addTearDown(outerController.dispose); final ScrollController innerController = ScrollController(); + addTearDown(innerController.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( selectionControls: materialTextSelectionControls, @@ -946,9 +1056,11 @@ void main() { expect(innerParagraph24.selections[0], const TextSelection(baseOffset: 0, extentOffset: 2)); }); - testWidgets('can copy off screen selection - Apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can copy off screen selection - Apple', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: focusNode, @@ -987,9 +1099,11 @@ void main() { expect(clipboardData['text'], 'em 0It'); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('can copy off screen selection - non-Apple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can copy off screen selection - non-Apple', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget(MaterialApp( home: SelectionArea( focusNode: focusNode, diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index 5307fc7a6d73e..f9e2d1679e8df 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -16,7 +17,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('scrollable exposes the correct semantic actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrollable exposes the correct semantic actions', (WidgetTester tester) async { semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -42,7 +43,7 @@ void main() { semantics.dispose(); }); - testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showOnScreen works in scrollable', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation const double kItemHeight = 40.0; @@ -57,6 +58,7 @@ void main() { final ScrollController scrollController = ScrollController( initialScrollOffset: kItemHeight / 2, ); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( @@ -80,7 +82,7 @@ void main() { semantics.dispose(); }); - testWidgets('showOnScreen works with pinned app bar and sliver list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showOnScreen works with pinned app bar and sliver list', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation const double kItemHeight = 100.0; @@ -96,6 +98,7 @@ void main() { final ScrollController scrollController = ScrollController( initialScrollOffset: kItemHeight / 2, ); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -142,7 +145,7 @@ void main() { semantics.dispose(); }); - testWidgets('showOnScreen works with pinned app bar and individual slivers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('showOnScreen works with pinned app bar and individual slivers', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation const double kItemHeight = 100.0; @@ -166,6 +169,7 @@ void main() { final ScrollController scrollController = ScrollController( initialScrollOffset: 2.5 * kItemHeight, ); + addTearDown(scrollController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -210,7 +214,7 @@ void main() { semantics.dispose(); }); - testWidgets('correct scrollProgress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('correct scrollProgress', (WidgetTester tester) async { semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( @@ -253,7 +257,7 @@ void main() { semantics.dispose(); }); - testWidgets('correct scrollProgress for unbound', (WidgetTester tester) async { + testWidgetsWithLeakTracking('correct scrollProgress for unbound', (WidgetTester tester) async { semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( @@ -303,7 +307,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics tree is populated mid-scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics tree is populated mid-scroll', (WidgetTester tester) async { semantics = SemanticsTester(tester); final List<Widget> children = List<Widget>.generate(80, (int i) => SizedBox( @@ -328,7 +332,7 @@ void main() { semantics.dispose(); }); - testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can toggle semantics on, off, on without crash', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -434,7 +438,7 @@ void main() { }); - testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('brings item above leading edge to leading edge', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -450,7 +454,7 @@ void main() { semantics.dispose(); }); - testWidgets('brings item below trailing edge to trailing edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('brings item below trailing edge to trailing edge', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -466,7 +470,7 @@ void main() { semantics.dispose(); }); - testWidgets('does not change position of items already fully on-screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not change position of items already fully on-screen', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -536,10 +540,13 @@ void main() { ), ), ); + }); + tearDown(() { + scrollController.dispose(); }); - testWidgets('brings item above leading edge to leading edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('brings item above leading edge to leading edge', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -555,7 +562,7 @@ void main() { semantics.dispose(); }); - testWidgets('brings item below trailing edge to trailing edge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('brings item below trailing edge to trailing edge', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -571,7 +578,7 @@ void main() { semantics.dispose(); }); - testWidgets('does not change position of items already fully on-screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not change position of items already fully on-screen', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation await tester.pumpWidget(widgetUnderTest); @@ -589,7 +596,7 @@ void main() { }); - testWidgets('transform of inner node from useTwoPaneSemantics scrolls correctly with nested scrollables', (WidgetTester tester) async { + testWidgetsWithLeakTracking('transform of inner node from useTwoPaneSemantics scrolls correctly with nested scrollables', (WidgetTester tester) async { semantics = SemanticsTester(tester); // enables semantics tree generation // Context: https://github.com/flutter/flutter/issues/61631 diff --git a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart index ff54d1084e71b..8e35b357982e0 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_traversal_order_test.dart @@ -5,13 +5,17 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Traversal Order of SliverList', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order of SliverList', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(initialScrollOffset: 3000.0); + addTearDown(controller.dispose); + final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SizedBox( height: 200.0, @@ -38,7 +42,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: CustomScrollView( - controller: ScrollController(initialScrollOffset: 3000.0), + controller: controller, semanticChildCount: 30, slivers: <Widget>[ SliverList( @@ -182,9 +186,12 @@ void main() { semantics.dispose(); }); - testWidgets('Traversal Order of SliverFixedExtentList', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order of SliverFixedExtentList', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(initialScrollOffset: 3000.0); + addTearDown(controller.dispose); + final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SizedBox( height: 200.0, @@ -211,7 +218,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: CustomScrollView( - controller: ScrollController(initialScrollOffset: 3000.0), + controller: controller, slivers: <Widget>[ SliverFixedExtentList( itemExtent: 200.0, @@ -321,9 +328,12 @@ void main() { semantics.dispose(); }); - testWidgets('Traversal Order of SliverGrid', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order of SliverGrid', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(initialScrollOffset: 1600.0); + addTearDown(controller.dispose); + final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SizedBox( height: 200.0, @@ -338,7 +348,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: CustomScrollView( - controller: ScrollController(initialScrollOffset: 1600.0), + controller: controller, slivers: <Widget>[ SliverGrid.count( crossAxisCount: 2, @@ -449,9 +459,12 @@ void main() { semantics.dispose(); }); - testWidgets('Traversal Order of List of individual slivers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order of List of individual slivers', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(initialScrollOffset: 3000.0); + addTearDown(controller.dispose); + final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SliverToBoxAdapter( child: SizedBox( @@ -480,7 +493,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: CustomScrollView( - controller: ScrollController(initialScrollOffset: 3000.0), + controller: controller, slivers: listChildren, ), ), @@ -585,9 +598,12 @@ void main() { semantics.dispose(); }); - testWidgets('Traversal Order of in a SingleChildScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order of in a SingleChildScrollView', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final ScrollController controller = ScrollController(initialScrollOffset: 3000.0); + addTearDown(controller.dispose); + final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SizedBox( height: 200.0, @@ -614,7 +630,7 @@ void main() { child: MediaQuery( data: const MediaQueryData(), child: SingleChildScrollView( - controller: ScrollController(initialScrollOffset: 3000.0), + controller: controller, child: Column( children: listChildren, ), @@ -671,7 +687,7 @@ void main() { semantics.dispose(); }); - testWidgets('Traversal Order with center child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Traversal Order with center child', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Semantics( diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart index 01ba9bccef8d3..e7576552f6b24 100644 --- a/packages/flutter/test/widgets/scrollable_test.dart +++ b/packages/flutter/test/widgets/scrollable_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -109,7 +110,7 @@ void resetScrollOffset(WidgetTester tester) { } void main() { - testWidgets('Flings on different platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Flings on different platforms', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.android); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); expect(getScrollOffset(tester), dragOffset); @@ -145,7 +146,7 @@ void main() { expect(macOSResult, lessThan(iOSResult)); // iOS is slipperier than macOS }); - testWidgets('Holding scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Holding scroll', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); await tester.drag(find.byType(Scrollable), const Offset(0.0, 200.0), touchSlopY: 0.0); expect(getScrollOffset(tester), -200.0); @@ -164,7 +165,7 @@ void main() { expect(getScrollOffset(tester), 0.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Repeated flings builds momentum', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Repeated flings builds momentum', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.pump(); // trigger fling @@ -177,7 +178,7 @@ void main() { expect(getScrollVelocity(tester), greaterThan(1100.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Repeated flings do not build momentum on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Repeated flings do not build momentum on Android', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.android); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.pump(); // trigger fling @@ -190,7 +191,7 @@ void main() { expect(getScrollVelocity(tester), moreOrLessEquals(1000.0)); }); - testWidgets('A slower final fling does not apply carried momentum', (WidgetTester tester) async { + testWidgetsWithLeakTracking('A slower final fling does not apply carried momentum', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.pump(); // trigger fling @@ -207,7 +208,7 @@ void main() { expect(getScrollVelocity(tester), lessThan(200.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('No iOS/macOS momentum build with flings in opposite directions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No iOS/macOS momentum build with flings in opposite directions', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.pump(); // trigger fling @@ -220,7 +221,7 @@ void main() { expect(getScrollVelocity(tester), -1000.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('No iOS/macOS momentum kept on hold gestures', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No iOS/macOS momentum kept on hold gestures', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.pump(); // trigger fling @@ -233,7 +234,7 @@ void main() { expect(getScrollVelocity(tester), 0.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Drags creeping unaffected on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drags creeping unaffected on Android', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.android); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); await gesture.moveBy(const Offset(0.0, -0.5)); @@ -244,7 +245,7 @@ void main() { expect(getScrollOffset(tester), 1.5); }); - testWidgets('Drags creeping must break threshold on iOS/macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drags creeping must break threshold on iOS/macOS', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); await gesture.moveBy(const Offset(0.0, -0.5)); @@ -264,7 +265,7 @@ void main() { expect(getScrollOffset(tester), 0.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Big drag over threshold magnitude preserved on iOS/macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Big drag over threshold magnitude preserved on iOS/macOS', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); await gesture.moveBy(const Offset(0.0, -30.0)); @@ -272,7 +273,7 @@ void main() { expect(getScrollOffset(tester), 30.0); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Slow threshold breaks are attenuated on iOS/macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slow threshold breaks are attenuated on iOS/macOS', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); // This is a typical 'hesitant' iOS scroll start. @@ -283,7 +284,7 @@ void main() { expect(getScrollOffset(tester), moreOrLessEquals(11.16666666666666673)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Small continuing motion preserved on iOS/macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Small continuing motion preserved on iOS/macOS', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold. @@ -296,7 +297,7 @@ void main() { expect(getScrollOffset(tester), 31.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Motion stop resets threshold on iOS/macOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Motion stop resets threshold on iOS/macOS', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true)); await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold. @@ -319,7 +320,7 @@ void main() { expect(getScrollOffset(tester), 32.5); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Scroll pointer signals are handled on Fuchsia', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll pointer signals are handled on Fuchsia', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.fuchsia); final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport)); final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse); @@ -332,7 +333,7 @@ void main() { expect(getScrollOffset(tester), 0.0); }); - testWidgets('Scroll pointer signals are handled when there is competition', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll pointer signals are handled when there is competition', (WidgetTester tester) async { // This is a regression test. When there are multiple scrollables listening // to the same event, for example when scrollables are nested, there used // to be exceptions at scrolling events. @@ -349,7 +350,7 @@ void main() { expect(getScrollOffset(tester), 0.0); }); - testWidgets('Scroll pointer signals are ignored when scrolling is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll pointer signals are ignored when scrolling is disabled', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.fuchsia, scrollable: false); final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport)); final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse); @@ -359,10 +360,12 @@ void main() { expect(getScrollOffset(tester), 0.0); }); - testWidgets('Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse', (WidgetTester tester) async { ScrollDirection? lastUserScrollingDirection; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await pumpTest(tester, TargetPlatform.fuchsia, controller: controller); controller.addListener(() { @@ -393,7 +396,7 @@ void main() { }); - testWidgets('Scrolls in correct direction when scroll axis is reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolls in correct direction when scroll axis is reversed', (WidgetTester tester) async { await pumpTest(tester, TargetPlatform.fuchsia, reverse: true); final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport)); @@ -405,7 +408,7 @@ void main() { expect(getScrollOffset(tester), 20.0); }); - testWidgets('Scrolls horizontally when shift is pressed by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolls horizontally when shift is pressed by default', (WidgetTester tester) async { await pumpTest( tester, debugDefaultTargetPlatformOverride, @@ -432,7 +435,7 @@ void main() { expect(getScrollOffset(tester), 20.0); }, variant: TargetPlatformVariant.all()); - testWidgets('Scroll axis is not flipped for trackpad', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll axis is not flipped for trackpad', (WidgetTester tester) async { await pumpTest( tester, debugDefaultTargetPlatformOverride, @@ -459,7 +462,7 @@ void main() { expect(getScrollOffset(tester), 0.0); }, variant: TargetPlatformVariant.all()); - testWidgets('Scrolls horizontally when custom key is pressed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrolls horizontally when custom key is pressed', (WidgetTester tester) async { await pumpTest( tester, debugDefaultTargetPlatformOverride, @@ -487,7 +490,7 @@ void main() { expect(getScrollOffset(tester), 20.0); }, variant: TargetPlatformVariant.all()); - testWidgets('Still scrolls horizontally when other keys are pressed at the same time', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Still scrolls horizontally when other keys are pressed at the same time', (WidgetTester tester) async { await pumpTest( tester, debugDefaultTargetPlatformOverride, @@ -536,7 +539,7 @@ void main() { ); } - testWidgets('Hold does not disable user interaction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hold does not disable user interaction', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66816. await pumpTestWidget(tester, canDrag: true); final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>( @@ -555,7 +558,7 @@ void main() { expect(renderIgnorePointer.ignoring, false); }); - testWidgets('Drag disables user interaction when recognized', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag disables user interaction when recognized', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66816. await pumpTestWidget(tester, canDrag: true); final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>( @@ -577,7 +580,7 @@ void main() { expect(renderIgnorePointer.ignoring, false); }); - testWidgets('Ballistic disables user interaction until it stops', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ballistic disables user interaction until it stops', (WidgetTester tester) async { await pumpTestWidget(tester, canDrag: true); final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>( find.descendant(of: find.byType(CustomScrollView), matching: find.byType(IgnorePointer)), @@ -595,11 +598,13 @@ void main() { }); }); - testWidgets('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async { final List<String> widgetTracker = <String>[]; int cheapWidgets = 0; int expensiveWidgets = 0; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: ListView.builder( @@ -650,7 +655,7 @@ void main() { expect(widgetTracker.skip(17).skip(25).skip(70).every((String type) => type == 'expensive'), true); }); - testWidgets('Can recommendDeferredLoadingForContext - ballistics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can recommendDeferredLoadingForContext - ballistics', (WidgetTester tester) async { int cheapWidgets = 0; int expensiveWidgets = 0; await tester.pumpWidget(Directionality( @@ -687,7 +692,7 @@ void main() { expect(cheapWidgets, 21); }); - testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async { int cheapWidgets = 0; int expensiveWidgets = 0; await tester.pumpWidget(Directionality( @@ -731,7 +736,7 @@ void main() { expect(physics.count, 44 + 17); }); - testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async { int cheapWidgets = 0; int expensiveWidgets = 0; await tester.pumpWidget(Directionality( @@ -772,8 +777,9 @@ void main() { expect(cheapWidgets, 61); }); - testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ensureVisible does not move PageViews', (WidgetTester tester) async { final PageController controller = PageController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -942,10 +948,13 @@ void main() { expect(targetMidLeftPage1, findsOneWidget); }); - testWidgets('PointerScroll on nested NeverScrollable ListView goes to outer Scrollable.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PointerScroll on nested NeverScrollable ListView goes to outer Scrollable.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/70948 final ScrollController outerController = ScrollController(); + addTearDown(outerController.dispose); final ScrollController innerController = ScrollController(); + addTearDown(innerController.dispose); + await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( @@ -999,8 +1008,10 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/71949 - testWidgets('Zero offset pointer scroll should not trigger an assertion.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Zero offset pointer scroll should not trigger an assertion.', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + Widget build(double height) { return MaterialApp( home: Scaffold( @@ -1039,7 +1050,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Accepts drag with unknown device kind by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Accepts drag with unknown device kind by default', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/90912. await tester.pumpWidget( const MaterialApp( @@ -1068,7 +1079,7 @@ void main() { await tester.pump(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android })); - testWidgets('Does not scroll with mouse pointer drag when behavior is configured to ignore them', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not scroll with mouse pointer drag when behavior is configured to ignore them', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: false); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); @@ -1088,7 +1099,7 @@ void main() { await tester.pump(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android })); - testWidgets("Support updating 'ScrollBehavior.dragDevices' at runtime", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Support updating 'ScrollBehavior.dragDevices' at runtime", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/111716 Widget buildFrame(Set<ui.PointerDeviceKind>? dragDevices) { return MaterialApp( @@ -1122,7 +1133,7 @@ void main() { expect(getScrollOffset(tester), 200.0); }); - testWidgets('Does scroll with mouse pointer drag when behavior is not configured to ignore them', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does scroll with mouse pointer drag when behavior is not configured to ignore them', (WidgetTester tester) async { await pumpTest(tester, debugDefaultTargetPlatformOverride); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); @@ -1142,7 +1153,7 @@ void main() { await tester.pump(); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android })); - testWidgets('Updated content dimensions correctly reflect in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updated content dimensions correctly reflect in semantics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/40419. final SemanticsHandle handle = tester.ensureSemantics(); final UniqueKey listView = UniqueKey(); @@ -1200,7 +1211,7 @@ void main() { handle.dispose(); }); - testWidgets('Two panel semantics is added to the sibling nodes of direct children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Two panel semantics is added to the sibling nodes of direct children', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final UniqueKey key = UniqueKey(); await tester.pumpWidget(MaterialApp( @@ -1245,7 +1256,7 @@ void main() { handle.dispose(); }); - testWidgets('Scroll inertia cancel event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scroll inertia cancel event', (WidgetTester tester) async { await pumpTest(tester, null); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); expect(getScrollOffset(tester), dragOffset); @@ -1261,7 +1272,7 @@ void main() { expect(getScrollOffset(tester), closeTo(344.0642, 0.0001)); }); - testWidgets('Swapping viewports in a scrollable does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swapping viewports in a scrollable does not crash', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey key = GlobalKey(); final GlobalKey key1 = GlobalKey(); @@ -1270,11 +1281,13 @@ void main() { key: key, viewportBuilder: (BuildContext context, ViewportOffset position) { if (withViewPort) { + final ViewportOffset offset = ViewportOffset.zero(); + addTearDown(() => offset.dispose()); return Viewport( slivers: <Widget>[ SliverToBoxAdapter(child: Semantics(key: key1, container: true, child: const Text('text1'))) ], - offset: ViewportOffset.zero(), + offset: offset, ); } return Semantics(key: key1, container: true, child: const Text('text1')); @@ -1306,7 +1319,7 @@ void main() { semantics.dispose(); }); - testWidgets('deltaToScrollOrigin getter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('deltaToScrollOrigin getter', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: CustomScrollView( @@ -1327,7 +1340,7 @@ void main() { expect(scrollable.deltaToScrollOrigin, const Offset(0.0, 200)); }); - testWidgets('resolvedPhysics getter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('resolvedPhysics getter', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.light().copyWith( @@ -1356,6 +1369,113 @@ void main() { 'AlwaysScrollableScrollPhysics ClampingScrollPhysics RangeMaintainingScrollPhysics', ); }); + + testWidgetsWithLeakTracking('dragDevices change updates widget', (WidgetTester tester) async { + bool enable = false; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Scaffold( + body: Scrollable( + scrollBehavior: const MaterialScrollBehavior().copyWith(dragDevices: <ui.PointerDeviceKind>{ + if (enable) ui.PointerDeviceKind.mouse, + }), + viewportBuilder: (BuildContext context, ViewportOffset position) => Viewport( + offset: position, + slivers: const <Widget>[ + SliverToBoxAdapter(child: SizedBox(height: 2000.0)), + ], + ), + ), + floatingActionButton: FloatingActionButton(onPressed: () { + setState(() { + enable = !enable; + }); + }), + ), + ); + }, + ); + }, + ) + ); + + // Gesture should not work. + TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); + expect(getScrollOffset(tester), 0.0); + await gesture.moveBy(const Offset(0.0, -200)); + await tester.pumpAndSettle(); + expect(getScrollOffset(tester), 0.0); + + // Change state to include mouse pointer device. + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(); + + // Gesture should work after state change. + gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); + expect(getScrollOffset(tester), 0.0); + await gesture.moveBy(const Offset(0.0, -200)); + await tester.pumpAndSettle(); + expect(getScrollOffset(tester), 200); + }); + + testWidgetsWithLeakTracking('dragDevices change updates widget when oldWidget scrollBehavior is null', (WidgetTester tester) async { + ScrollBehavior? scrollBehavior; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Scaffold( + body: Scrollable( + physics: const ScrollPhysics(), + scrollBehavior: scrollBehavior, + viewportBuilder: (BuildContext context, ViewportOffset position) => Viewport( + offset: position, + slivers: const <Widget>[ + SliverToBoxAdapter(child: SizedBox(height: 2000.0)), + ], + ), + ), + floatingActionButton: FloatingActionButton(onPressed: () { + setState(() { + scrollBehavior = const MaterialScrollBehavior().copyWith(dragDevices: <ui.PointerDeviceKind>{ + ui.PointerDeviceKind.mouse + }); + }); + }), + ), + ); + }, + ); + }, + ) + ); + + // Gesture should not work. + TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); + expect(getScrollOffset(tester), 0.0); + await gesture.moveBy(const Offset(0.0, -200)); + await tester.pumpAndSettle(); + expect(getScrollOffset(tester), 0.0); + + // Change state to include mouse pointer device. + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(); + + // Gesture should work after state change. + gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse); + expect(getScrollOffset(tester), 0.0); + await gesture.moveBy(const Offset(0.0, -200)); + await tester.pumpAndSettle(); + expect(getScrollOffset(tester), 200); + }); } // ignore: must_be_immutable diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index 4a20a61c183fa..c518dccc8f677 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -8,8 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/src/physics/utils.dart' show nearEqual; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color _kScrollbarColor = Color(0xFF123456); const double _kThickness = 2.5; @@ -386,7 +385,7 @@ void main() { scrollMetrics: metrics, ); - testWidgets('down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('down', (WidgetTester tester) async { painter.update( metrics.copyWith( viewportDimension: size.height, @@ -416,7 +415,7 @@ void main() { expect(size.width - rect1.right, padding.right); }); - testWidgets('up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('up', (WidgetTester tester) async { painter.update( metrics.copyWith( viewportDimension: size.height, @@ -448,7 +447,7 @@ void main() { expect(size.width - rect1.right, padding.right); }); - testWidgets('left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('left', (WidgetTester tester) async { painter.update( metrics.copyWith( viewportDimension: size.width, @@ -480,7 +479,7 @@ void main() { expect(rect1.left, padding.left); }); - testWidgets('right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('right', (WidgetTester tester) async { painter.update( metrics.copyWith( viewportDimension: size.width, @@ -513,7 +512,7 @@ void main() { }); }); - testWidgets('thumb resizes gradually on overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('thumb resizes gradually on overscroll', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(1, 2, 3, 4); const Size size = Size(60, 300); final double scrollExtent = size.height * 10; @@ -666,7 +665,7 @@ void main() { expect(trackRRect.trRadius, const Radius.circular(2.0)); }); - testWidgets('ScrollbarPainter asserts if no TextDirection has been provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarPainter asserts if no TextDirection has been provided', (WidgetTester tester) async { final ScrollbarPainter painter = ScrollbarPainter( color: _kScrollbarColor, fadeoutOpacityAnimation: kAlwaysCompleteAnimation, @@ -685,8 +684,9 @@ void main() { } }); - testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Tapping the track area pages the Scroll View', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -747,7 +747,7 @@ void main() { ); }); - testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar never goes away until finger lift', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -805,7 +805,7 @@ void main() { ); }); - testWidgets('Scrollbar does not fade away while hovering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar does not fade away while hovering', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -852,7 +852,7 @@ void main() { ); }); - testWidgets('Scrollbar will fade back in when hovering over known track area', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar will fade back in when hovering over known track area', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -913,7 +913,7 @@ void main() { ); }); - testWidgets('Scrollbar will show on hover without needing to scroll first for metrics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar will show on hover without needing to scroll first for metrics', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -948,8 +948,9 @@ void main() { ); }); - testWidgets('Scrollbar thumb can be dragged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1003,8 +1004,9 @@ void main() { ); }); - testWidgets('Scrollbar thumb cannot be dragged into overscroll if the physics do not allow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb cannot be dragged into overscroll if the physics do not allow', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1055,8 +1057,9 @@ void main() { ); }); - testWidgets('Scrollbar thumb cannot be dragged into overscroll if the platform does not allow it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb cannot be dragged into overscroll if the platform does not allow it', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1123,8 +1126,9 @@ void main() { TargetPlatform.fuchsia, })); - testWidgets('Scrollbar thumb can be dragged into overscroll if the platform allows it', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged into overscroll if the platform allows it', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1190,7 +1194,7 @@ void main() { })); // Regression test for https://github.com/flutter/flutter/issues/66444 - testWidgets("RawScrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { + testWidgetsWithLeakTracking("RawScrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final GlobalKey outerKey = GlobalKey(); @@ -1252,8 +1256,9 @@ void main() { ); }); - testWidgets('Scrollbar hit test area adjusts for PointerDeviceKind', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar hit test area adjusts for PointerDeviceKind', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1341,9 +1346,10 @@ void main() { ); }); - testWidgets('hit test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hit test', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/99324 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); bool onTap = false; await tester.pumpWidget( Directionality( @@ -1390,12 +1396,14 @@ void main() { expect(onTap, true); }); - testWidgets('RawScrollbar.thumbVisibility asserts that a ScrollPosition is attached', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawScrollbar.thumbVisibility asserts that a ScrollPosition is attached', (WidgetTester tester) async { final FlutterExceptionHandler? handler = FlutterError.onError; FlutterErrorDetails? error; FlutterError.onError = (FlutterErrorDetails details) { error = details; }; + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -1404,7 +1412,7 @@ void main() { data: const MediaQueryData(), child: RawScrollbar( thumbVisibility: true, - controller: ScrollController(), + controller: controller, thumbColor: const Color(0x11111111), child: const SingleChildScrollView( child: SizedBox( @@ -1428,12 +1436,14 @@ void main() { FlutterError.onError = handler; }); - testWidgets('RawScrollbar.thumbVisibility asserts that a ScrollPosition is attached', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawScrollbar.thumbVisibility asserts that a ScrollPosition is attached', (WidgetTester tester) async { final FlutterExceptionHandler? handler = FlutterError.onError; FlutterErrorDetails? error; FlutterError.onError = (FlutterErrorDetails details) { error = details; }; + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -1442,7 +1452,7 @@ void main() { data: const MediaQueryData(), child: RawScrollbar( thumbVisibility: true, - controller: ScrollController(), + controller: controller, thumbColor: const Color(0x11111111), child: const SingleChildScrollView( child: SizedBox( @@ -1466,9 +1476,11 @@ void main() { FlutterError.onError = handler; }); - testWidgets('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( @@ -1507,9 +1519,10 @@ void main() { ); }); - testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/70105 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1681,8 +1694,9 @@ void main() { ); }); - testWidgets('Scrollbar thumb can be dragged in reverse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged in reverse', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1737,7 +1751,7 @@ void main() { ); }); - testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async { final ScrollbarPainter painter = ScrollbarPainter( color: _kScrollbarColor, fadeoutOpacityAnimation: kAlwaysCompleteAnimation, @@ -1755,8 +1769,9 @@ void main() { expect(() => painter.paint(testCanvas, size), throwsA(isA<AssertionError>())); }); - testWidgets('RawScrollbar mainAxisMargin property works properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawScrollbar mainAxisMargin property works properly', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1785,8 +1800,9 @@ void main() { ); }); - testWidgets('shape property of RawScrollbar can draw a BeveledRectangleBorder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shape property of RawScrollbar can draw a BeveledRectangleBorder', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1822,8 +1838,9 @@ void main() { ); }); - testWidgets('minThumbLength property of RawScrollbar is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('minThumbLength property of RawScrollbar is respected', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1848,8 +1865,9 @@ void main() { ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 21.0))); // thumb }); - testWidgets('shape property of RawScrollbar can draw a CircleBorder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shape property of RawScrollbar can draw a CircleBorder', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1884,8 +1902,9 @@ void main() { ); }); - testWidgets('crossAxisMargin property of RawScrollbar is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('crossAxisMargin property of RawScrollbar is respected', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1909,8 +1928,9 @@ void main() { ..rect(rect: const Rect.fromLTRB(764.0, 0.0, 770.0, 360.0))); }); - testWidgets('shape property of RawScrollbar can draw a RoundedRectangleBorder', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shape property of RawScrollbar can draw a RoundedRectangleBorder', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1943,8 +1963,9 @@ void main() { ); }); - testWidgets('minOverscrollLength property of RawScrollbar is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('minOverscrollLength property of RawScrollbar is respected', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1975,8 +1996,9 @@ void main() { ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 8.0))); }); - testWidgets('not passing any shape or radius to RawScrollbar will draw the usual rectangular thumb', (WidgetTester tester) async { + testWidgetsWithLeakTracking('not passing any shape or radius to RawScrollbar will draw the usual rectangular thumb', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2001,8 +2023,9 @@ void main() { ); }); - testWidgets('The bar can show or hide when the viewport size change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The bar can show or hide when the viewport size change', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildFrame(double height) { return Directionality( textDirection: TextDirection.ltr, @@ -2032,10 +2055,11 @@ void main() { expect(find.byType(RawScrollbar), isNot(paints..rect())); // Hide the bar. }); - testWidgets('The bar can show or hide when the view size change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The bar can show or hide when the view size change', (WidgetTester tester) async { addTearDown(tester.view.reset); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildFrame() { return Directionality( textDirection: TextDirection.ltr, @@ -2074,10 +2098,12 @@ void main() { expect(find.byType(RawScrollbar), isNot(paints..rect())); // Not shown. }); - testWidgets('Scrollbar will not flip axes based on notification is there is a scroll controller', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar will not flip axes based on notification is there is a scroll controller', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/87697 final ScrollController verticalScrollController = ScrollController(); + addTearDown(verticalScrollController.dispose); final ScrollController horizontalScrollController = ScrollController(); + addTearDown(horizontalScrollController.dispose); Widget buildFrame() { return Directionality( textDirection: TextDirection.ltr, @@ -2135,8 +2161,9 @@ void main() { ); }); - testWidgets('notificationPredicate depth test.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('notificationPredicate depth test.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final List<int> depths = <int>[]; Widget buildFrame() { return Directionality( @@ -2169,8 +2196,9 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/92262 - testWidgets('Do not crash when resize from scrollable to non-scrollable.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when resize from scrollable to non-scrollable.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildFrame(double height) { return Directionality( textDirection: TextDirection.ltr, @@ -2205,10 +2233,11 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - desktop', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - desktop', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/95840 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final UniqueKey uniqueKey = UniqueKey(); await tester.pumpWidget( Directionality( @@ -2287,10 +2316,11 @@ void main() { ); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - mobile', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/95840 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final UniqueKey uniqueKey = UniqueKey(); await tester.pumpWidget( Directionality( @@ -2423,8 +2453,9 @@ void main() { expect(painter.shouldRepaint(createPainter(scrollbarOrientation: ScrollbarOrientation.bottom)), true); }); - testWidgets('Scrollbar track can be drawn', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar track can be drawn', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2466,8 +2497,9 @@ void main() { ); }); - testWidgets('RawScrollbar correctly assigns colors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawScrollbar correctly assigns colors', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2512,8 +2544,9 @@ void main() { ); }); - testWidgets('trackRadius and radius properties of RawScrollbar can draw RoundedRectangularRect', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trackRadius and radius properties of RawScrollbar can draw RoundedRectangularRect', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2551,8 +2584,9 @@ void main() { ); }); - testWidgets('Scrollbar asserts that a visible track has a visible thumb', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar asserts that a visible track has a visible thumb', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildApp() { return Directionality( textDirection: TextDirection.ltr, @@ -2575,9 +2609,10 @@ void main() { expect(() => tester.pumpWidget(buildApp()), throwsAssertionError); }); - testWidgets('Skip the ScrollPosition check if the bar was unmounted', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Skip the ScrollPosition check if the bar was unmounted', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/103939 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildApp(bool buildBar) { return Directionality( textDirection: TextDirection.ltr, @@ -2613,9 +2648,10 @@ void main() { // Go without throw. }); - testWidgets('Track offset respects MediaQuery padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Track offset respects MediaQuery padding', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/106834 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2645,8 +2681,9 @@ void main() { ); // thumb }); - testWidgets('RawScrollbar.padding replaces MediaQueryData.padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RawScrollbar.padding replaces MediaQueryData.padding', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2677,8 +2714,9 @@ void main() { ); // thumb }); - testWidgets('Scrollbar respect the NeverScrollableScrollPhysics physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollbar respect the NeverScrollableScrollPhysics physics', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2719,9 +2757,10 @@ void main() { expect(scrollController.offset, 0.0); }); - testWidgets('The thumb should follow the pointer when the scroll metrics changed during dragging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The thumb should follow the pointer when the scroll metrics changed during dragging', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/112072 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2789,9 +2828,10 @@ void main() { ); }); - testWidgets('The scrollable should not stutter when the scroll metrics shrink during dragging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The scrollable should not stutter when the scroll metrics shrink during dragging', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/121574 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -2853,9 +2893,10 @@ void main() { expect(scrollController.offset, greaterThan(lastPosition)); }); - testWidgets('The bar support mouse wheel event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The bar support mouse wheel event', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/109659 final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); Widget buildFrame() { return Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart index 5ec1891246ce1..a7a43dab80158 100644 --- a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart +++ b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:web/web.dart' as web; extension on web.HTMLCollection { @@ -59,8 +60,9 @@ void main() { expect(foundStyle, isTrue); }); - testWidgets('right click can trigger select word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('right click can trigger select word', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final UniqueKey spy = UniqueKey(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart index e2903ab77837e..4be55348f5d2d 100644 --- a/packages/flutter/test/widgets/selectable_region_test.dart +++ b/packages/flutter/test/widgets/selectable_region_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'clipboard_utils.dart'; import 'keyboard_utils.dart'; @@ -37,12 +38,15 @@ void main() { }); group('SelectableRegion', () { - testWidgets('mouse selection sends correct events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse selection single click sends correct events', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -53,6 +57,7 @@ void main() { final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy)); final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse); addTearDown(gesture.removePointer); + await tester.pumpAndSettle(); renderSelectionSpy.events.clear(); await gesture.moveTo(const Offset(200.0, 100.0)); @@ -74,8 +79,42 @@ void main() { await gesture.up(); }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410. - testWidgets('Does not crash when using Navigator pages', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse double click sends select-word event', (WidgetTester tester) async { + final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: SelectionSpy(key: spy), + ), + ) + ); + + final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy)); + final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + renderSelectionSpy.events.clear(); + await gesture.down(const Offset(200.0, 200.0)); + await tester.pump(); + await gesture.up(); + expect(renderSelectionSpy.events.length, 1); + expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>()); + final SelectWordSelectionEvent selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent; + expect(selectionEvent.globalPosition, const Offset(200.0, 200.0)); + }); + + testWidgetsWithLeakTracking('Does not crash when using Navigator pages', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/119776 + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: Navigator( @@ -85,7 +124,7 @@ void main() { children: <Widget>[ const Text('How are you?'), SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const SelectAllWidget(child: SizedBox(width: 100, height: 100)), ), @@ -105,15 +144,18 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('can draw handles when they are at rect boundaries', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can draw handles when they are at rect boundaries', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: Column( children: <Widget>[ const Text('How are you?'), SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectAllWidget(key: spy, child: const SizedBox(width: 100, height: 100)), ), @@ -135,12 +177,15 @@ void main() { expect(renderSpy.endHandle, isNotNull); }); - testWidgets('touch does not accept drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('touch does not accept drag', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -158,12 +203,15 @@ void main() { ); }); - testWidgets('does not merge semantics node of the children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not merge semantics node of the children', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: Scaffold( body: Center( @@ -233,12 +281,15 @@ void main() { semantics.dispose(); }); - testWidgets('mouse selection always cancels previous selection', (WidgetTester tester) async { + testWidgets('mouse single-click selection collapses the selection', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -249,16 +300,25 @@ void main() { final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy)); final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse); addTearDown(gesture.removePointer); - expect(renderSelectionSpy.events.length, 1); - expect(renderSelectionSpy.events[0], isA<ClearSelectionEvent>()); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(renderSelectionSpy.events.length, 2); + expect(renderSelectionSpy.events[0], isA<SelectionEdgeUpdateEvent>()); + expect((renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent).type, SelectionEventType.startEdgeUpdate); + expect(renderSelectionSpy.events[1], isA<SelectionEdgeUpdateEvent>()); + expect((renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent).type, SelectionEventType.endEdgeUpdate); }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410. - testWidgets('touch long press sends select-word event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('touch long press sends select-word event', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -278,12 +338,15 @@ void main() { expect(selectionEvent.globalPosition, const Offset(200.0, 200.0)); }); - testWidgets('touch long press and drag sends correct events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('touch long press and drag sends correct events', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -308,16 +371,20 @@ void main() { expect(renderSelectionSpy.events[0].type, SelectionEventType.endEdgeUpdate); final SelectionEdgeUpdateEvent edgeEvent = renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent; expect(edgeEvent.globalPosition, const Offset(200.0, 50.0)); + expect(edgeEvent.granularity, TextGranularity.word); }); - testWidgets( + testWidgetsWithLeakTracking( 'touch long press cancel does not send ClearSelectionEvent', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -342,11 +409,14 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'scrolling after the selection does not send ClearSelectionEvent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/128765 final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SizedBox( @@ -355,7 +425,7 @@ void main() { child: SizedBox( height: 2000, child: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -386,12 +456,15 @@ void main() { }, ); - testWidgets('mouse long press does not send select-word event', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse long press does not send select-word event', (WidgetTester tester) async { final UniqueKey spy = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: SelectionSpy(key: spy), ), @@ -406,13 +479,13 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); await gesture.up(); expect( - renderSelectionSpy.events.every((SelectionEvent element) => element is ClearSelectionEvent), + renderSelectionSpy.events.every((SelectionEvent element) => element is SelectionEdgeUpdateEvent), isTrue, ); }); }); - testWidgets('dragging handle or selecting word triggers haptic feedback on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('dragging handle or selecting word triggers haptic feedback on Android', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { log.add(methodCall); @@ -421,11 +494,13 @@ void main() { addTearDown(() { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall); }); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Text('How are you?'), ), @@ -473,11 +548,14 @@ void main() { }, variant: TargetPlatformVariant.all()); group('SelectionArea integration', () { - testWidgets('mouse can select single text', (WidgetTester tester) async { + testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Center( child: Text('How are you'), @@ -501,12 +579,17 @@ void main() { // Check backward selection. await gesture.moveTo(textOffsetToPosition(paragraph, 1)); await tester.pump(); + expect(paragraph.selections.isEmpty, isFalse); expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1)); // Start a new drag. await gesture.up(); + await tester.pumpAndSettle(); + await gesture.down(textOffsetToPosition(paragraph, 5)); - expect(paragraph.selections.isEmpty, isTrue); + await tester.pumpAndSettle(); + expect(paragraph.selections.isEmpty, isFalse); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5)); // Selecting across line should select to the end. await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0)); @@ -514,13 +597,300 @@ void main() { expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11)); await gesture.up(); - }); + }, variant: TargetPlatformVariant.desktop()); + + testWidgetsWithLeakTracking('mouse can select single text on mobile platforms', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Center( + child: Text('How are you'), + ), + ), + ), + ); + final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.moveTo(textOffsetToPosition(paragraph, 4)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4)); + + await gesture.moveTo(textOffsetToPosition(paragraph, 6)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6)); + + // Check backward selection. + await gesture.moveTo(textOffsetToPosition(paragraph, 1)); + await tester.pump(); + expect(paragraph.selections.isEmpty, isFalse); + expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1)); + + // Start a new drag. + await gesture.up(); + await tester.pumpAndSettle(); + + await gesture.down(textOffsetToPosition(paragraph, 5)); + await tester.pumpAndSettle(); + await gesture.moveTo(textOffsetToPosition(paragraph, 6)); + await tester.pump(); + expect(paragraph.selections.isEmpty, isFalse); + expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 6)); + + // Selecting across line should select to the end. + await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11)); + + await gesture.up(); + }, variant: TargetPlatformVariant.mobile()); + + testWidgetsWithLeakTracking('mouse can select word-by-word on double click drag', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Center( + child: Text('How are you'), + ), + ), + ), + ); + final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph, 2)); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + + await gesture.moveTo(textOffsetToPosition(paragraph, 3)); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); + + await gesture.moveTo(textOffsetToPosition(paragraph, 4)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); + + await gesture.moveTo(textOffsetToPosition(paragraph, 7)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8)); + + await gesture.moveTo(textOffsetToPosition(paragraph, 8)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + + // Check backward selection. + await gesture.moveTo(textOffsetToPosition(paragraph, 1)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + + // Start a new double-click drag. + await gesture.up(); + await tester.pump(); + await gesture.down(textOffsetToPosition(paragraph, 5)); + await tester.pump(); + await gesture.up(); + expect(paragraph.selections.isEmpty, isFalse); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5)); + await tester.pump(kDoubleTapTimeout); + + // Double-click. + await gesture.down(textOffsetToPosition(paragraph, 5)); + await tester.pump(); + await gesture.up(); + await tester.pump(); + await gesture.down(textOffsetToPosition(paragraph, 5)); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); + + // Selecting across line should select to the end. + await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0)); + await tester.pump(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11)); + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('mouse can select multiple widgets on double click drag', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph1, 2)); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + await tester.pump(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + // Should select the rest of paragraph 1. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph3, 6)); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('mouse can select multiple widgets on double click drag and return to origin word', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph1, 2)); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + await tester.pump(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + // Should select the rest of paragraph 1. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph3, 6)); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + + await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + // Should clear the selection on paragraph 3. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + expect(paragraph3.selections.isEmpty, isTrue); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + // Should clear the selection on paragraph 2. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); + expect(paragraph2.selections.isEmpty, isTrue); + expect(paragraph3.selections.isEmpty, isTrue); + + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('mouse can reverse selection across multiple widgets on double click drag', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 10), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph3, 10)); + await tester.pumpAndSettle(); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11)); + + await gesture.moveTo(textOffsetToPosition(paragraph3, 4)); + await tester.pump(); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 4)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5)); + + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph1, 6)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0)); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 4)); + + await gesture.up(); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + + testWidgetsWithLeakTracking('mouse can select multiple widgets', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); - testWidgets('mouse can select multiple widgets', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -556,11 +926,60 @@ void main() { await gesture.up(); }); - testWidgets('mouse can work with disabled container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('collapsing selection should clear selection of all other selectables', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 2)); + + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + await gesture.down(textOffsetToPosition(paragraph2, 5)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections[0], const TextSelection.collapsed(offset: 5)); + + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + await gesture.down(textOffsetToPosition(paragraph3, 13)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(paragraph1.selections.isEmpty, isTrue); + expect(paragraph2.selections.isEmpty, isTrue); + expect(paragraph3.selections[0], const TextSelection.collapsed(offset: 13)); + }); + + testWidgetsWithLeakTracking('mouse can work with disabled container', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -597,11 +1016,14 @@ void main() { await gesture.up(); }); - testWidgets('mouse can reverse selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse can reverse selection', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -633,18 +1055,191 @@ void main() { expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 6)); - await gesture.up(); - }); + await gesture.up(); + }); + + testWidgets( + 'long press selection overlay behavior on iOS and Android', + (WidgetTester tester) async { + // This test verifies that all platforms wait until long press end to + // show the context menu, and only Android waits until long press end to + // show the selection handles. + final bool isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; + Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; + final UniqueKey toolbarKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: FocusNode(), + selectionControls: materialTextSelectionHandleControls, + contextMenuBuilder: ( + BuildContext context, + SelectableRegionState selectableRegionState, + ) { + buttonTypes = selectableRegionState.contextMenuButtonItems + .map((ContextMenuButtonItem buttonItem) => buttonItem.type) + .toSet(); + return SizedBox.shrink(key: toolbarKey); + }, + child: const Text('How are you?'), + ), + ), + ); + + expect(buttonTypes.isEmpty, true); + expect(find.byKey(toolbarKey), findsNothing); + + final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2)); + addTearDown(gesture.removePointer); + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + + // All platform except Android should show the selection handles when the + // long press starts. + List<FadeTransition> transitions = find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), + matching: find.byType(FadeTransition), + ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); + expect(transitions.length, isPlatformAndroid ? 0 : 2); + FadeTransition? left; + FadeTransition? right; + if (!isPlatformAndroid) { + left = transitions[0]; + right = transitions[1]; + expect(left.opacity.value, equals(1.0)); + expect(right.opacity.value, equals(1.0)); + } + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + expect(find.byKey(toolbarKey), findsNothing); + + await gesture.moveTo(textOffsetToPosition(paragraph, 8)); + await tester.pumpAndSettle(); + transitions = find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), + matching: find.byType(FadeTransition), + ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); + // All platform except Android should show the selection handles while doing + // a long press drag. + expect(transitions.length, isPlatformAndroid ? 0 : 2); + if (!isPlatformAndroid) { + left = transitions[0]; + right = transitions[1]; + expect(left.opacity.value, equals(1.0)); + expect(right.opacity.value, equals(1.0)); + } + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + expect(find.byKey(toolbarKey), findsNothing); + + await gesture.up(); + await tester.pumpAndSettle(); + transitions = find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), + matching: find.byType(FadeTransition), + ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); + expect(transitions.length, 2); + left = transitions[0]; + right = transitions[1]; + + // All platforms should show the selection handles and context menu when + // the long press ends. + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + expect(left.opacity.value, equals(1.0)); + expect(right.opacity.value, equals(1.0)); + expect(find.byKey(toolbarKey), findsOneWidget); + }, + variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }), + skip: kIsWeb, // [intended] Web uses its native context menu. + ); + + testWidgetsWithLeakTracking( + 'single tap on the previous selection toggles the toolbar on iOS', + (WidgetTester tester) async { + Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; + final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionHandleControls, + contextMenuBuilder: ( + BuildContext context, + SelectableRegionState selectableRegionState, + ) { + buttonTypes = selectableRegionState.contextMenuButtonItems + .map((ContextMenuButtonItem buttonItem) => buttonItem.type) + .toSet(); + return SizedBox.shrink(key: toolbarKey); + }, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + + expect(buttonTypes.isEmpty, true); + expect(find.byKey(toolbarKey), findsNothing); + + final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2)); + addTearDown(gesture.removePointer); + await tester.pump(const Duration(milliseconds: 500)); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + expect(buttonTypes, contains(ContextMenuButtonType.copy)); + expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); + expect(find.byKey(toolbarKey), findsOneWidget); + + await gesture.down(textOffsetToPosition(paragraph, 2)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + expect(buttonTypes, contains(ContextMenuButtonType.copy)); + expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); + expect(find.byKey(toolbarKey), findsNothing); + + await gesture.down(textOffsetToPosition(paragraph, 2)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + expect(buttonTypes, contains(ContextMenuButtonType.copy)); + expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); + expect(find.byKey(toolbarKey), findsOneWidget); + + // Collapse selection. + await tester.tapAt(textOffsetToPosition(paragraph, 9)); + await tester.pump(); + expect(paragraph.selections.isEmpty, isFalse); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9)); + expect(find.byKey(toolbarKey), findsNothing); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + skip: kIsWeb, // [intended] Web uses its native context menu. + ); - testWidgets( + testWidgetsWithLeakTracking( 'right-click mouse can select word at position on Apple platforms', (WidgetTester tester) async { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -666,7 +1261,9 @@ void main() { expect(find.byKey(toolbarKey), findsNothing); final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); + final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + addTearDown(primaryMouseButtonGesture.removePointer); addTearDown(gesture.removePointer); await tester.pump(); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); @@ -700,25 +1297,32 @@ void main() { expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), skip: kIsWeb, // [intended] Web uses its native context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'right-click mouse at the same position as previous right-click toggles the context menu on macOS', (WidgetTester tester) async { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -741,6 +1345,8 @@ void main() { final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(primaryMouseButtonGesture.removePointer); addTearDown(gesture.removePointer); await tester.pump(); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); @@ -798,25 +1404,32 @@ void main() { expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.macOS), skip: kIsWeb, // [intended] Web uses its native context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'right-click mouse shows the context menu at position on Android, Fucshia, and Windows', (WidgetTester tester) async { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -839,21 +1452,24 @@ void main() { final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(primaryMouseButtonGesture.removePointer); addTearDown(gesture.removePointer); await tester.pump(); - // Selection is collapsed so none is reported. - expect(paragraph.selections.isEmpty, true); - await gesture.up(); await tester.pump(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2)); expect(buttonTypes.length, 1); expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(find.byKey(toolbarKey), findsOneWidget); await gesture.down(textOffsetToPosition(paragraph, 6)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 6)); await gesture.up(); await tester.pump(); @@ -864,7 +1480,8 @@ void main() { await gesture.down(textOffsetToPosition(paragraph, 9)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9)); await gesture.up(); await tester.pump(); @@ -873,20 +1490,23 @@ void main() { expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); // Create an uncollapsed selection by dragging. - final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse); - addTearDown(dragGesture.removePointer); + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0)); await tester.pump(); - await dragGesture.moveTo(textOffsetToPosition(paragraph, 5)); + await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5)); await tester.pump(); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); - await dragGesture.up(); + await primaryMouseButtonGesture.up(); await tester.pump(); // Right click on previous selection should not collapse the selection. @@ -903,28 +1523,36 @@ void main() { await tester.pump(); await gesture.up(); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }), skip: kIsWeb, // [intended] Web uses its native context menu. ); - testWidgets( + testWidgetsWithLeakTracking( 'right-click mouse toggles the context menu on Linux', (WidgetTester tester) async { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -947,13 +1575,15 @@ void main() { final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(primaryMouseButtonGesture.removePointer); addTearDown(gesture.removePointer); await tester.pump(); - // Selection is collapsed so none is reported. - expect(paragraph.selections.isEmpty, true); - await gesture.up(); await tester.pump(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2)); // Context menu toggled on. expect(buttonTypes.length, 1); @@ -962,17 +1592,18 @@ void main() { await gesture.down(textOffsetToPosition(paragraph, 6)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); - await gesture.up(); await tester.pump(); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2)); - // Context menu toggled off. + // Context menu toggled off. Selection remains the same. expect(find.byKey(toolbarKey), findsNothing); await gesture.down(textOffsetToPosition(paragraph, 9)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9)); await gesture.up(); await tester.pump(); @@ -982,19 +1613,22 @@ void main() { expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); - final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse); - addTearDown(dragGesture.removePointer); + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0)); await tester.pump(); - await dragGesture.moveTo(textOffsetToPosition(paragraph, 5)); + await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5)); await tester.pump(); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); - await dragGesture.up(); + await primaryMouseButtonGesture.up(); await tester.pump(); // Right click on previous selection should not collapse the selection. @@ -1020,24 +1654,32 @@ void main() { await tester.pump(); await gesture.up(); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7)); expect(find.byKey(toolbarKey), findsOneWidget); - // Clear selection. - await tester.tapAt(textOffsetToPosition(paragraph, 1)); + // Collapse selection. + await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1)); await tester.pump(); - expect(paragraph.selections.isEmpty, true); + await primaryMouseButtonGesture.up(); + await tester.pumpAndSettle(); + // Selection is collapsed. + expect(paragraph.selections.isEmpty, false); + expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1)); expect(find.byKey(toolbarKey), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.linux), skip: kIsWeb, // [intended] Web uses its native context menu. ); - testWidgets('can copy a selection made with the mouse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can copy a selection made with the mouse', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1066,12 +1708,16 @@ void main() { expect(clipboardData['text'], 'w are you?Good, and you?Fine, '); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia })); - testWidgets( + testWidgetsWithLeakTracking( 'does not override TextField keyboard shortcuts if the TextField is focused - non apple', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'I am fine, thank you.'); + addTearDown(controller.dispose); final FocusNode selectableRegionFocus = FocusNode(); + addTearDown(selectableRegionFocus.dispose); final FocusNode textFieldFocus = FocusNode(); + addTearDown(textFieldFocus.dispose); + await tester.pumpWidget( MaterialApp( home: Material( @@ -1119,12 +1765,16 @@ void main() { skip: kIsWeb, // [intended] the web handles this on its own. ); - testWidgets( + testWidgetsWithLeakTracking( 'does not override TextField keyboard shortcuts if the TextField is focused - apple', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'I am fine, thank you.'); + addTearDown(controller.dispose); final FocusNode selectableRegionFocus = FocusNode(); + addTearDown(selectableRegionFocus.dispose); final FocusNode textFieldFocus = FocusNode(); + addTearDown(textFieldFocus.dispose); + await tester.pumpWidget( MaterialApp( home: Material( @@ -1172,8 +1822,10 @@ void main() { skip: kIsWeb, // [intended] the web handles this on its own. ); - testWidgets('select all', (WidgetTester tester) async { + testWidgetsWithLeakTracking('select all', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( @@ -1203,13 +1855,16 @@ void main() { expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia })); - testWidgets( + testWidgetsWithLeakTracking( 'mouse selection can handle widget span', (WidgetTester tester) async { final UniqueKey outerText = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: Center( child: Text.rich( @@ -1242,15 +1897,17 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/61020 ); - testWidgets( + testWidgetsWithLeakTracking( 'can select word when a selectables rect is completely inside of another selectables rect', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/127076. final UniqueKey outerText = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( - theme: ThemeData(useMaterial3: false), home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: Scaffold( body: Center( @@ -1273,8 +1930,13 @@ void main() { ), ); final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first); + + // Adjust `textOffsetToPosition` result because it returns the wrong vertical position (wrong line). + // TODO(bleroux): Remove when https://github.com/flutter/flutter/issues/133637 is fixed. + final Offset gestureOffset = textOffsetToPosition(paragraph, 125).translate(0, 10); + // Right click to select word at position. - final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 125), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); addTearDown(gesture.removePointer); await tester.pump(); await gesture.up(); @@ -1286,14 +1948,17 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/61020 ); - testWidgets( + testWidgetsWithLeakTracking( 'widget span is ignored if it does not contain text - non Apple', (WidgetTester tester) async { final UniqueKey outerText = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: Center( child: Text.rich( @@ -1326,14 +1991,17 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/61020 ); - testWidgets( + testWidgetsWithLeakTracking( 'widget span is ignored if it does not contain text - Apple', (WidgetTester tester) async { final UniqueKey outerText = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: Center( child: Text.rich( @@ -1366,11 +2034,14 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/61020 ); - testWidgets('mouse can select across bidi text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('mouse can select across bidi text', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1407,11 +2078,14 @@ void main() { await gesture.up(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 - testWidgets('long press and drag touch selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long press and drag touch moves selection word by word', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1432,21 +2106,24 @@ void main() { expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); - await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + await gesture.moveTo(textOffsetToPosition(paragraph2, 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 12)); - expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 9)); await gesture.up(); }); - testWidgets('can drag end handle when not covering entire screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can drag end handle when not covering entire screen', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104620. + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: Column( children: <Widget>[ const Text('How are you?'), SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Text('Good, and you?'), ), @@ -1475,15 +2152,18 @@ void main() { await gesture.up(); }); - testWidgets('can drag start handle when not covering entire screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can drag start handle when not covering entire screen', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/104620. + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: Column( children: <Widget>[ const Text('How are you?'), SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Text('Good, and you?'), ), @@ -1511,11 +2191,14 @@ void main() { await gesture.up(); }); - testWidgets('can drag start selection handle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can drag start selection handle', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1553,11 +2236,14 @@ void main() { await gesture.up(); }); - testWidgets('can drag start selection handle across end selection handle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can drag start selection handle across end selection handle', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1590,11 +2276,14 @@ void main() { await gesture.up(); }); - testWidgets('can drag end selection handle across start selection handle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can drag end selection handle across start selection handle', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1627,11 +2316,14 @@ void main() { await gesture.up(); }); - testWidgets('can select all from toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can select all from toolbar', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1663,11 +2355,14 @@ void main() { expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); }, skip: kIsWeb); // [intended] Web uses its native context menu. - testWidgets('can copy from toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can copy from toolbar', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1703,11 +2398,14 @@ void main() { expect(clipboardData['text'], 'thank'); }, skip: kIsWeb); // [intended] Web uses its native context menu. - testWidgets('can use keyboard to granularly extend selection - character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - character', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1763,11 +2461,14 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('can use keyboard to granularly extend selection - word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - word', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1858,7 +2559,9 @@ void main() { expect(paragraph1.selections.length, 1); expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].end, 12); - expect(paragraph2.selections.length, 0); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 0); await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control)); await tester.pump(); @@ -1868,14 +2571,19 @@ void main() { expect(paragraph1.selections.length, 1); expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].end, 8); - expect(paragraph2.selections.length, 0); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 0); }, variant: TargetPlatformVariant.all()); - testWidgets('can use keyboard to granularly extend selection - line', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - line', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -1948,7 +2656,9 @@ void main() { expect(paragraph1.selections.length, 1); expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].end, 12); - expect(paragraph2.selections.length, 0); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 0); await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta)); await tester.pump(); @@ -1960,11 +2670,14 @@ void main() { expect(paragraph1.selections[0].end, 2); }, variant: TargetPlatformVariant.all()); - testWidgets('can use keyboard to granularly extend selection - document', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - document', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -2032,15 +2745,22 @@ void main() { expect(paragraph1.selections.length, 1); expect(paragraph1.selections[0].start, 0); expect(paragraph1.selections[0].end, 2); - expect(paragraph2.selections.length, 0); - expect(paragraph3.selections.length, 0); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 0); + expect(paragraph3.selections.length, 1); + expect(paragraph3.selections[0].start, 0); + expect(paragraph3.selections[0].end, 0); }, variant: TargetPlatformVariant.all()); - testWidgets('can use keyboard to directionally extend selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can use keyboard to directionally extend selection', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Column( children: <Widget>[ @@ -2101,7 +2821,9 @@ void main() { expect(paragraph2.selections.length, 1); expect(paragraph2.selections[0].start, 2); expect(paragraph2.selections[0].end, 6); - expect(paragraph3.selections.length, 0); + expect(paragraph3.selections.length, 1); + expect(paragraph3.selections[0].start, 0); + expect(paragraph3.selections[0].end, 0); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true)); await tester.pump(); @@ -2133,9 +2855,11 @@ void main() { late ValueNotifier<MagnifierInfo> magnifierInfo; final Widget fakeMagnifier = Container(key: UniqueKey()); - testWidgets('Can drag handles to show, unshow, and update magnifier', + testWidgetsWithLeakTracking('Can drag handles to show, unshow, and update magnifier', (WidgetTester tester) async { const String text = 'Monkeys and rabbits in my soup'; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -2149,7 +2873,7 @@ void main() { return fakeMagnifier; }, ), - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Text(text), ), @@ -2199,13 +2923,15 @@ void main() { }); }); - testWidgets('toolbar is hidden on mobile when orientation changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toolbar is hidden on mobile when orientation changes', (WidgetTester tester) async { addTearDown(tester.view.reset); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Text('How are you?'), ), @@ -2220,6 +2946,9 @@ void main() { // `are` is selected. expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); await tester.pumpAndSettle(); + + await gesture.up(); + await tester.pumpAndSettle(); // Text selection toolbar has appeared. expect(find.text('Copy'), findsOneWidget); @@ -2241,12 +2970,15 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }), ); - testWidgets('the selection behavior when clicking `Copy` item in mobile platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the selection behavior when clicking `Copy` item in mobile platforms', (WidgetTester tester) async { List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[]; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -2295,12 +3027,15 @@ void main() { skip: kIsWeb, // [intended] ); - testWidgets('the handles do not disappear when clicking `Select all` item in mobile platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('the handles do not disappear when clicking `Select all` item in mobile platforms', (WidgetTester tester) async { List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[]; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -2348,12 +3083,15 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android, TargetPlatform.fuchsia }), ); - testWidgets('builds the correct button items', (WidgetTester tester) async { + testWidgetsWithLeakTracking('builds the correct button items', (WidgetTester tester) async { Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{}; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionHandleControls, contextMenuBuilder: ( BuildContext context, @@ -2378,6 +3116,8 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); // `are` is selected. expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); + + await gesture.up(); await tester.pumpAndSettle(); expect(buttonTypes, contains(ContextMenuButtonType.copy)); @@ -2387,14 +3127,16 @@ void main() { skip: kIsWeb, // [intended] ); - testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSelectionChange is called when the selection changes through gestures', (WidgetTester tester) async { SelectedContent? content; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: SelectableRegion( onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent, - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Center( child: Text('How are you'), @@ -2402,26 +3144,273 @@ void main() { ), ), ); + final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); - final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 4), kind: PointerDeviceKind.mouse); + final TestGesture mouseGesture = await tester.startGesture(textOffsetToPosition(paragraph, 4), kind: PointerDeviceKind.mouse); + final TestGesture touchGesture = await tester.createGesture(); + expect(content, isNull); - addTearDown(gesture.removePointer); + addTearDown(mouseGesture.removePointer); + addTearDown(touchGesture.removePointer); await tester.pump(); - await gesture.moveTo(textOffsetToPosition(paragraph, 7)); - await gesture.up(); - await tester.pump(); + // Called on drag. + await mouseGesture.moveTo(textOffsetToPosition(paragraph, 7)); + await tester.pumpAndSettle(); expect(content, isNotNull); expect(content!.plainText, 'are'); + // Updates on drag. + await mouseGesture.moveTo(textOffsetToPosition(paragraph, 10)); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'are yo'); + + // Called on drag end. + await mouseGesture.up(); + await tester.pump(); + expect(content, isNotNull); + expect(content!.plainText, 'are yo'); + // Backwards selection. - await gesture.down(textOffsetToPosition(paragraph, 3)); + await mouseGesture.down(textOffsetToPosition(paragraph, 3)); + await tester.pump(); + await mouseGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(content, isNotNull); + expect(content!.plainText, ''); + + await mouseGesture.down(textOffsetToPosition(paragraph, 3)); + await tester.pump(); + + await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0)); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'How'); + + await mouseGesture.up(); + await tester.pump(); + expect(content, isNotNull); + expect(content!.plainText, 'How'); + + // Called on double tap. + await mouseGesture.down(textOffsetToPosition(paragraph, 6)); + await tester.pump(); + await mouseGesture.up(); + await tester.pump(); + await mouseGesture.down(textOffsetToPosition(paragraph, 6)); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'are'); + await mouseGesture.up(); + await tester.pumpAndSettle(); + + // Called on tap. + await mouseGesture.down(textOffsetToPosition(paragraph, 0)); + await tester.pumpAndSettle(); + await mouseGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(content, isNotNull); + expect(content!.plainText, ''); + + // With touch gestures. + + // Called on long press start. + await touchGesture.down(textOffsetToPosition(paragraph, 0)); + await tester.pumpAndSettle(kLongPressTimeout); + expect(content, isNotNull); + expect(content!.plainText, 'How'); + + // Called on long press update. + await touchGesture.moveTo(textOffsetToPosition(paragraph, 5)); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'How are'); + + // Called on long press end. + await touchGesture.up(); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'How are'); + + // Long press to select 'you'. + await touchGesture.down(textOffsetToPosition(paragraph, 9)); + await tester.pumpAndSettle(kLongPressTimeout); + expect(content, isNotNull); + expect(content!.plainText, 'you'); + await touchGesture.up(); + await tester.pumpAndSettle(); + + // Called while moving selection handles. + final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]); + expect(boxes.length, 1); + final Offset startHandlePos = globalize(boxes[0].toRect().bottomLeft, paragraph); + final Offset endHandlePos = globalize(boxes[0].toRect().bottomRight, paragraph); + final Offset startPos = Offset(textOffsetToPosition(paragraph, 4).dx, startHandlePos.dy); + final Offset endPos = Offset(textOffsetToPosition(paragraph, 6).dx, endHandlePos.dy); + + // Start handle. + await touchGesture.down(startHandlePos); + await touchGesture.moveTo(startPos); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'are you'); + await touchGesture.up(); + await tester.pumpAndSettle(); + + // End handle. + await touchGesture.down(endHandlePos); + await touchGesture.moveTo(endPos); + await tester.pumpAndSettle(); + expect(content, isNotNull); + expect(content!.plainText, 'ar'); + await touchGesture.up(); + await tester.pumpAndSettle(); + }); + + testWidgetsWithLeakTracking('onSelectionChange is called when the selection changes through keyboard actions', (WidgetTester tester) async { + SelectedContent? content; + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent, + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: <Widget>[ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + expect(content, isNull); - await gesture.moveTo(textOffsetToPosition(paragraph, 0)); + await tester.pump(); + + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(textOffsetToPosition(paragraph1, 6)); await gesture.up(); await tester.pump(); + + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 6); expect(content, isNotNull); - expect(content!.plainText, 'How'); + expect(content!.plainText, 'w ar'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 7); + expect(content, isNotNull); + expect(content!.plainText, 'w are'); + + for (int i = 0; i < 5; i += 1) { + await sendKeyCombination(tester, + const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 8 + i); + expect(content, isNotNull); + } + expect(content, isNotNull); + expect(content!.plainText, 'w are you?'); + + for (int i = 0; i < 5; i += 1) { + await sendKeyCombination(tester, + const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 11 - i); + expect(content, isNotNull); + } + expect(content, isNotNull); + expect(content!.plainText, 'w are'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 12); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 8); + expect(content, isNotNull); + expect(content!.plainText, 'w are you?Good, an'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 12); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 14); + expect(paragraph3.selections.length, 1); + expect(paragraph3.selections[0].start, 0); + expect(paragraph3.selections[0].end, 9); + expect(content, isNotNull); + expect(content!.plainText, 'w are you?Good, and you?Fine, tha'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 12); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 14); + expect(paragraph3.selections.length, 1); + expect(paragraph3.selections[0].start, 0); + expect(paragraph3.selections[0].end, 16); + expect(content, isNotNull); + expect(content!.plainText, 'w are you?Good, and you?Fine, thank you.'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 12); + expect(paragraph2.selections.length, 1); + expect(paragraph2.selections[0].start, 0); + expect(paragraph2.selections[0].end, 8); + expect(paragraph3.selections.length, 1); + expect(content, isNotNull); + expect(content!.plainText, 'w are you?Good, an'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 2); + expect(paragraph1.selections[0].end, 7); + expect(paragraph2.selections.length, 1); + expect(paragraph3.selections.length, 1); + expect(content, isNotNull); + expect(content!.plainText, 'w are'); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true)); + await tester.pump(); + expect(paragraph1.selections.length, 1); + expect(paragraph1.selections[0].start, 0); + expect(paragraph1.selections[0].end, 2); + expect(paragraph2.selections.length, 1); + expect(paragraph3.selections.length, 1); + expect(content, isNotNull); + expect(content!.plainText, 'Ho'); }); group('BrowserContextMenu', () { @@ -2439,12 +3428,15 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null); }); - testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + await tester.pumpWidget( MaterialApp( home: SelectableRegion( onSelectionChanged: (SelectedContent? selectedContent) {}, - focusNode: FocusNode(), + focusNode: focusNode, selectionControls: materialTextSelectionControls, child: const Center( child: Text('How are you'), @@ -2469,6 +3461,54 @@ void main() { skip: !kIsWeb, // [intended] ); }); + + testWidgetsWithLeakTracking('Multiple selectables on a single line should be in screen order', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/127942. + final UniqueKey outerText = UniqueKey(); + const TextStyle textStyle = TextStyle(fontSize: 10); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: Scaffold( + body: Center( + child: Text.rich( + const TextSpan( + children: <InlineSpan>[ + TextSpan(text: 'Hello my name is ', style: textStyle), + WidgetSpan( + child: Text('Dash', style: textStyle), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: '.', style: textStyle), + ], + ), + key: outerText, + ), + ), + ), + ), + ), + ); + final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 0), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + + // Select all. + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true)); + + // keyboard copy. + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true)); + + final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>; + expect(clipboardData['text'], 'Hello my name is Dash.'); + }); } class SelectionSpy extends LeafRenderObjectWidget { diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 34099ff2e0510..1de30a19e45c6 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; @@ -55,6 +56,7 @@ Widget overlay({ Widget? child }) { ); }, ); + addTearDown(() => entry..remove()..dispose()); return overlayWithEntry(entry); } @@ -215,7 +217,7 @@ void main() { expect(tester.takeException(), isNotNull); // side effect exception }); - testWidgets('Do not crash when remove SelectableText during handle drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash when remove SelectableText during handle drag', (WidgetTester tester) async { // Regression test https://github.com/flutter/flutter/issues/108242 bool isShow = true; late StateSetter setter; @@ -283,7 +285,7 @@ void main() { await tester.pump(); }); - testWidgets('has expected defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const SelectableText('selectable text'), @@ -299,7 +301,7 @@ void main() { expect(selectableText.enableInteractiveSelection, true); }); - testWidgets('Rich selectable text has expected defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rich selectable text has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), @@ -344,7 +346,7 @@ void main() { expect(selectableText.enableInteractiveSelection, true); }); - testWidgets('Rich selectable text supports WidgetSpan', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rich selectable text supports WidgetSpan', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), @@ -385,7 +387,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('no text keyboard when widget is focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no text keyboard when widget is focused', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('selectable text'), @@ -396,7 +398,7 @@ void main() { expect(tester.testTextInput.hasAnyClients, false); }); - testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { const Color selectionColor = Colors.orange; const Color cursorColor = Colors.red; @@ -417,7 +419,7 @@ void main() { expect(state.widget.cursorColor, cursorColor); }); - testWidgets('Selectable Text has adaptive size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable Text has adaptive size', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const SelectableText('s'), @@ -439,7 +441,7 @@ void main() { expect(longtextBox.size, const Size(199.0, 14.0)); }); - testWidgets('can scale with textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can scale with textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const SelectableText('selectable text'), @@ -462,7 +464,7 @@ void main() { expect(scaledBox.size.height, 27.0); }); - testWidgets('can switch between textWidthBasis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can switch between textWidthBasis', (WidgetTester tester) async { RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); const String text = 'I can face roll keyboardkeyboardaszzaaaaszzaaaaszzaaaaszzaaaa'; await tester.pumpWidget( @@ -488,7 +490,7 @@ void main() { expect(textBox.size, const Size(633.0, 28.0)); }); - testWidgets('can switch between textHeightBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can switch between textHeightBehavior', (WidgetTester tester) async { const String text = 'selectable text'; const TextHeightBehavior textHeightBehavior = TextHeightBehavior( applyHeightToFirstAscent: false, @@ -512,7 +514,7 @@ void main() { expect(findRenderEditable(tester).textHeightBehavior, textHeightBehavior); }); - testWidgets('Cursor blinks when showCursor is true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cursor blinks when showCursor is true', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -540,7 +542,7 @@ void main() { expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); }); - testWidgets('selectable text selection toolbar renders correctly inside opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selectable text selection toolbar renders correctly inside opacity', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -571,7 +573,7 @@ void main() { expect(find.text('Select all'), findsOneWidget); }); - testWidgets('Caret position is updated on tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret position is updated on tap', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('abc def ghi'), @@ -591,7 +593,7 @@ void main() { expect(editableText.controller.selection.extentOffset, tapIndex); }); - testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('enableInteractiveSelection = false, tap', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -614,7 +616,7 @@ void main() { expect(editableText.controller.selection.extentOffset, -1); }); - testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -639,7 +641,7 @@ void main() { expect(editableText.controller.selection.extentOffset, -1); }); - testWidgets('Can long press to select', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can long press to select', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('abc def ghi'), @@ -668,7 +670,7 @@ void main() { expect(editableText.controller.selection.baseOffset, 9); }); - testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('abc def ghi'), @@ -695,7 +697,7 @@ void main() { expect(handle.opacity.value, equals(1.0)); }); - testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse long press is just like a tap', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('abc def ghi'), @@ -717,7 +719,7 @@ void main() { expect(editableText.controller.selection.extentOffset, eIndex); }); - testWidgets('selectable text basic', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selectable text basic', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText('selectable'), @@ -749,7 +751,7 @@ void main() { expect(find.text('Cut'), findsNothing); }); - testWidgets('selectable text can disable toolbar options', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selectable text can disable toolbar options', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -769,7 +771,7 @@ void main() { expect(find.text('Select all'), findsOneWidget); }); - testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can select text by dragging with a mouse', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -797,7 +799,7 @@ void main() { expect(controller.selection.extentOffset, 8); }); - testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Continuous dragging does not cause flickering', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -849,7 +851,7 @@ void main() { expect(controller.selection.extentOffset, 9); }); - testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dragging in opposite direction also works', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -877,7 +879,7 @@ void main() { expect(controller.selection.extentOffset, 5); }); - testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slow mouse dragging also selects text', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -904,7 +906,7 @@ void main() { expect(controller.selection.extentOffset,8); }); - testWidgets('Can drag handles to change selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can drag handles to change selection', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -966,7 +968,7 @@ void main() { expect(controller.selection.extentOffset, 11); }); - testWidgets('Dragging handles calls onSelectionChanged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Dragging handles calls onSelectionChanged', (WidgetTester tester) async { TextSelection? newSelection; await tester.pumpWidget( MaterialApp( @@ -1018,7 +1020,7 @@ void main() { expect(newSelection!.extentOffset, 9); }); - testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Cannot drag one handle past the other', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -1076,7 +1078,7 @@ void main() { expect(controller.selection.extentOffset, 5); }); - testWidgets('Can use selection toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can use selection toolbar', (WidgetTester tester) async { const String testValue = 'abc def ghi'; await tester.pumpWidget( const MaterialApp( @@ -1118,7 +1120,7 @@ void main() { expect(controller.selection.isCollapsed, true); }); - testWidgets('Selectable height with maxLine', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable height with maxLine', (WidgetTester tester) async { await tester.pumpWidget(selectableTextBuilder()); RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); @@ -1170,7 +1172,7 @@ void main() { expect(textBox.size.height, greaterThan(fourLineInputSize.height)); }); - testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can drag handles to change selection in multiline', (WidgetTester tester) async { const String testValue = kThreeLines; await tester.pumpWidget( overlay( @@ -1258,7 +1260,7 @@ void main() { expect(controller.selection.isCollapsed, true); }); - testWidgets('Can scroll multiline input', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can scroll multiline input', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -1352,7 +1354,7 @@ void main() { expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); }); - testWidgets('minLines cannot be greater than maxLines', (WidgetTester tester) async { + testWidgetsWithLeakTracking('minLines cannot be greater than maxLines', (WidgetTester tester) async { expect( () async { await tester.pumpWidget( @@ -1376,7 +1378,7 @@ void main() { ); }); - testWidgets('Selectable height with minLine', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable height with minLine', (WidgetTester tester) async { await tester.pumpWidget(selectableTextBuilder()); RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); @@ -1390,7 +1392,7 @@ void main() { expect(textBox.size.height, emptyInputSize.height * 2); }); - testWidgets('Can align to center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align to center', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -1412,7 +1414,7 @@ void main() { expect(topLeft.dx, equals(399.0)); }); - testWidgets('Can align to center within center', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align to center within center', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -1436,9 +1438,11 @@ void main() { expect(topLeft.dx, equals(399.0)); }); - testWidgets('Selectable text is skipped during focus traversal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable text is skipped during focus traversal', (WidgetTester tester) async { final FocusNode firstFieldFocus = FocusNode(); + addTearDown(firstFieldFocus.dispose); final FocusNode lastFieldFocus = FocusNode(); + addTearDown(lastFieldFocus.dispose); await tester.pumpWidget( MaterialApp( @@ -1474,7 +1478,7 @@ void main() { expect(lastFieldFocus.hasFocus, isTrue); }); - testWidgets('Selectable text identifies as text field in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable text identifies as text field in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1501,7 +1505,7 @@ void main() { semantics.dispose(); }); - testWidgets('Selectable text rich text with spell out in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable text rich text with spell out in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1534,7 +1538,7 @@ void main() { semantics.dispose(); }); - testWidgets('Selectable text rich text with locale in semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable text rich text with locale in semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -1567,7 +1571,7 @@ void main() { semantics.dispose(); }); - testWidgets('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( overlay( @@ -1623,6 +1627,7 @@ void main() { Future<void> setupWidget(WidgetTester tester, String text) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -1642,7 +1647,7 @@ void main() { controller = editableTextWidget.controller; } - testWidgets('Shift test 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shift test 1', (WidgetTester tester) async { await setupWidget(tester, 'a big house'); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); @@ -1650,7 +1655,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Shift test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shift test 2', (WidgetTester tester) async { await setupWidget(tester, 'abcdefghi'); controller.selection = const TextSelection.collapsed(offset: 3); @@ -1662,7 +1667,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Control Shift test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Control Shift test', (WidgetTester tester) async { await setupWidget(tester, 'their big house'); await tester.sendKeyDownEvent(LogicalKeyboardKey.control); @@ -1674,7 +1679,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Down and up test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Down and up test', (WidgetTester tester) async { await setupWidget(tester, 'a big house'); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); @@ -1692,7 +1697,7 @@ void main() { expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Down and up test 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Down and up test 2', (WidgetTester tester) async { await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay'); controller.selection = const TextSelection.collapsed(offset: 0); @@ -1744,8 +1749,9 @@ void main() { }, variant: KeySimulatorTransitModeVariant.all()); }); - testWidgets('Copy test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Copy test', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); String clipboardContent = ''; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { @@ -1802,8 +1808,9 @@ void main() { await tester.pumpAndSettle(); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Select all test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Select all test', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); const String testValue = 'a big house\njumped over a mouse'; await tester.pumpWidget( MaterialApp( @@ -1836,8 +1843,9 @@ void main() { expect(controller.selection.extentOffset, 31); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboard selection should call onSelectionChanged', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); TextSelection? newSelection; const String testValue = 'a big house\njumped over a mouse'; await tester.pumpWidget( @@ -1882,8 +1890,9 @@ void main() { } }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Changing positions of selectable text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing positions of selectable text', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<RawKeyEvent> events = <RawKeyEvent>[]; final Key key1 = UniqueKey(); @@ -1972,8 +1981,9 @@ void main() { expect(c1.selection.extentOffset - c1.selection.baseOffset, -10); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Changing focus test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing focus test', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final List<RawKeyEvent> events = <RawKeyEvent>[]; final Key key1 = UniqueKey(); @@ -2041,7 +2051,7 @@ void main() { expect(c2.selection.extentOffset - c2.selection.baseOffset, -5); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('Caret works when maxLines is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret works when maxLines is null', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SelectableText( @@ -2064,7 +2074,7 @@ void main() { expect(controller.selection.baseOffset, 0); }); - testWidgets('SelectableText baseline alignment no-strut', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText baseline alignment no-strut', (WidgetTester tester) async { final Key keyA = UniqueKey(); final Key keyB = UniqueKey(); @@ -2113,7 +2123,7 @@ void main() { expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); }); - testWidgets('SelectableText baseline alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText baseline alignment', (WidgetTester tester) async { final Key keyA = UniqueKey(); final Key keyB = UniqueKey(); @@ -2160,7 +2170,7 @@ void main() { expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); }); - testWidgets('SelectableText semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); @@ -2280,7 +2290,7 @@ void main() { semantics.dispose(); }); - testWidgets('SelectableText semantics, with semanticsLabel', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText semantics, with semanticsLabel', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); @@ -2307,7 +2317,7 @@ void main() { semantics.dispose(); }); - testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); @@ -2353,7 +2363,7 @@ void main() { semantics.dispose(); }); - testWidgets('SelectableText semantics for selections', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText semantics for selections', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); @@ -2449,13 +2459,15 @@ void main() { semantics.dispose(); }); - testWidgets('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/100395. final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(fontSize: 200); const String onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'; const String offScreenText = 'off screen'; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: SingleChildScrollView( @@ -2542,7 +2554,7 @@ void main() { semantics.dispose(); }); - testWidgets('SelectableText change selection with semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText change selection with semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; final Key key = UniqueKey(); @@ -2641,7 +2653,7 @@ void main() { semantics.dispose(); }); - testWidgets('Can activate SelectableText with explicit controller via semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can activate SelectableText with explicit controller via semantics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17801 const String testValue = 'Hello'; @@ -2715,7 +2727,7 @@ void main() { semantics.dispose(); }); - testWidgets('onTap is called upon tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTap is called upon tap', (WidgetTester tester) async { int tapCount = 0; await tester.pumpWidget( overlay( @@ -2739,7 +2751,7 @@ void main() { expect(tapCount, 3); }); - testWidgets('SelectableText style is merged with default text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText style is merged with default text style', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/23994 final TextStyle defaultStyle = TextStyle( color: Colors.blue[500], @@ -2786,7 +2798,7 @@ void main() { expect(editableText.style.color, isNull); }); - testWidgets('style enforces required fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('style enforces required fields', (WidgetTester tester) async { Widget buildFrame(TextStyle style) { return MaterialApp( home: Material( @@ -2818,7 +2830,7 @@ void main() { expect(tester.takeException(), isNotNull); }); - testWidgets( + testWidgetsWithLeakTracking( 'tap moves cursor to the edge of the word it tapped', (WidgetTester tester) async { await tester.pumpWidget( @@ -2850,7 +2862,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'tap moves cursor to the position tapped (Android)', (WidgetTester tester) async { await tester.pumpWidget( @@ -2882,7 +2894,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'two slow taps do not trigger a word selection', (WidgetTester tester) async { await tester.pumpWidget( @@ -2917,7 +2929,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap selects word and first tap of double tap moves cursor', (WidgetTester tester) async { await tester.pumpWidget( @@ -2930,6 +2942,7 @@ void main() { ), ); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); // This tap just puts the cursor somewhere different than where the double @@ -2957,13 +2970,13 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); - // Selected text shows 1 toolbar buttons. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + // Selected text shows 1 toolbar buttons on MacOS, 2 on iOS. + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3008,7 +3021,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap on top of cursor also selects word (Android)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3057,7 +3070,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap hold selects word', (WidgetTester tester) async { await tester.pumpWidget( @@ -3071,6 +3084,7 @@ void main() { ); final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3087,8 +3101,8 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); - // Selected text shows 1 toolbar buttons. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + // Selected text shows 2 toolbar buttons for iOS, 1 for macOS. + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); await gesture.up(); await tester.pump(); @@ -3099,12 +3113,12 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); // The toolbar is still showing. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap selects word with semantics label', (WidgetTester tester) async { await tester.pumpWidget( @@ -3137,7 +3151,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'tap after a double tap select is not affected (iOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3184,7 +3198,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press selects word and shows toolbar (iOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3198,6 +3212,7 @@ void main() { ); final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); await tester.pump(); @@ -3215,12 +3230,12 @@ void main() { ); // Toolbar shows one button. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press selects word and shows toolbar (Android)', (WidgetTester tester) async { @@ -3252,7 +3267,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'long press selects word and shows custom toolbar (Android)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3290,7 +3305,7 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press selects word and shows custom toolbar (iOS)', (WidgetTester tester) async { @@ -3325,7 +3340,7 @@ void main() { variant: TargetPlatformVariant.all(), ); - testWidgets( + testWidgetsWithLeakTracking( 'textSelectionControls is passed to EditableText', (WidgetTester tester) async { await tester.pumpWidget( @@ -3345,7 +3360,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'long press tap cannot initiate a double tap', (WidgetTester tester) async { await tester.pumpWidget( @@ -3387,7 +3402,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', (WidgetTester tester) async { await tester.pumpWidget( @@ -3463,7 +3478,7 @@ void main() { variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag extends the selection to the word under the drag and shows toolbar on lift (iOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3480,6 +3495,7 @@ void main() { await tester.startGesture(textOffsetToPosition(tester, 18)); await tester.pump(const Duration(milliseconds: 500)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first); final TextEditingController controller = editableTextWidget.controller; @@ -3550,12 +3566,12 @@ void main() { ), ); // The toolbar now shows up. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3633,7 +3649,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), ); - testWidgets('long press drag can edge scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('long press drag can edge scroll', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -3737,7 +3753,7 @@ void main() { skip: true, // https://github.com/flutter/flutter/issues/64059 ); - testWidgets( + testWidgetsWithLeakTracking( 'long tap still selects after a double tap select (iOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3778,12 +3794,12 @@ void main() { ); // Long press toolbar. - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), findsNWidgets(4)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'long tap still selects after a double tap select (macOS)', (WidgetTester tester) async { await tester.pumpWidget( @@ -3827,7 +3843,7 @@ void main() { variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), ); //convert - testWidgets( + testWidgetsWithLeakTracking( 'double tap after a long tap is not affected', (WidgetTester tester) async { await tester.pumpWidget( @@ -3841,6 +3857,7 @@ void main() { ); final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3870,11 +3887,12 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); - expect(find.byType(CupertinoButton), findsNWidgets(1)); + + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets( + testWidgetsWithLeakTracking( 'double tap chains work', (WidgetTester tester) async { await tester.pumpWidget( @@ -3887,6 +3905,7 @@ void main() { ), ); final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); + final bool isTargetPlatformIOS = defaultTargetPlatform == TargetPlatform.iOS; await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); @@ -3904,7 +3923,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); // Double tap selecting the same word somewhere else is fine. await tester.pumpAndSettle(kDoubleTapTimeout); @@ -3921,7 +3940,7 @@ void main() { controller.selection, const TextSelection(baseOffset: 0, extentOffset: 7), ); - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); // Hide the toolbar so it doesn't interfere with taps on the text. final EditableTextState editableTextState = @@ -3943,12 +3962,12 @@ void main() { controller.selection, const TextSelection(baseOffset: 8, extentOffset: 12), ); - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), isTargetPlatformIOS ? findsNWidgets(4) : findsNWidgets(1)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('force press does not select a word on (android)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('force press does not select a word on (android)', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -3984,7 +4003,7 @@ void main() { expect(find.byType(TextButton), findsNothing); }); - testWidgets('force press selects word', (WidgetTester tester) async { + testWidgetsWithLeakTracking('force press selects word', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4022,10 +4041,10 @@ void main() { await gesture.up(); await tester.pump(); - expect(find.byType(CupertinoButton), findsNWidgets(1)); + expect(find.byType(CupertinoButton), findsNWidgets(4)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); - testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { + testWidgetsWithLeakTracking('tap on non-force-press-supported devices work', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4073,7 +4092,7 @@ void main() { // https://github.com/flutter/flutter/issues/43445 }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); - testWidgets('default SelectableText debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default SelectableText debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SelectableText('something').debugFillProperties(builder); @@ -4085,7 +4104,7 @@ void main() { expect(description, <String>['data: something']); }); - testWidgets('SelectableText implements debugFillProperties', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); // Not checking controller, inputFormatters, focusNode @@ -4094,7 +4113,7 @@ void main() { style: TextStyle(color: Color(0xff00ff00)), textAlign: TextAlign.end, textDirection: TextDirection.ltr, - textScaleFactor: 1.0, + textScaler: TextScaler.noScaling, autofocus: true, showCursor: true, minLines: 2, @@ -4122,7 +4141,7 @@ void main() { 'maxLines: 10', 'textAlign: end', 'textDirection: ltr', - 'textScaleFactor: 1.0', + 'textScaler: no scaling', 'cursorWidth: 1.0', 'cursorHeight: 1.0', 'cursorRadius: Radius.circular(0.0)', @@ -4132,7 +4151,7 @@ void main() { ]); }); - testWidgets( + testWidgetsWithLeakTracking( 'strut basic single line', (WidgetTester tester) async { await tester.pumpWidget( @@ -4155,7 +4174,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut TextStyle increases height', (WidgetTester tester) async { await tester.pumpWidget( @@ -4202,7 +4221,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut basic multi line', (WidgetTester tester) async { await tester.pumpWidget( @@ -4226,7 +4245,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut no force small strut', (WidgetTester tester) async { await tester.pumpWidget( @@ -4258,7 +4277,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut no force large strut', (WidgetTester tester) async { await tester.pumpWidget( @@ -4287,7 +4306,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut height override', (WidgetTester tester) async { await tester.pumpWidget( @@ -4316,7 +4335,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'strut forces field taller', (WidgetTester tester) async { await tester.pumpWidget( @@ -4347,7 +4366,7 @@ void main() { }, ); - testWidgets('Caret center position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret center position', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -4383,7 +4402,7 @@ void main() { expect(topLeft.dx, equals(385)); }); - testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { await tester.pumpWidget( overlay( child: const SizedBox( @@ -4429,7 +4448,7 @@ void main() { expect(topLeft.dx, equals(385)); }); - testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection handles are rendered and not faded away', (WidgetTester tester) async { const String testText = 'lorem ipsum'; await tester.pumpWidget( const MaterialApp( @@ -4459,7 +4478,7 @@ void main() { expect(right.opacity.value, equals(1.0)); }); - testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection handles are rendered and not faded away', (WidgetTester tester) async { const String testText = 'lorem ipsum'; await tester.pumpWidget( @@ -4487,7 +4506,7 @@ void main() { expect(right.opacity.value, equals(1.0)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Long press shows handles and toolbar', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4506,7 +4525,7 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue); }); - testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Double tap shows handles and toolbar', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4527,7 +4546,7 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse tap does not show handles nor toolbar', (WidgetTester tester) async { await tester.pumpWidget( @@ -4555,7 +4574,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse long press does not show handles nor toolbar', (WidgetTester tester) async { await tester.pumpWidget( @@ -4583,7 +4602,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'Mouse double tap does not show handles nor toolbar', (WidgetTester tester) async { await tester.pumpWidget( @@ -4615,7 +4634,7 @@ void main() { }, ); - testWidgets('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async { int spyTaps = 0; final TapGestureRecognizer spyRecognizer = TapGestureRecognizer() ..onTap = () { @@ -4665,7 +4684,7 @@ void main() { expect(spyTaps, 1); }); - testWidgets('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async { int spyLongPress = 0; final LongPressGestureRecognizer spyRecognizer = LongPressGestureRecognizer() ..onLongPress = () { @@ -4716,7 +4735,7 @@ void main() { expect(spyLongPress, 1); }); - testWidgets('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4735,7 +4754,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('The handles show after pressing Select All', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The handles show after pressing Select All', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4772,7 +4791,7 @@ void main() { }), ); - testWidgets('The Select All calls on selection changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The Select All calls on selection changed', (WidgetTester tester) async { TextSelection? newSelection; await tester.pumpWidget( MaterialApp( @@ -4808,7 +4827,7 @@ void main() { }), ); - testWidgets('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async { + testWidgetsWithLeakTracking('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async { const String string = 'abc def ghi'; TextSelection? newSelection; await tester.pumpWidget( @@ -4850,7 +4869,7 @@ void main() { }), ); - testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not show handles when updated from the web engine', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( @@ -4890,7 +4909,7 @@ void main() { } }); - testWidgets('onSelectionChanged is called when selection changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onSelectionChanged is called when selection changes', (WidgetTester tester) async { int onSelectionChangedCallCount = 0; await tester.pumpWidget( @@ -4920,7 +4939,7 @@ void main() { expect(onSelectionChangedCallCount, equals(3)); }); - testWidgets('selecting a space selects the previous word on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selecting a space selects the previous word on mobile', (WidgetTester tester) async { TextSelection? selection; await tester.pumpWidget( @@ -5021,7 +5040,7 @@ void main() { expect(selection!.extentOffset, 1); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia })); - testWidgets('double tapping a space selects the previous word on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('double tapping a space selects the previous word on mobile', (WidgetTester tester) async { TextSelection? selection; await tester.pumpWidget( @@ -5086,7 +5105,7 @@ void main() { expect(selection!.extentOffset, 14); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); - testWidgets('text selection style 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text selection style 1', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -5139,7 +5158,7 @@ void main() { ); }); - testWidgets('text selection style 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('text selection style 2', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -5191,7 +5210,7 @@ void main() { ); }); - testWidgets('keeps alive when has focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keeps alive when has focus', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -5283,7 +5302,7 @@ void main() { late ValueNotifier<MagnifierInfo> magnifierInfo; final Widget fakeMagnifier = Container(key: UniqueKey()); - testWidgets( + testWidgetsWithLeakTracking( 'Can drag handles to show, unshow, and update magnifier', (WidgetTester tester) async { const String testValue = 'abc def ghi'; @@ -5352,7 +5371,7 @@ void main() { }); }); - testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText text span style is merged with default text style', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/71389 const TextStyle textStyle = TextStyle(color: Color(0xff00ff00), fontSize: 12.0); @@ -5373,7 +5392,7 @@ void main() { expect(editableText.style.fontSize, textStyle.fontSize); }); - testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SelectableText text span style is merged with default text style', (WidgetTester tester) async { TextSelection? selection; int count = 0; @@ -5413,7 +5432,7 @@ void main() { expect(count, 1); // The `onSelectionChanged` will not be triggered. }); - testWidgets("Off-screen selected text doesn't throw exception", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Off-screen selected text doesn't throw exception", (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/123527 TextSelection? selection; diff --git a/packages/flutter/test/widgets/selection_container_test.dart b/packages/flutter/test/widgets/selection_container_test.dart index 3c7648c9209c7..c6ceebf778106 100644 --- a/packages/flutter/test/widgets/selection_container_test.dart +++ b/packages/flutter/test/widgets/selection_container_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { @@ -20,9 +21,11 @@ void main() { ); } - testWidgets('updates its registrar and delegate based on the number of selectables', (WidgetTester tester) async { + testWidgetsWithLeakTracking('updates its registrar and delegate based on the number of selectables', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -42,9 +45,11 @@ void main() { expect(delegate.selectables.length, 3); }); - testWidgets('disabled container', (WidgetTester tester) async { + testWidgetsWithLeakTracking('disabled container', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -65,10 +70,13 @@ void main() { expect(delegate.selectables.length, 0); }); - testWidgets('Swapping out container delegate does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Swapping out container delegate does not crash', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); final TestContainerDelegate childDelegate = TestContainerDelegate(); + addTearDown(childDelegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -90,6 +98,8 @@ void main() { expect(delegate.value.hasContent, isTrue); final TestContainerDelegate newDelegate = TestContainerDelegate(); + addTearDown(newDelegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -112,10 +122,13 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Can update within one frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can update within one frame', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); final TestContainerDelegate childDelegate = TestContainerDelegate(); + addTearDown(childDelegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -139,9 +152,11 @@ void main() { expect(delegate.value.hasContent, isTrue); }); - testWidgets('selection container registers itself if there is a selectable child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection container registers itself if there is a selectable child', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); + await pumpContainer( tester, SelectionContainer( @@ -181,9 +196,10 @@ void main() { expect(registrar.selectables.length, 0); }); - testWidgets('selection container gets registrar from context if not provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('selection container gets registrar from context if not provided', (WidgetTester tester) async { final TestSelectionRegistrar registrar = TestSelectionRegistrar(); final TestContainerDelegate delegate = TestContainerDelegate(); + addTearDown(delegate.dispose); await pumpContainer( tester, diff --git a/packages/flutter/test/widgets/semantics_10_test.dart b/packages/flutter/test/widgets/semantics_10_test.dart index 0ef416efd64bf..d550253e9bbb9 100644 --- a/packages/flutter/test/widgets/semantics_10_test.dart +++ b/packages/flutter/test/widgets/semantics_10_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('can cease to be semantics boundary after markNeedsSemanticsUpdate() has already been called once', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can cease to be semantics boundary after markNeedsSemanticsUpdate() has already been called once', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_11_test.dart b/packages/flutter/test/widgets/semantics_11_test.dart index 7979f319d6075..05b1d572eda8f 100644 --- a/packages/flutter/test/widgets/semantics_11_test.dart +++ b/packages/flutter/test/widgets/semantics_11_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('markNeedsSemanticsUpdate() called on non-boundary with non-boundary parent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('markNeedsSemanticsUpdate() called on non-boundary with non-boundary parent', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_1_test.dart b/packages/flutter/test/widgets/semantics_1_test.dart index 7d475627942f2..3631f73d2b2e3 100644 --- a/packages/flutter/test/widgets/semantics_1_test.dart +++ b/packages/flutter/test/widgets/semantics_1_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 1', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // smoketest diff --git a/packages/flutter/test/widgets/semantics_2_test.dart b/packages/flutter/test/widgets/semantics_2_test.dart index 2a0c69bf357fe..e56248d3d827a 100644 --- a/packages/flutter/test/widgets/semantics_2_test.dart +++ b/packages/flutter/test/widgets/semantics_2_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // this test is the same as the test in Semantics 1, but diff --git a/packages/flutter/test/widgets/semantics_3_test.dart b/packages/flutter/test/widgets/semantics_3_test.dart index 32c1ac218b13d..11757f5d5fda5 100644 --- a/packages/flutter/test/widgets/semantics_3_test.dart +++ b/packages/flutter/test/widgets/semantics_3_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 3', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // implicit annotators diff --git a/packages/flutter/test/widgets/semantics_4_test.dart b/packages/flutter/test/widgets/semantics_4_test.dart index 084e1d30a20f6..34ee195838e6b 100644 --- a/packages/flutter/test/widgets/semantics_4_test.dart +++ b/packages/flutter/test/widgets/semantics_4_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 4', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 4', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // O diff --git a/packages/flutter/test/widgets/semantics_5_test.dart b/packages/flutter/test/widgets/semantics_5_test.dart index 16859cb921c84..160c54303d334 100644 --- a/packages/flutter/test/widgets/semantics_5_test.dart +++ b/packages/flutter/test/widgets/semantics_5_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 5', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 5', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_6_test.dart b/packages/flutter/test/widgets/semantics_6_test.dart index 30c126126b726..88bd11fc1fb3c 100644 --- a/packages/flutter/test/widgets/semantics_6_test.dart +++ b/packages/flutter/test/widgets/semantics_6_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('can change semantics in a branch blocked by BlockSemantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can change semantics in a branch blocked by BlockSemantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final TestSemantics expectedSemantics = TestSemantics.root( diff --git a/packages/flutter/test/widgets/semantics_7_test.dart b/packages/flutter/test/widgets/semantics_7_test.dart index 2ad88a3c446bf..3f9af1cad843d 100644 --- a/packages/flutter/test/widgets/semantics_7_test.dart +++ b/packages/flutter/test/widgets/semantics_7_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 7 - Merging', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 7 - Merging', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); String label; diff --git a/packages/flutter/test/widgets/semantics_8_test.dart b/packages/flutter/test/widgets/semantics_8_test.dart index 4fe5cd885f1b2..ffea309d169df 100644 --- a/packages/flutter/test/widgets/semantics_8_test.dart +++ b/packages/flutter/test/widgets/semantics_8_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics 8 - Merging with reset', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_9_test.dart b/packages/flutter/test/widgets/semantics_9_test.dart index 9152a13fce0e2..9d2ac510ef4ef 100644 --- a/packages/flutter/test/widgets/semantics_9_test.dart +++ b/packages/flutter/test/widgets/semantics_9_test.dart @@ -6,12 +6,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group('BlockSemantics', () { - testWidgets('hides semantic nodes of siblings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hides semantic nodes of siblings', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Stack( @@ -49,7 +50,7 @@ void main() { semantics.dispose(); }); - testWidgets('does not hides semantic nodes of siblings outside the current semantic boundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not hides semantic nodes of siblings outside the current semantic boundary', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr, child: Stack( @@ -103,7 +104,7 @@ void main() { semantics.dispose(); }); - testWidgets('node is semantic boundary and blocking previously painted nodes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('node is semantic boundary and blocking previously painted nodes', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey stackKey = GlobalKey(); diff --git a/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart index a562a53891d6f..b2e67726b1cae 100644 --- a/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart +++ b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics can merge sibling group', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics can merge sibling group', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); @@ -74,7 +75,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics can drop semantics config', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics can drop semantics config', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); @@ -132,7 +133,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async { const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); @@ -178,7 +179,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async { const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); @@ -224,7 +225,7 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('RenderObject with semantics child delegate will mark correct boundary dirty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderObject with semantics child delegate will mark correct boundary dirty', (WidgetTester tester) async { final UniqueKey inner = UniqueKey(); final UniqueKey boundaryParent = UniqueKey(); final UniqueKey grandBoundaryParent = UniqueKey(); diff --git a/packages/flutter/test/widgets/semantics_clipping_test.dart b/packages/flutter/test/widgets/semantics_clipping_test.dart index 73554422dceaf..8b63e86a08d25 100644 --- a/packages/flutter/test/widgets/semantics_clipping_test.dart +++ b/packages/flutter/test/widgets/semantics_clipping_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('SemanticNode.rect is clipped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticNode.rect is clipped', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const Directionality( @@ -67,7 +68,7 @@ void main() { semantics.dispose(); }); - testWidgets('SemanticsNode is not removed if out of bounds and merged into something within bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNode is not removed if out of bounds and merged into something within bounds', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const Directionality( diff --git a/packages/flutter/test/widgets/semantics_debugger_test.dart b/packages/flutter/test/widgets/semantics_debugger_test.dart index d5985b933d964..4c48e69664591 100644 --- a/packages/flutter/test/widgets/semantics_debugger_test.dart +++ b/packages/flutter/test/widgets/semantics_debugger_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SemanticsDebugger will schedule a frame', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger will schedule a frame', (WidgetTester tester) async { await tester.pumpWidget( SemanticsDebugger( child: Container(), @@ -17,7 +18,7 @@ void main() { expect(tester.binding.hasScheduledFrame, isTrue); }); - testWidgets('SemanticsDebugger smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger smoke test', (WidgetTester tester) async { // This is a smoketest to verify that adding a debugger doesn't crash. await tester.pumpWidget( @@ -61,7 +62,7 @@ void main() { expect(true, isTrue); // expect that we reach here without crashing }); - testWidgets('SemanticsDebugger reparents subtree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger reparents subtree', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -147,7 +148,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('SemanticsDebugger interaction test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger interaction test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -185,7 +186,7 @@ void main() { log.clear(); }); - testWidgets('SemanticsDebugger interaction test - negative', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger interaction test - negative', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( @@ -225,7 +226,7 @@ void main() { log.clear(); }); - testWidgets('SemanticsDebugger scroll test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger scroll test', (WidgetTester tester) async { final Key childKey = UniqueKey(); await tester.pumpWidget( @@ -268,7 +269,7 @@ void main() { expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); }); - testWidgets('SemanticsDebugger long press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger long press', (WidgetTester tester) async { bool didLongPress = false; await tester.pumpWidget( @@ -290,7 +291,7 @@ void main() { expect(didLongPress, isTrue); }); - testWidgets('SemanticsDebugger slider', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger slider', (WidgetTester tester) async { double value = 0.75; await tester.pumpWidget( @@ -337,7 +338,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('SemanticsDebugger checkbox', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger checkbox', (WidgetTester tester) async { final Key keyTop = UniqueKey(); final Key keyBottom = UniqueKey(); @@ -378,7 +379,7 @@ void main() { expect(valueTop, isFalse); }); - testWidgets('SemanticsDebugger checkbox message', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger checkbox message', (WidgetTester tester) async { final Key checkbox = UniqueKey(); final Key checkboxUnchecked = UniqueKey(); final Key checkboxDisabled = UniqueKey(); @@ -450,7 +451,7 @@ void main() { ); }); - testWidgets('SemanticsDebugger ignores duplicated label and tooltip for Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger ignores duplicated label and tooltip for Android', (WidgetTester tester) async { final Key child = UniqueKey(); final Key debugger = UniqueKey(); final bool isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; @@ -477,7 +478,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('SemanticsDebugger textfield', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger textfield', (WidgetTester tester) async { final UniqueKey textField = UniqueKey(); final UniqueKey debugger = UniqueKey(); @@ -504,7 +505,7 @@ void main() { ); }); - testWidgets('SemanticsDebugger label style is used in the painter.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsDebugger label style is used in the painter.', (WidgetTester tester) async { final UniqueKey debugger = UniqueKey(); const TextStyle labelStyle = TextStyle(color: Colors.amber); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_keep_alive_offstage_test.dart b/packages/flutter/test/widgets/semantics_keep_alive_offstage_test.dart index 8cffc4eb5f1d7..915b7fc6c6485 100644 --- a/packages/flutter/test/widgets/semantics_keep_alive_offstage_test.dart +++ b/packages/flutter/test/widgets/semantics_keep_alive_offstage_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Un-layouted RenderObject in keep alive offstage area do not crash semantics compiler', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Un-layouted RenderObject in keep alive offstage area do not crash semantics compiler', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/20313. final SemanticsTester semantics = SemanticsTester(tester); @@ -18,6 +19,7 @@ void main() { const double bottomScrollOffset = 3000.0; final ScrollController controller = ScrollController(initialScrollOffset: bottomScrollOffset); + addTearDown(controller.dispose); await tester.pumpWidget(_buildTestWidget( extraPadding: false, diff --git a/packages/flutter/test/widgets/semantics_merge_test.dart b/packages/flutter/test/widgets/semantics_merge_test.dart index a4a997d37b91e..82d3c8157aa4b 100644 --- a/packages/flutter/test/widgets/semantics_merge_test.dart +++ b/packages/flutter/test/widgets/semantics_merge_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -14,7 +15,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('MergeSemantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeSemantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // not merged @@ -120,7 +121,7 @@ void main() { semantics.dispose(); }); - testWidgets('MergeSemantics works if other nodes are implicitly merged into its node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MergeSemantics works if other nodes are implicitly merged into its node', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index 809eb09c8cdfc..8ec5e33799432 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -7,6 +7,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -15,7 +16,7 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('Semantics shutdown and restart', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics shutdown and restart', (WidgetTester tester) async { SemanticsTester? semantics = SemanticsTester(tester); final TestSemantics expectedSemantics = TestSemantics.root( @@ -59,7 +60,7 @@ void main() { semantics.dispose(); }, semanticsEnabled: false); - testWidgets('Semantics tag only applies to immediate child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics tag only applies to immediate child', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -95,7 +96,7 @@ void main() { semantics.dispose(); }, semanticsEnabled: false); - testWidgets('Semantics tooltip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics tooltip', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final TestSemantics expectedSemantics = TestSemantics.root( @@ -123,7 +124,7 @@ void main() { semantics.dispose(); }); - testWidgets('Detach and reattach assert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Detach and reattach assert', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey key = GlobalKey(); @@ -201,7 +202,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics and Directionality - RTL', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -218,7 +219,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics and Directionality - LTR', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -235,7 +236,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final TestSemantics expectedSemantics = TestSemantics.root( @@ -262,7 +263,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final TestSemantics expectedSemantics = TestSemantics.root( @@ -289,7 +290,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics label and hint', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics label and hint', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -319,7 +320,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics hints can merge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics hints can merge', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -355,7 +356,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics values do not merge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics values do not merge', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -406,7 +407,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics value and hint can merge', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics value and hint can merge', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -442,7 +443,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics tagForChildren works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics tagForChildren works', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -490,7 +491,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widget supports all actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widget supports all actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<SemanticsAction> performedActions = <SemanticsAction>[]; @@ -580,7 +581,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widget supports all flags', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widget supports all flags', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // Checked state and toggled state are mutually exclusive. await tester.pumpWidget( @@ -609,6 +610,7 @@ void main() { namesRoute: true, image: true, liveRegion: true, + expanded: true, ), ); final List<SemanticsFlag> flags = SemanticsFlag.values.toList(); @@ -691,6 +693,7 @@ void main() { namesRoute: true, image: true, liveRegion: true, + expanded: true, ), ); flags @@ -708,7 +711,7 @@ void main() { expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); }); - testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Actions can be replaced without triggering semantics update', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics( @@ -805,7 +808,7 @@ void main() { semantics.dispose(); }); - testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); await tester.pumpWidget(Semantics( container: true, @@ -831,7 +834,7 @@ void main() { semantics.dispose(); }); - testWidgets('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async { final SemanticsHandle semantics = tester.ensureSemantics(); await tester.pumpWidget(Semantics( container: true, @@ -850,7 +853,7 @@ void main() { semantics.dispose(); }); - testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Increased/decreased values are annotated', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -882,7 +885,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics( @@ -965,7 +968,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics( @@ -1023,7 +1026,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widgets without sort orders are sorted properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widgets without sort orders are sorted properly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics( @@ -1084,7 +1087,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widgets that are transformed are sorted properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widgets that are transformed are sorted properly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics( @@ -1148,7 +1151,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); int semanticsUpdateCount = 0; final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(listener: () { @@ -1249,7 +1252,7 @@ void main() { semantics.dispose(); }); - testWidgets('Semantics excludeSemantics ignores children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics excludeSemantics ignores children', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Semantics( label: 'label', @@ -1277,7 +1280,7 @@ void main() { semantics.dispose(); }); - testWidgets('Can change handlers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can change handlers', (WidgetTester tester) async { await tester.pumpWidget(Semantics( container: true, label: 'foo', @@ -1664,7 +1667,7 @@ void main() { )); }); - testWidgets('Semantics with zero transform gets dropped', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics with zero transform gets dropped', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110671. // Construct a widget tree that will end up with a fitted box that applies // a zero transform because it does not actually draw its children. @@ -1691,7 +1694,7 @@ void main() { expect(node.childrenCount, 0); }); - testWidgets('blocking user interaction works on explicit child node.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('blocking user interaction works on explicit child node.', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); await tester.pumpWidget( @@ -1734,7 +1737,7 @@ void main() { ); }); - testWidgets('blocking user interaction on a merged child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('blocking user interaction on a merged child', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( @@ -1769,7 +1772,7 @@ void main() { ); }); - testWidgets('does not merge conflicting actions even if one of them is blocked', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not merge conflicting actions even if one of them is blocked', (WidgetTester tester) async { final UniqueKey key = UniqueKey(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart b/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart index 1f7aec466cf97..479f20e6319c1 100644 --- a/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart +++ b/packages/flutter/test/widgets/semantics_tester_generate_test_semantics_expression_for_current_semantics_tree_test.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -52,7 +53,7 @@ void _tests() { // also update this code to reflect the new output. // // This test is flexible w.r.t. leading and trailing whitespace. - testWidgets('generates code', (WidgetTester tester) async { + testWidgetsWithLeakTracking('generates code', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await pumpTestWidget(tester); final String code = semantics @@ -91,7 +92,7 @@ void _tests() { expect('$code,', expectedCode); }); - testWidgets('generated code is correct', (WidgetTester tester) async { + testWidgetsWithLeakTracking('generated code is correct', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await pumpTestWidget(tester); expect( diff --git a/packages/flutter/test/widgets/semantics_tester_test.dart b/packages/flutter/test/widgets/semantics_tester_test.dart index c5a9de1763cb9..e8b8a1ea59493 100644 --- a/packages/flutter/test/widgets/semantics_tester_test.dart +++ b/packages/flutter/test/widgets/semantics_tester_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Semantics tester visits last child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Semantics tester visits last child', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/semantics_traversal_test.dart b/packages/flutter/test/widgets/semantics_traversal_test.dart index a509ab2d2cf4e..23247ad651380 100644 --- a/packages/flutter/test/widgets/semantics_traversal_test.dart +++ b/packages/flutter/test/widgets/semantics_traversal_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -21,7 +22,7 @@ void main() { }); void testTraversal(String description, TraversalTestFunction testFunction) { - testWidgets(description, (WidgetTester tester) async { + testWidgetsWithLeakTracking(description, (WidgetTester tester) async { final TraversalTester traversalTester = TraversalTester(tester); await testFunction(traversalTester); traversalTester.dispose(); diff --git a/packages/flutter/test/widgets/semantics_zero_surface_size_test.dart b/packages/flutter/test/widgets/semantics_zero_surface_size_test.dart index 07c58cdafa4ef..273c437cca453 100644 --- a/packages/flutter/test/widgets/semantics_zero_surface_size_test.dart +++ b/packages/flutter/test/widgets/semantics_zero_surface_size_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('has only root node if surface size is 0x0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('has only root node if surface size is 0x0', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Semantics( diff --git a/packages/flutter/test/widgets/set_state_1_test.dart b/packages/flutter/test/widgets/set_state_1_test.dart index ca29a40b26fc6..dc85b46e470c3 100644 --- a/packages/flutter/test/widgets/set_state_1_test.dart +++ b/packages/flutter/test/widgets/set_state_1_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Inside extends StatefulWidget { const Inside({ super.key }); @@ -65,7 +66,7 @@ class OutsideState extends State<Outside> { } void main() { - testWidgets('setState() smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState() smoke test', (WidgetTester tester) async { await tester.pumpWidget(const Outside()); final Offset location = tester.getCenter(find.text('INSIDE')); final TestGesture gesture = await tester.startGesture(location); diff --git a/packages/flutter/test/widgets/set_state_2_test.dart b/packages/flutter/test/widgets/set_state_2_test.dart index 4df03006cff66..1894cea871bae 100644 --- a/packages/flutter/test/widgets/set_state_2_test.dart +++ b/packages/flutter/test/widgets/set_state_2_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('setState() overbuild test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState() overbuild test', (WidgetTester tester) async { final List<String> log = <String>[]; final Builder inner = Builder( builder: (BuildContext context) { diff --git a/packages/flutter/test/widgets/set_state_3_test.dart b/packages/flutter/test/widgets/set_state_3_test.dart index 461746e98a122..534d9cb976646 100644 --- a/packages/flutter/test/widgets/set_state_3_test.dart +++ b/packages/flutter/test/widgets/set_state_3_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; late ChangerState changer; @@ -52,7 +53,7 @@ class LeafState extends State<Leaf> { } void main() { - testWidgets('three-way setState() smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('three-way setState() smoke test', (WidgetTester tester) async { await tester.pumpWidget(const Changer(Wrapper(Leaf()))); await tester.pumpWidget(const Changer(Wrapper(Leaf()))); changer.test(); diff --git a/packages/flutter/test/widgets/set_state_4_test.dart b/packages/flutter/test/widgets/set_state_4_test.dart index e0fc2e7ee0693..2349f8151a3eb 100644 --- a/packages/flutter/test/widgets/set_state_4_test.dart +++ b/packages/flutter/test/widgets/set_state_4_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Changer extends StatefulWidget { const Changer({ super.key }); @@ -21,7 +22,7 @@ class ChangerState extends State<Changer> { } void main() { - testWidgets('setState() catches being used with an async callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState() catches being used with an async callback', (WidgetTester tester) async { await tester.pumpWidget(const Changer()); final ChangerState s = tester.state(find.byType(Changer)); expect(s.test0, isNot(throwsFlutterError)); diff --git a/packages/flutter/test/widgets/set_state_5_test.dart b/packages/flutter/test/widgets/set_state_5_test.dart index fd7bdaccd73f1..335d427d643c0 100644 --- a/packages/flutter/test/widgets/set_state_5_test.dart +++ b/packages/flutter/test/widgets/set_state_5_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class BadWidget extends StatefulWidget { const BadWidget({ super.key }); @@ -27,7 +28,7 @@ class BadWidgetState extends State<BadWidget> { } void main() { - testWidgets('setState() catches being used inside a constructor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setState() catches being used inside a constructor', (WidgetTester tester) async { await tester.pumpWidget(const BadWidget()); expect(tester.takeException(), isFlutterError); }); diff --git a/packages/flutter/test/widgets/shader_mask_test.dart b/packages/flutter/test/widgets/shader_mask_test.dart index 018950f20bc8b..7b941c06d4903 100644 --- a/packages/flutter/test/widgets/shader_mask_test.dart +++ b/packages/flutter/test/widgets/shader_mask_test.dart @@ -9,6 +9,7 @@ library; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Shader createShader(Rect bounds) { return const LinearGradient( @@ -21,12 +22,12 @@ Shader createShader(Rect bounds) { void main() { - testWidgets('Can be constructed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can be constructed', (WidgetTester tester) async { const Widget child = SizedBox(width: 100.0, height: 100.0); await tester.pumpWidget(const ShaderMask(shaderCallback: createShader, child: child)); }); - testWidgets('Bounds rect includes offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bounds rect includes offset', (WidgetTester tester) async { late Rect shaderBounds; Shader recordShaderBounds(Rect bounds) { shaderBounds = bounds; @@ -50,7 +51,7 @@ void main() { }); - testWidgets('Bounds rect includes offset visual inspection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Bounds rect includes offset visual inspection', (WidgetTester tester) async { final Widget widgetBottomRight = Container( width: 400, height: 400, diff --git a/packages/flutter/test/widgets/shadow_test.dart b/packages/flutter/test/widgets/shadow_test.dart index eb97b9af2d9ae..f2d5f93b429e5 100644 --- a/packages/flutter/test/widgets/shadow_test.dart +++ b/packages/flutter/test/widgets/shadow_test.dart @@ -9,13 +9,14 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { tearDown(() { debugDisableShadows = true; }); - testWidgets('Shadows on BoxDecoration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shadows on BoxDecoration', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -61,7 +62,7 @@ void main() { ); } for (final int elevation in kElevationToShadow.keys) { - testWidgets('elevation $elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('elevation $elevation', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget(build(elevation)); await expectLater( @@ -73,7 +74,7 @@ void main() { } }); - testWidgets('Shadows with PhysicalLayer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shadows with PhysicalLayer', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -132,7 +133,7 @@ void main() { } for (final int elevation in kElevationToShadow.keys) { - testWidgets('elevation $elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('elevation $elevation', (WidgetTester tester) async { debugDisableShadows = false; await tester.pumpWidget(build(elevation.toDouble())); await expectLater( diff --git a/packages/flutter/test/widgets/shape_decoration_test.dart b/packages/flutter/test/widgets/shape_decoration_test.dart index 36a0b4b4362d5..0db118ad09d3c 100644 --- a/packages/flutter/test/widgets/shape_decoration_test.dart +++ b/packages/flutter/test/widgets/shape_decoration_test.dart @@ -7,17 +7,17 @@ import 'dart:ui' as ui show Image; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../image_data.dart'; import '../painting/mocks_for_image_cache.dart'; -import '../rendering/mock_canvas.dart'; import 'test_border.dart' show TestBorder; Future<void> main() async { AutomatedTestWidgetsFlutterBinding(); final ui.Image rawImage = await decodeImageFromList(Uint8List.fromList(kTransparentImage)); final ImageProvider image = TestImageProvider(0, 0, image: rawImage); - testWidgets('ShapeDecoration.image', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ShapeDecoration.image', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: DecoratedBox( @@ -40,7 +40,7 @@ Future<void> main() async { ); }); - testWidgets('ShapeDecoration.color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ShapeDecoration.color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: DecoratedBox( @@ -69,7 +69,7 @@ Future<void> main() async { expect(decoration.padding, isA<EdgeInsetsDirectional>()); }); - testWidgets('TestBorder and Directionality - 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TestBorder and Directionality - 1', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( MaterialApp( @@ -90,7 +90,7 @@ Future<void> main() async { ); }); - testWidgets('TestBorder and Directionality - 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TestBorder and Directionality - 2', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( @@ -114,7 +114,7 @@ Future<void> main() async { ); }); - testWidgets('Does not crash with directional gradient', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not crash with directional gradient', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/76967. await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/shared_app_data_test.dart b/packages/flutter/test/widgets/shared_app_data_test.dart index 6d57734d4b1b3..db9ec5d3a02f7 100644 --- a/packages/flutter/test/widgets/shared_app_data_test.dart +++ b/packages/flutter/test/widgets/shared_app_data_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SharedAppData basics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SharedAppData basics', (WidgetTester tester) async { int columnBuildCount = 0; int child1BuildCount = 0; int child2BuildCount = 0; @@ -116,7 +117,7 @@ void main() { expect(find.text('null').evaluate().length, 2); }); - testWidgets('WidgetsApp SharedAppData ', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp SharedAppData ', (WidgetTester tester) async { int parentBuildCount = 0; int childBuildCount = 0; @@ -154,7 +155,7 @@ void main() { expect(find.text('child'), findsOneWidget); }); - testWidgets('WidgetsApp SharedAppData Shadowing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetsApp SharedAppData Shadowing', (WidgetTester tester) async { int innerTapCount = 0; int outerTapCount = 0; diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart index 0481f8cd6b585..047f727a466c9 100644 --- a/packages/flutter/test/widgets/shortcuts_test.dart +++ b/packages/flutter/test/widgets/shortcuts_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group(LogicalKeySet, () { @@ -111,7 +112,7 @@ void main() { ); }); - testWidgets('handles two keys', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles two keys', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( LogicalKeySet( @@ -210,7 +211,7 @@ void main() { ); }); - testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( @@ -252,7 +253,7 @@ void main() { }); group(SingleActivator, () { - testWidgets('handles Ctrl-C', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( @@ -351,7 +352,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('handles repeated events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( @@ -377,7 +378,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('rejects repeated events if requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('rejects repeated events if requested', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( @@ -404,7 +405,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles Shift-Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( @@ -454,7 +455,7 @@ void main() { expect(RawKeyboard.instance.keysPressed, isEmpty); }); - testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( @@ -533,15 +534,16 @@ void main() { }); group(Shortcuts, () { - testWidgets('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async { const Shortcuts shortcuts = Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); - testWidgets('Default constructed Shortcuts.manager has empty shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default constructed Shortcuts.manager has empty shortcuts', (WidgetTester tester) async { final ShortcutManager manager = ShortcutManager(); + addTearDown(manager.dispose); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, isEmpty); final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); @@ -550,11 +552,12 @@ void main() { expect(shortcuts.shortcuts, isEmpty); }); - testWidgets('Shortcuts.manager passes on shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts.manager passes on shortcuts', (WidgetTester tester) async { final Map<LogicalKeySet, Intent> testShortcuts = <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }; final ShortcutManager manager = ShortcutManager(shortcuts: testShortcuts); + addTearDown(manager.dispose); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, equals(testShortcuts)); final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); @@ -563,7 +566,7 @@ void main() { expect(shortcuts.shortcuts, equals(testShortcuts)); }); - testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ShortcutManager handles shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -572,6 +575,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( Actions( @@ -598,7 +602,7 @@ void main() { expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); - testWidgets('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -607,6 +611,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( Actions( @@ -633,7 +638,7 @@ void main() { expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); - testWidgets('ShortcutManager ignores key presses with no primary focus', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ShortcutManager ignores key presses with no primary focus', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -642,6 +647,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( Actions( @@ -666,7 +672,14 @@ void main() { expect(pressedKeys, isEmpty); }); - testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async { + test('$ShortcutManager dispatches object creation in constructor', () async { + await expectLater( + await memoryEvents(() => ShortcutManager().dispose(), ShortcutManager), + areCreateAndDispose, + ); + }); + + testWidgetsWithLeakTracking("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -675,6 +688,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( Shortcuts.manager( @@ -706,7 +720,7 @@ void main() { expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); - testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -715,6 +729,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( MaterialApp( @@ -748,7 +763,7 @@ void main() { expect(pressedKeys, isEmpty); }); - testWidgets("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -757,6 +772,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); + addTearDown(testManager.dispose); await tester.pumpWidget( MaterialApp( home: Material( @@ -773,7 +789,7 @@ void main() { expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); }); - testWidgets('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -782,6 +798,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( MaterialApp( @@ -810,7 +827,7 @@ void main() { expect(invoked, isTrue); }); - testWidgets('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -819,6 +836,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( MaterialApp( @@ -851,7 +869,7 @@ void main() { expect(invoked, isFalse); }); - testWidgets('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( @@ -860,6 +878,7 @@ void main() { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); + addTearDown(testManager.dispose); bool invoked = false; await tester.pumpWidget( MaterialApp( @@ -977,6 +996,7 @@ void main() { ): const ActivateIntent(), }, ); + addTearDown(testManager.dispose); Shortcuts.manager( manager: testManager, @@ -992,7 +1012,7 @@ void main() { expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}')); }); - testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts support multiple intents', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp() { @@ -1073,7 +1093,7 @@ void main() { expect(controller.position.pixels, 0.0); }); - testWidgets('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const DumbLogicalActivator(LogicalKeyboardKey.keyC), @@ -1115,7 +1135,7 @@ void main() { }); group('CharacterActivator', () { - testWidgets('is triggered on events with correct character', (WidgetTester tester) async { + testWidgetsWithLeakTracking('is triggered on events with correct character', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), @@ -1133,7 +1153,7 @@ void main() { invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('handles repeated events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), @@ -1153,7 +1173,7 @@ void main() { invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('rejects repeated events if requested', (WidgetTester tester) async { + testWidgetsWithLeakTracking('rejects repeated events if requested', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?', includeRepeats: false), @@ -1173,7 +1193,7 @@ void main() { invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('handles Alt, Ctrl and Meta', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handles Alt, Ctrl and Meta', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?', alt: true, meta: true, control: true), @@ -1219,7 +1239,7 @@ void main() { invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); - testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( @@ -1288,7 +1308,7 @@ void main() { }); group('CallbackShortcuts', () { - testWidgets('trigger on key events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trigger on key events', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( @@ -1326,7 +1346,7 @@ void main() { expect(invokedB, equals(1)); }); - testWidgets('nested CallbackShortcuts stop propagation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('nested CallbackShortcuts stop propagation', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( @@ -1359,7 +1379,7 @@ void main() { expect(invokedInner, equals(1)); }); - testWidgets('non-overlapping nested CallbackShortcuts fire appropriately', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-overlapping nested CallbackShortcuts fire appropriately', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( @@ -1396,7 +1416,7 @@ void main() { expect(invokedInner, equals(1)); }); - testWidgets('Works correctly with Shortcuts too', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Works correctly with Shortcuts too', (WidgetTester tester) async { int invokedCallbackA = 0; int invokedCallbackB = 0; int invokedActionA = 0; @@ -1470,7 +1490,7 @@ void main() { }); group('ShortcutRegistrar', () { - testWidgets('trigger ShortcutRegistrar on key events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('trigger ShortcutRegistrar on key events', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( @@ -1515,7 +1535,7 @@ void main() { expect(invokedB, equals(1)); }); - testWidgets('MaterialApp has a ShortcutRegistrar listening', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MaterialApp has a ShortcutRegistrar listening', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( @@ -1560,7 +1580,7 @@ void main() { expect(invokedB, equals(1)); }); - testWidgets("doesn't override text field shortcuts", (WidgetTester tester) async { + testWidgetsWithLeakTracking("doesn't override text field shortcuts", (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MaterialApp( @@ -1592,7 +1612,7 @@ void main() { expect(controller.selection.extentOffset, equals(7)); }); - testWidgets('nested ShortcutRegistrars stop propagation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('nested ShortcutRegistrars stop propagation', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( @@ -1633,7 +1653,7 @@ void main() { expect(invokedInner, equals(1)); }); - testWidgets('non-overlapping nested ShortcutRegistrars fire appropriately', (WidgetTester tester) async { + testWidgetsWithLeakTracking('non-overlapping nested ShortcutRegistrars fire appropriately', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( @@ -1679,7 +1699,7 @@ void main() { expect(invokedInner, equals(1)); }); - testWidgets('Works correctly with Shortcuts too', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Works correctly with Shortcuts too', (WidgetTester tester) async { int invokedCallbackA = 0; int invokedCallbackB = 0; int invokedActionA = 0; @@ -1756,7 +1776,7 @@ void main() { await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); }); - testWidgets('Updating shortcuts triggers dependency rebuild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Updating shortcuts triggers dependency rebuild', (WidgetTester tester) async { final List<Map<ShortcutActivator, Intent>> shortcutsChanged = <Map<ShortcutActivator, Intent>>[]; void dependenciesUpdated(Map<ShortcutActivator, Intent> shortcuts) { shortcutsChanged.add(shortcuts); @@ -1830,8 +1850,9 @@ void main() { })); }); - testWidgets('using a disposed token asserts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('using a disposed token asserts', (WidgetTester tester) async { final ShortcutRegistry registry = ShortcutRegistry(); + addTearDown(registry.dispose); final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); @@ -1839,8 +1860,9 @@ void main() { expect(() {token.replaceAll(<ShortcutActivator, Intent>{}); }, throwsFlutterError); }); - testWidgets('setting duplicate bindings asserts', (WidgetTester tester) async { + testWidgetsWithLeakTracking('setting duplicate bindings asserts', (WidgetTester tester) async { final ShortcutRegistry registry = ShortcutRegistry(); + addTearDown(registry.dispose); final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); @@ -1852,6 +1874,13 @@ void main() { }, throwsAssertionError); token.dispose(); }); + + test('dispatches object creation in constructor', () async { + await expectLater( + await memoryEvents(() => ShortcutRegistry().dispose(), ShortcutRegistry), + areCreateAndDispose, + ); + }); }); } diff --git a/packages/flutter/test/widgets/simple_semantics_test.dart b/packages/flutter/test/widgets/simple_semantics_test.dart index 4b5729ea5d461..34b0733a94855 100644 --- a/packages/flutter/test/widgets/simple_semantics_test.dart +++ b/packages/flutter/test/widgets/simple_semantics_test.dart @@ -6,11 +6,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Simple tree is simple', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple tree is simple', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( @@ -34,7 +35,7 @@ void main() { semantics.dispose(); }); - testWidgets('Simple tree is simple - material', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Simple tree is simple - material', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // Not using Text widget because of https://github.com/flutter/flutter/issues/12357. diff --git a/packages/flutter/test/widgets/single_child_scroll_view_test.dart b/packages/flutter/test/widgets/single_child_scroll_view_test.dart index 1cf85166116fb..dfe67d39ff20a 100644 --- a/packages/flutter/test/widgets/single_child_scroll_view_test.dart +++ b/packages/flutter/test/widgets/single_child_scroll_view_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; import 'semantics_tester.dart'; @@ -119,16 +120,32 @@ void main() { renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls expect(context.clipBehavior, equals(Clip.hardEdge)); - // 3rd, pump a new widget to check that the render object can update its clip behavior. + // 3rd, check that the underlying Scrollable has the same clipBehavior + // Regression test for https://github.com/flutter/flutter/issues/133330 + Finder scrollable = find.byWidgetPredicate((Widget widget) => widget is Scrollable); + expect( + (tester.widget(scrollable) as Scrollable).clipBehavior, + Clip.hardEdge, + ); + + // 4th, pump a new widget to check that the render object can update its clip behavior. await tester.pumpWidget(SingleChildScrollView(clipBehavior: Clip.antiAlias, child: Container(height: 2000.0))); expect(renderObject.clipBehavior, equals(Clip.antiAlias)); // ignore: avoid_dynamic_calls - // 4th, check that a non-default clip behavior can be sent to the painting context. + // 5th, check that a non-default clip behavior can be sent to the painting context. renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls expect(context.clipBehavior, equals(Clip.antiAlias)); + + // 6th, check that the underlying Scrollable has the same clipBehavior + // Regression test for https://github.com/flutter/flutter/issues/133330 + scrollable = find.byWidgetPredicate((Widget widget) => widget is Scrollable); + expect( + (tester.widget(scrollable) as Scrollable).clipBehavior, + Clip.antiAlias, + ); }); - testWidgets('SingleChildScrollView control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView control test', (WidgetTester tester) async { await tester.pumpWidget(SingleChildScrollView( child: Container( height: 2000.0, @@ -144,8 +161,9 @@ void main() { expect(box.localToGlobal(Offset.zero), equals(const Offset(0.0, -200.0))); }); - testWidgets('Changing controllers changes scroll position', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing controllers changes scroll position', (WidgetTester tester) async { final TestScrollController controller = TestScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(SingleChildScrollView( child: Container( @@ -166,8 +184,9 @@ void main() { expect(scrollable.position, isA<TestScrollPosition>()); }); - testWidgets('Sets PrimaryScrollController when primary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget(PrimaryScrollController( controller: primaryScrollController, child: SingleChildScrollView( @@ -184,8 +203,9 @@ void main() { }); - testWidgets('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(Center( child: SizedBox( @@ -221,25 +241,28 @@ void main() { )); }); - testWidgets('Vertical SingleChildScrollViews are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical SingleChildScrollViews are not primary by default', (WidgetTester tester) async { const SingleChildScrollView view = SingleChildScrollView(); expect(view.primary, isNull); }); - testWidgets('Horizontal SingleChildScrollViews are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal SingleChildScrollViews are not primary by default', (WidgetTester tester) async { const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal); expect(view.primary, isNull); }); - testWidgets('SingleChildScrollViews with controllers are not primary by default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollViews with controllers are not primary by default', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final SingleChildScrollView view = SingleChildScrollView( - controller: ScrollController(), + controller: controller, ); expect(view.primary, isNull); }); - testWidgets('Vertical SingleChildScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical SingleChildScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const SingleChildScrollView(), controller: controller, @@ -247,8 +270,9 @@ void main() { expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); - testWidgets("Vertical SingleChildScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Vertical SingleChildScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const SingleChildScrollView(), controller: controller, @@ -256,9 +280,10 @@ void main() { expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); - testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async { const Key innerKey = Key('inner'); final ScrollController primaryScrollController = ScrollController(); + addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -284,9 +309,10 @@ void main() { expect(innerScrollable.controller, isNull); }); - testWidgets('SingleChildScrollView semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( @@ -379,8 +405,9 @@ void main() { semantics.dispose(); }); - testWidgets('SingleChildScrollView semantics clips cover entire child vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView semantics clips cover entire child vertical', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final UniqueKey scrollView = UniqueKey(); final UniqueKey childBox = UniqueKey(); const double length = 10000; @@ -418,8 +445,9 @@ void main() { expect(semanticsClip.size.height, length); }); - testWidgets('SingleChildScrollView semantics clips cover entire child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView semantics clips cover entire child', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final UniqueKey scrollView = UniqueKey(); final UniqueKey childBox = UniqueKey(); const double length = 10000; @@ -458,7 +486,43 @@ void main() { expect(semanticsClip.size.width, length); }); - testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView getOffsetToReveal - will not assert on axis mismatch', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); + List<Widget> children; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + height: 200.0, + width: 300.0, + child: SingleChildScrollView( + controller: controller, + child: Column( + children: children = List<Widget>.generate(20, (int i) { + return SizedBox( + height: 100.0, + width: 300.0, + child: Text('Tile $i'), + ); + }), + ), + ), + ), + ), + ), + ); + + final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first; + + final RenderObject target = tester.renderObject(find.byWidget(children[5])); + viewport.getOffsetToReveal(target, 0.0, axis: Axis.horizontal); + }); + + testWidgetsWithLeakTracking('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); List<Widget> children; await tester.pumpWidget( Directionality( @@ -468,7 +532,7 @@ void main() { height: 200.0, width: 300.0, child: SingleChildScrollView( - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, child: Column( children: children = List<Widget>.generate(20, (int i) { return SizedBox( @@ -504,7 +568,9 @@ void main() { expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0)); }); - testWidgets('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); final List<Widget> children = List<Widget>.generate(20, (int i) { return SizedBox( height: 100.0, @@ -520,7 +586,7 @@ void main() { height: 200.0, width: 300.0, child: SingleChildScrollView( - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, reverse: true, child: Column( children: children.reversed.toList(), @@ -551,7 +617,9 @@ void main() { expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0)); }); - testWidgets('SingleChildScrollView getOffsetToReveal - right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView getOffsetToReveal - right', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); List<Widget> children; await tester.pumpWidget( @@ -563,7 +631,7 @@ void main() { width: 200.0, child: SingleChildScrollView( scrollDirection: Axis.horizontal, - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, child: Row( children: children = List<Widget>.generate(20, (int i) { return SizedBox( @@ -599,7 +667,9 @@ void main() { expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0)); }); - testWidgets('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async { + final ScrollController controller = ScrollController(initialScrollOffset: 300.0); + addTearDown(controller.dispose); final List<Widget> children = List<Widget>.generate(20, (int i) { return SizedBox( height: 300.0, @@ -618,7 +688,7 @@ void main() { child: SingleChildScrollView( scrollDirection: Axis.horizontal, reverse: true, - controller: ScrollController(initialScrollOffset: 300.0), + controller: controller, child: Row( children: children.reversed.toList(), ), @@ -648,7 +718,7 @@ void main() { expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0)); }); - testWidgets('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async { final List<List<Widget>> children = List<List<Widget>>.generate(10, (int x) { return List<Widget>.generate(10, (int y) { return SizedBox( @@ -658,8 +728,10 @@ void main() { ); }); }); - ScrollController controllerX; - ScrollController controllerY; + late ScrollController controllerX; + addTearDown(() => controllerX.dispose()); + late ScrollController controllerY; + addTearDown(() => controllerY.dispose()); /// Builds a gird: /// @@ -858,9 +930,11 @@ void main() { ); } - testWidgets('in view in inner, but not in outer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('in view in inner, but not in outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(); + addTearDown(inner.dispose); final ScrollController outer = ScrollController(); + addTearDown(outer.dispose); await buildNestedScroller( tester: tester, inner: inner, @@ -875,9 +949,11 @@ void main() { expect(outer.offset, 100.0); }); - testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('not in view of neither inner nor outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(); + addTearDown(inner.dispose); final ScrollController outer = ScrollController(); + addTearDown(outer.dispose); await buildNestedScroller( tester: tester, inner: inner, @@ -892,9 +968,11 @@ void main() { expect(outer.offset, 200.0); }); - testWidgets('in view in inner and outer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('in view in inner and outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(initialScrollOffset: 200.0); + addTearDown(inner.dispose); final ScrollController outer = ScrollController(initialScrollOffset: 200.0); + addTearDown(outer.dispose); await buildNestedScroller( tester: tester, inner: inner, @@ -909,9 +987,11 @@ void main() { expect(inner.offset, 200.0); }); - testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inner shown in outer, but item not visible', (WidgetTester tester) async { final ScrollController inner = ScrollController(initialScrollOffset: 200.0); + addTearDown(inner.dispose); final ScrollController outer = ScrollController(initialScrollOffset: 200.0); + addTearDown(outer.dispose); await buildNestedScroller( tester: tester, inner: inner, @@ -926,9 +1006,11 @@ void main() { expect(inner.offset, 400.0); }); - testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inner half shown in outer, item only visible in inner', (WidgetTester tester) async { final ScrollController inner = ScrollController(); + addTearDown(inner.dispose); final ScrollController outer = ScrollController(initialScrollOffset: 100.0); + addTearDown(outer.dispose); await buildNestedScroller( tester: tester, inner: inner, @@ -944,8 +1026,13 @@ void main() { }); }); - testWidgets('keyboardDismissBehavior tests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('keyboardDismissBehavior tests', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); + addTearDown(() { + for (final FocusNode node in focusNodes) { + node.dispose(); + } + }); Future<void> boilerplate(ScrollViewKeyboardDismissBehavior behavior) { return tester.pumpWidget( diff --git a/packages/flutter/test/widgets/size_changed_layout_notification_test.dart b/packages/flutter/test/widgets/size_changed_layout_notification_test.dart index 6f8c8c83c1759..f4b1e8862057a 100644 --- a/packages/flutter/test/widgets/size_changed_layout_notification_test.dart +++ b/packages/flutter/test/widgets/size_changed_layout_notification_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SizeChangedLayoutNotification test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizeChangedLayoutNotification test', (WidgetTester tester) async { bool notified = false; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/sized_box_test.dart b/packages/flutter/test/widgets/sized_box_test.dart index 4d1d5094e1a59..60822a1e31b56 100644 --- a/packages/flutter/test/widgets/sized_box_test.dart +++ b/packages/flutter/test/widgets/sized_box_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SizedBox constructors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedBox constructors', (WidgetTester tester) async { const SizedBox a = SizedBox(); expect(a.width, isNull); expect(a.height, isNull); @@ -37,7 +38,7 @@ void main() { expect(g.height, 0.0); }); - testWidgets('SizedBox - no child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedBox - no child', (WidgetTester tester) async { final GlobalKey patient = GlobalKey(); await tester.pumpWidget( @@ -109,7 +110,7 @@ void main() { expect(patient.currentContext!.size, equals(Size.zero)); }); - testWidgets('SizedBox - container child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedBox - container child', (WidgetTester tester) async { final GlobalKey patient = GlobalKey(); await tester.pumpWidget( @@ -188,7 +189,7 @@ void main() { expect(patient.currentContext!.size, equals(Size.zero)); }); - testWidgets('SizedBox.square tests', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizedBox.square tests', (WidgetTester tester) async { await tester.pumpWidget( const SizedBox.square( dimension: 100, diff --git a/packages/flutter/test/widgets/sliver_appbar_opacity_test.dart b/packages/flutter/test/widgets/sliver_appbar_opacity_test.dart index 474af5272cbe8..41d222c353c1e 100644 --- a/packages/flutter/test/widgets/sliver_appbar_opacity_test.dart +++ b/packages/flutter/test/widgets/sliver_appbar_opacity_test.dart @@ -5,10 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('!pinned && !floating && !bottom ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('!pinned && !floating && !bottom ==> fade opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: false, @@ -26,8 +28,9 @@ void main() { expect(render.text.style!.color!.opacity, 0.0); }); - testWidgets('!pinned && !floating && bottom ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('!pinned && !floating && bottom ==> fade opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: false, @@ -45,8 +48,9 @@ void main() { expect(render.text.style!.color!.opacity, 0.0); }); - testWidgets('!pinned && floating && !bottom ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('!pinned && floating && !bottom ==> fade opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: false, @@ -64,8 +68,9 @@ void main() { expect(render.text.style!.color!.opacity, 0.0); }); - testWidgets('!pinned && floating && bottom ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('!pinned && floating && bottom ==> fade opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: false, @@ -83,8 +88,9 @@ void main() { expect(render.text.style!.color!.opacity, 0.0); }); - testWidgets('pinned && !floating && !bottom ==> 1.0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pinned && !floating && !bottom ==> 1.0 opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: true, @@ -102,8 +108,9 @@ void main() { expect(render.text.style!.color!.opacity, 1.0); }); - testWidgets('pinned && !floating && bottom ==> 1.0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pinned && !floating && bottom ==> 1.0 opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: true, @@ -121,10 +128,11 @@ void main() { expect(render.text.style!.color!.opacity, 1.0); }); - testWidgets('pinned && floating && !bottom ==> 1.0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pinned && floating && !bottom ==> 1.0 opacity', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25000. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: true, @@ -142,10 +150,11 @@ void main() { expect(render.text.style!.color!.opacity, 1.0); }); - testWidgets('pinned && floating && bottom && extraToolbarHeight == 0.0 ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pinned && floating && bottom && extraToolbarHeight == 0.0 ==> fade opacity', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/25993. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: true, @@ -163,8 +172,9 @@ void main() { expect(render.text.style!.color!.opacity, 0.0); }); - testWidgets('pinned && floating && bottom && extraToolbarHeight != 0.0 ==> 1.0 opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('pinned && floating && bottom && extraToolbarHeight != 0.0 ==> 1.0 opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _TestWidget( pinned: true, @@ -183,8 +193,9 @@ void main() { expect(render.text.style!.color!.opacity, 1.0); }); - testWidgets('!pinned && !floating && !bottom && extraToolbarHeight != 0.0 ==> fade opacity', (WidgetTester tester) async { + testWidgetsWithLeakTracking('!pinned && !floating && !bottom && extraToolbarHeight != 0.0 ==> fade opacity', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); const double collapsedHeight = 100.0; await tester.pumpWidget( _TestWidget( diff --git a/packages/flutter/test/widgets/sliver_constrained_cross_axis_test.dart b/packages/flutter/test/widgets/sliver_constrained_cross_axis_test.dart index 0fa6bf5eff40e..6cf42bd9cb92c 100644 --- a/packages/flutter/test/widgets/sliver_constrained_cross_axis_test.dart +++ b/packages/flutter/test/widgets/sliver_constrained_cross_axis_test.dart @@ -5,12 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const double VIEWPORT_HEIGHT = 500; const double VIEWPORT_WIDTH = 300; void main() { - testWidgets('SliverConstrainedCrossAxis basic test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis basic test', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 50)); final RenderBox box = tester.renderObject(find.byType(Container)); @@ -21,7 +22,7 @@ void main() { expect(sliver.geometry!.paintExtent, equals(100)); }); - testWidgets('SliverConstrainedCrossAxis updates correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis updates correctly', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 50)); final RenderBox box1 = tester.renderObject(find.byType(Container)); @@ -35,7 +36,7 @@ void main() { expect(box2.size.width, 80); }); - testWidgets('SliverConstrainedCrossAxis uses parent extent if maxExtent is greater', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis uses parent extent if maxExtent is greater', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 400)); final RenderBox box = tester.renderObject(find.byType(Container)); @@ -43,7 +44,7 @@ void main() { expect(box.size.width, VIEWPORT_WIDTH); }); - testWidgets('SliverConstrainedCrossAxis constrains the height when direction is horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis constrains the height when direction is horizontal', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverConstrainedCrossAxis( maxExtent: 50, scrollDirection: Axis.horizontal, @@ -53,7 +54,7 @@ void main() { expect(box.size.height, 50); }); - testWidgets('SliverConstrainedCrossAxis sets its own flex to 0', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis sets its own flex to 0', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverConstrainedCrossAxis( maxExtent: 50, )); diff --git a/packages/flutter/test/widgets/sliver_constraints_test.dart b/packages/flutter/test/widgets/sliver_constraints_test.dart index 56565b69ebb5d..e2ac47afe7d98 100644 --- a/packages/flutter/test/widgets/sliver_constraints_test.dart +++ b/packages/flutter/test/widgets/sliver_constraints_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('precedingScrollExtent is reported as infinity for Sliver of unknown size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('precedingScrollExtent is reported as infinity for Sliver of unknown size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CustomScrollView( diff --git a/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart b/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart index 745f2c28ff403..139077213f317 100644 --- a/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart +++ b/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart @@ -2,17 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +import '../rendering/sliver_utils.dart'; + const double VIEWPORT_HEIGHT = 600; const double VIEWPORT_WIDTH = 300; void main() { - testWidgets('SliverCrossAxisGroup is laid out properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverCrossAxisGroup is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, @@ -57,7 +63,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('SliverExpanded is laid out properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverExpanded is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( slivers: <Widget>[ @@ -95,7 +101,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('SliverConstrainedCrossAxis is laid out properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverConstrainedCrossAxis is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( slivers: <Widget>[ @@ -124,7 +130,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('Mix of slivers is laid out properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mix of slivers is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( slivers: <Widget>[ @@ -157,7 +163,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('Mix of slivers is laid out properly when horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mix of slivers is laid out properly when horizontal', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( scrollDirection: Axis.horizontal, @@ -212,7 +218,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('Mix of slivers is laid out properly when reversed horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mix of slivers is laid out properly when reversed horizontal', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( scrollDirection: Axis.horizontal, @@ -268,7 +274,7 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('Mix of slivers is laid out properly when reversed vertical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mix of slivers is laid out properly when reversed vertical', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( reverse: true, @@ -322,7 +328,7 @@ void main() { testWidgets('Assertion error when SliverExpanded is used outside of SliverCrossAxisGroup', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; - final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); await tester.pumpWidget( @@ -351,9 +357,10 @@ void main() { ); }); - testWidgets('Hit test works properly on various parts of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hit test works properly on various parts of SliverCrossAxisGroup', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); String? clickedTile; @@ -422,7 +429,7 @@ void main() { expect(clickedTile, equals('Group 1 Tile 2')); }); - testWidgets('Constrained sliver takes up remaining space', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Constrained sliver takes up remaining space', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); await tester.pumpWidget(_buildSliverCrossAxisGroup( slivers: <Widget>[ @@ -451,9 +458,9 @@ void main() { expect(renderGroup.geometry!.scrollExtent, equals(300 * 20)); }); - testWidgets('Assertion error when constrained widget runs out of cross axis extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assertion error when constrained widget runs out of cross axis extent', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; - final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); final List<int> items = List<int>.generate(20, (int i) => i); @@ -473,9 +480,9 @@ void main() { ); }); - testWidgets('Assertion error when expanded widget runs out of cross axis extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Assertion error when expanded widget runs out of cross axis extent', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; - final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); final List<int> items = List<int>.generate(20, (int i) => i); @@ -496,7 +503,7 @@ void main() { ); }); - testWidgets('applyPaintTransform is implemented properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applyPaintTransform is implemented properly', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverCrossAxisGroup( slivers: <Widget>[ const SliverToBoxAdapter(child: Text('first box')), @@ -512,8 +519,9 @@ void main() { expect(second.localToGlobal(Offset.zero), const Offset(VIEWPORT_WIDTH / 2, 0)); }); - testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedPersistentHeader is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -537,8 +545,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-20.0)); }); - testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -566,8 +575,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -596,8 +606,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -632,8 +643,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverPinnedFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedFloatingPersistentHeader is painted within bounds of SliverCrossAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -669,8 +681,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverAppBar with floating: false, pinned: false, snap: false is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: false, pinned: false, snap: false is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -698,8 +711,9 @@ void main() { expect(renderHeader.geometry!.paintExtent, equals(0.0)); }); - testWidgets('SliverAppBar with floating: true, pinned: false, snap: true is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: true, pinned: false, snap: true is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -738,8 +752,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); - testWidgets('SliverAppBar with floating: true, pinned: true, snap: true is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: true, pinned: true, snap: true is painted within bounds of SliverCrossAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -778,8 +793,9 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); - testWidgets('SliverFloatingPersistentHeader scroll direction is not affected by controller.jumpTo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFloatingPersistentHeader scroll direction is not affected by controller.jumpTo', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverCrossAxisGroup( controller: controller, slivers: <Widget>[ @@ -806,8 +822,64 @@ void main() { // If renderHeader._lastStartedScrollDirection is not ScrollDirection.forward, then we shouldn't see the header at all. expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); + + testWidgetsWithLeakTracking('SliverCrossAxisGroup skips painting invisible children', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + + int counter = 0; + void incrementCounter() { + counter += 1; + } + + await tester.pumpWidget( + _buildSliverCrossAxisGroup( + controller: controller, + slivers: <Widget>[ + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 1000, + decoration: const BoxDecoration(color: Colors.amber), + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 400, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 500, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 300, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + ], + ), + ); + expect(counter, equals(4)); + + // Reset paint counter. + counter = 0; + controller.jumpTo(400); + await tester.pumpAndSettle(); + + expect(controller.offset, 400); + expect(counter, equals(2)); + }); } + Widget _buildSliverList({ double itemMainAxisExtent = 100, List<int> items = const <int>[], diff --git a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart index f0279e27437fc..a1965c49f76e9 100644 --- a/packages/flutter/test/widgets/sliver_fill_remaining_test.dart +++ b/packages/flutter/test/widgets/sliver_fill_remaining_test.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { @@ -39,8 +40,9 @@ void main() { group('SliverFillRemaining', () { group('hasScrollBody: true, default', () { - testWidgets('no siblings', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no siblings', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -79,8 +81,9 @@ void main() { ); }); - testWidgets('one sibling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('one sibling', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -120,8 +123,9 @@ void main() { ); }); - testWidgets('scrolls beyond viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('scrolls beyond viewportMainAxisExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -139,8 +143,9 @@ void main() { }); group('hasScrollBody: false', () { - testWidgets('does not extend past viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not extend past viewportMainAxisExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -158,7 +163,7 @@ void main() { expect(find.byType(Container), findsNWidgets(2)); }); - testWidgets('child without size is sized by extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child without size is sized by extent', (WidgetTester tester) async { final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -179,7 +184,7 @@ void main() { expect(box.size.width, equals(650)); }); - testWidgets('child with smaller size is sized by extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child with smaller size is sized by extent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ sliverBox, @@ -220,7 +225,7 @@ void main() { ); }); - testWidgets('extent is overridden by child with larger size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extent is overridden by child with larger size', (WidgetTester tester) async { final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -244,7 +249,7 @@ void main() { expect(box.size.width, equals(1000)); }); - testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ SliverFixedExtentList( @@ -285,7 +290,7 @@ void main() { expect(tester.getCenter(button).dx, equals(400.0)); }); - testWidgets('alignment with a flexible works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alignment with a flexible works', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ sliverBox, @@ -354,7 +359,7 @@ void main() { }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); group('fillOverscroll: true, relevant platforms', () { - testWidgets('child without size is sized by extent and overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child without size is sized by extent and overscroll', (WidgetTester tester) async { final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -381,7 +386,7 @@ void main() { expect(box3.size.height, equals(450)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('child with smaller size is overridden and sized by extent and overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child with smaller size is overridden and sized by extent and overscroll', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ sliverBox, @@ -428,9 +433,10 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('extent is overridden by child size and overscroll if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extent is overridden by child size and overscroll if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ SliverFixedExtentList( itemExtent: 150, @@ -492,9 +498,10 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('fillOverscroll works when child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fillOverscroll works when child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ SliverFixedExtentList( itemExtent: 150, @@ -554,7 +561,7 @@ void main() { ); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('alignment with a flexible works with fillOverscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('alignment with a flexible works with fillOverscroll', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ sliverBox, @@ -648,7 +655,7 @@ void main() { group('fillOverscroll: true, is ignored on irrelevant platforms', () { // Android/Other scroll physics when hasScrollBody: false, ignores fillOverscroll: true - testWidgets('child without size is sized by extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child without size is sized by extent', (WidgetTester tester) async { final List<Widget> slivers = <Widget>[ sliverBox, SliverFillRemaining( @@ -667,7 +674,7 @@ void main() { expect(box2.size.height, equals(450)); }); - testWidgets('child with size is overridden and sized by extent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child with size is overridden and sized by extent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final List<Widget> slivers = <Widget>[ sliverBox, @@ -706,9 +713,10 @@ void main() { expect(tester.getCenter(button).dx, equals(400.0)); }); - testWidgets('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('extent is overridden by child size if precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ SliverFixedExtentList( itemExtent: 150, @@ -763,9 +771,10 @@ void main() { expect(tester.getCenter(button).dx, equals(400.0)); }); - testWidgets('child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('child has no size and precedingScrollExtent > viewportMainAxisExtent', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final List<Widget> slivers = <Widget>[ SliverFixedExtentList( itemExtent: 150, diff --git a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart index a08d235a979ef..a17abfb90a7f0 100644 --- a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart +++ b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SliverFillViewport control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFillViewport control test', (WidgetTester tester) async { final List<Widget> children = List<Widget>.generate(20, (int i) { return ColoredBox(color: Colors.green, child: Text('$i', textDirection: TextDirection.ltr)); }); @@ -158,7 +159,7 @@ void main() { ); }); - testWidgets('SliverFillViewport padding test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFillViewport padding test', (WidgetTester tester) async { final SliverChildListDelegate delegate = SliverChildListDelegate( <Widget>[ const Text('0'), diff --git a/packages/flutter/test/widgets/sliver_list_test.dart b/packages/flutter/test/widgets/sliver_list_test.dart index 1131e4b43b0a4..bad587cdfa691 100644 --- a/packages/flutter/test/widgets/sliver_list_test.dart +++ b/packages/flutter/test/widgets/sliver_list_test.dart @@ -4,15 +4,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SliverList reverse children (with keys)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList reverse children (with keys)', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); const double itemHeight = 300.0; const double viewportHeight = 500.0; const double scrollPosition = 18 * itemHeight; final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverList( items: items, @@ -53,13 +55,14 @@ void main() { expect(find.text('Tile 0'), findsNothing); }); - testWidgets('SliverList replace children (with keys)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList replace children (with keys)', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); const double itemHeight = 300.0; const double viewportHeight = 500.0; const double scrollPosition = 18 * itemHeight; final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverList( items: items, @@ -105,13 +108,14 @@ void main() { expect(find.text('Tile 119'), findsNothing); }); - testWidgets('SliverList replace with shorter children list (with keys)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList replace with shorter children list (with keys)', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); const double itemHeight = 300.0; const double viewportHeight = 500.0; final double scrollPosition = items.length * itemHeight - viewportHeight; final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); + addTearDown(controller.dispose); await tester.pumpWidget(_buildSliverList( items: items, @@ -145,28 +149,33 @@ void main() { expect(find.text('Tile 19'), findsNothing); }); - testWidgets('SliverList should layout first child in case of child reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList should layout first child in case of child reordering', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/35904. List<String> items = <String>['1', '2']; - - await tester.pumpWidget(_buildSliverListRenderWidgetChild(items)); + final ScrollController controller1 = ScrollController(); + addTearDown(controller1.dispose); + await tester.pumpWidget(_buildSliverListRenderWidgetChild(items, controller1)); await tester.pumpAndSettle(); expect(find.text('Tile 1'), findsOneWidget); expect(find.text('Tile 2'), findsOneWidget); items = items.reversed.toList(); - await tester.pumpWidget(_buildSliverListRenderWidgetChild(items)); + final ScrollController controller2 = ScrollController(); + addTearDown(controller2.dispose); + await tester.pumpWidget(_buildSliverListRenderWidgetChild(items, controller2)); await tester.pumpAndSettle(); expect(find.text('Tile 1'), findsOneWidget); expect(find.text('Tile 2'), findsOneWidget); }); - testWidgets('SliverList should recalculate inaccurate layout offset case 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList should recalculate inaccurate layout offset case 1', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/42142. final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( _buildSliverList( items: List<int>.from(items), @@ -223,10 +232,12 @@ void main() { }); - testWidgets('SliverList should recalculate inaccurate layout offset case 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList should recalculate inaccurate layout offset case 2', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/42142. final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( _buildSliverList( items: List<int>.from(items), @@ -275,10 +286,12 @@ void main() { expect(find.text('Tile 3'), findsOneWidget); }); - testWidgets('SliverList should start to perform layout from the initial child when there is no valid offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList should start to perform layout from the initial child when there is no valid offset', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/66198. bool isShow = true; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + Widget buildSliverList(ScrollController controller) { return Directionality( textDirection: TextDirection.ltr, @@ -335,7 +348,7 @@ void main() { }); } -Widget _buildSliverListRenderWidgetChild(List<String> items) { +Widget _buildSliverListRenderWidgetChild(List<String> items, ScrollController controller) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, @@ -343,7 +356,7 @@ Widget _buildSliverListRenderWidgetChild(List<String> items) { child: SizedBox( height: 500, child: CustomScrollView( - controller: ScrollController(), + controller: controller, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate( diff --git a/packages/flutter/test/widgets/sliver_main_axis_group_test.dart b/packages/flutter/test/widgets/sliver_main_axis_group_test.dart index 46d518589d381..9ad4699af5910 100644 --- a/packages/flutter/test/widgets/sliver_main_axis_group_test.dart +++ b/packages/flutter/test/widgets/sliver_main_axis_group_test.dart @@ -5,14 +5,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +import '../rendering/sliver_utils.dart'; + const double VIEWPORT_HEIGHT = 600; const double VIEWPORT_WIDTH = 300; void main() { - testWidgets('SliverMainAxisGroup is laid out properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverMainAxisGroup is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _buildSliverMainAxisGroup( @@ -61,9 +66,10 @@ void main() { expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); - testWidgets('SliverMainAxisGroup is laid out properly when reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverMainAxisGroup is laid out properly when reversed', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _buildSliverMainAxisGroup( @@ -113,9 +119,10 @@ void main() { expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); - testWidgets('SliverMainAxisGroup is laid out properly when horizontal', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverMainAxisGroup is laid out properly when horizontal', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _buildSliverMainAxisGroup( @@ -170,9 +177,10 @@ void main() { expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); - testWidgets('SliverMainAxisGroup is laid out properly when horizontal, reversed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverMainAxisGroup is laid out properly when horizontal, reversed', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( _buildSliverMainAxisGroup( @@ -228,9 +236,10 @@ void main() { expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); - testWidgets('Hit test works properly on various parts of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hit test works properly on various parts of SliverMainAxisGroup', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); String? clickedTile; @@ -300,7 +309,7 @@ void main() { expect(clickedTile, equals('Group 1 Tile 2')); }); - testWidgets('applyPaintTransform is implemented properly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('applyPaintTransform is implemented properly', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverMainAxisGroup( slivers: <Widget>[ const SliverToBoxAdapter(child: Text('first box')), @@ -316,8 +325,10 @@ void main() { expect(second.localToGlobal(Offset.zero), Offset(0, first.size.height)); }); - testWidgets('visitChildrenForSemantics visits children in the correct order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('visitChildrenForSemantics visits children in the correct order', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: const <Widget>[ @@ -341,8 +352,10 @@ void main() { expect(visitedChildren[1].geometry!.scrollExtent, equals(500)); }); - testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -368,8 +381,10 @@ void main() { }); - testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -397,8 +412,10 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -426,8 +443,10 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -462,8 +481,10 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverPinnedFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPinnedFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -499,8 +520,10 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); - testWidgets('SliverAppBar with floating: false, pinned: false, snap: false is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: false, pinned: false, snap: false is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -529,8 +552,10 @@ void main() { expect(renderHeader.geometry!.layoutExtent, equals(0.0)); }); - testWidgets('SliverAppBar with floating: true, pinned: false, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: true, pinned: false, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -569,8 +594,10 @@ void main() { expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); - testWidgets('SliverAppBar with floating: true, pinned: true, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar with floating: true, pinned: true, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ @@ -608,6 +635,64 @@ void main() { expect(renderHeader.geometry!.paintExtent, equals(60.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); + + testWidgetsWithLeakTracking('SliverMainAxisGroup skips painting invisible children', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + + int counter = 0; + void incrementCounter() { + counter += 1; + } + + await tester.pumpWidget( + _buildSliverMainAxisGroup( + controller: controller, + slivers: <Widget>[ + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 1000, + decoration: const BoxDecoration(color: Colors.amber), + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 400, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 500, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 300, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + ], + ), + ); + + // Can only see top sliver. + expect(counter, equals(1)); + + // Reset paint counter. + counter = 0; + controller.jumpTo(1000); + await tester.pumpAndSettle(); + + // Can only see second and third slivers. + expect(controller.offset, 1000); + expect(counter, equals(2)); + }); } Widget _buildSliverList({ diff --git a/packages/flutter/test/widgets/sliver_persistent_header_test.dart b/packages/flutter/test/widgets/sliver_persistent_header_test.dart index 8fb239f7a52d5..26f8cb2f4ec9a 100644 --- a/packages/flutter/test/widgets/sliver_persistent_header_test.dart +++ b/packages/flutter/test/widgets/sliver_persistent_header_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/rendering/sliver_persistent_header.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets( + testWidgetsWithLeakTracking( '_SliverScrollingPersistentHeader should update stretchConfiguration', (WidgetTester tester) async { for (final double stretchTriggerOffset in <double>[10.0, 20.0]) { @@ -37,7 +38,7 @@ void main() { expect(render.stretchConfiguration?.stretchTriggerOffset, 20); }); - testWidgets( + testWidgetsWithLeakTracking( '_SliverPinnedPersistentHeader should update stretchConfiguration', (WidgetTester tester) async { for (final double stretchTriggerOffset in <double>[10.0, 20.0]) { @@ -68,7 +69,7 @@ void main() { expect(render.stretchConfiguration?.stretchTriggerOffset, 20); }); - testWidgets( + testWidgetsWithLeakTracking( '_SliverPinnedPersistentHeader should update showOnScreenConfiguration', (WidgetTester tester) async { for (final double maxShowOnScreenExtent in <double>[1000, 2000]) { diff --git a/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart index 48b000f5eb477..c7e9ff7409f40 100644 --- a/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart +++ b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestItem extends StatelessWidget { const TestItem({ super.key, required this.item, this.width, this.height }); @@ -40,7 +41,7 @@ Widget buildFrame({ int? count, double? width, double? height, Axis? scrollDirec } void main() { - testWidgets('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -69,7 +70,7 @@ void main() { } }); - testWidgets('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -95,7 +96,7 @@ void main() { expect(find.text('Item 7'), findsNothing); }); - testWidgets('SliverPrototypeExtentList vertical scrolling basics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList vertical scrolling basics', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(count: 20, height: 100.0)); // The viewport is 600 pixels high, lazily created items are 100 pixels high. @@ -121,7 +122,7 @@ void main() { } }); - testWidgets('SliverPrototypeExtentList horizontal scrolling basics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList horizontal scrolling basics', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(count: 20, width: 100.0, scrollDirection: Axis.horizontal)); // The viewport is 800 pixels wide, lazily created items are 100 pixels wide. @@ -147,7 +148,7 @@ void main() { } }); - testWidgets('SliverPrototypeExtentList change the prototype item', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList change the prototype item', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(count: 10, height: 60.0)); // The viewport is 600 pixels high, each of the 10 items is 60 pixels high @@ -173,7 +174,7 @@ void main() { } }); - testWidgets('SliverPrototypeExtentList first item is also the prototype', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList first item is also the prototype', (WidgetTester tester) async { final List<Widget> items = List<Widget>.generate(10, (int index) { return TestItem(key: ValueKey<int>(index), item: index, height: index == 0 ? 60.0 : null); }).toList(); @@ -203,7 +204,7 @@ void main() { } }); - testWidgets('SliverPrototypeExtentList prototypeItem paint transform is zero.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPrototypeExtentList prototypeItem paint transform is zero.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/67117 // This test ensures that the SliverPrototypeExtentList does not cause an // assertion error when calculating the paint transform of its prototypeItem. diff --git a/packages/flutter/test/widgets/sliver_semantics_test.dart b/packages/flutter/test/widgets/sliver_semantics_test.dart index 9ccafcf316fbe..24e599281fd5e 100644 --- a/packages/flutter/test/widgets/sliver_semantics_test.dart +++ b/packages/flutter/test/widgets/sliver_semantics_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; @@ -19,12 +20,13 @@ void main() { } void _tests() { - testWidgets('excludeFromScrollable works correctly', (WidgetTester tester) async { + testWidgetsWithLeakTracking('excludeFromScrollable works correctly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const double appBarExpandedHeight = 200.0; final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); final List<Widget> listChildren = List<Widget>.generate(30, (int i) { return SizedBox( height: appBarExpandedHeight, @@ -281,7 +283,7 @@ void _tests() { semantics.dispose(); }); - testWidgets('Offscreen sliver are hidden in semantics tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Offscreen sliver are hidden in semantics tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const double containerHeight = 200.0; @@ -289,6 +291,7 @@ void _tests() { final ScrollController scrollController = ScrollController( initialScrollOffset: containerHeight * 1.5, ); + addTearDown(scrollController.dispose); final List<Widget> slivers = List<Widget>.generate(30, (int i) { return SliverToBoxAdapter( child: SizedBox( @@ -373,7 +376,7 @@ void _tests() { semantics.dispose(); }); - testWidgets('SemanticsNodes of Slivers are in paint order', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNodes of Slivers are in paint order', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<Widget> slivers = List<Widget>.generate(5, (int i) { @@ -453,7 +456,7 @@ void _tests() { semantics.dispose(); }); - testWidgets('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<Widget> listChildren = List<Widget>.generate(10, (int i) { @@ -463,6 +466,7 @@ void _tests() { ); }); final ScrollController controller = ScrollController(initialScrollOffset: 280.0); + addTearDown(controller.dispose); await tester.pumpWidget(Semantics( textDirection: TextDirection.ltr, child: Localizations( @@ -564,10 +568,11 @@ void _tests() { semantics.dispose(); }); - testWidgets('Slivers fully covered by another overlapping sliver are hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slivers fully covered by another overlapping sliver are hidden', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(initialScrollOffset: 280.0); + addTearDown(controller.dispose); final List<Widget> slivers = List<Widget>.generate(10, (int i) { return SliverToBoxAdapter( child: SizedBox( @@ -675,7 +680,7 @@ void _tests() { semantics.dispose(); }); - testWidgets('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded (reverse)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNodes of a sliver fully covered by another overlapping sliver are excluded (reverse)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<Widget> listChildren = List<Widget>.generate(10, (int i) { @@ -685,6 +690,7 @@ void _tests() { ); }); final ScrollController controller = ScrollController(initialScrollOffset: 280.0); + addTearDown(controller.dispose); await tester.pumpWidget(Semantics( textDirection: TextDirection.ltr, child: Localizations( @@ -789,10 +795,11 @@ void _tests() { semantics.dispose(); }); - testWidgets('Slivers fully covered by another overlapping sliver are hidden (reverse)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slivers fully covered by another overlapping sliver are hidden (reverse)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(initialScrollOffset: 280.0); + addTearDown(controller.dispose); final List<Widget> slivers = List<Widget>.generate(10, (int i) { return SliverToBoxAdapter( child: SizedBox( @@ -903,10 +910,11 @@ void _tests() { semantics.dispose(); }); - testWidgets('Slivers fully covered by another overlapping sliver are hidden (with center sliver)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Slivers fully covered by another overlapping sliver are hidden (with center sliver)', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final ScrollController controller = ScrollController(initialScrollOffset: 280.0); + addTearDown(controller.dispose); final GlobalKey forwardAppBarKey = GlobalKey(debugLabel: 'forward app bar'); final List<Widget> forwardChildren = List<Widget>.generate(10, (int i) { return SizedBox( diff --git a/packages/flutter/test/widgets/sliver_visibility_test.dart b/packages/flutter/test/widgets/sliver_visibility_test.dart index bb02a1ceb6d2c..af9148d5dca57 100644 --- a/packages/flutter/test/widgets/sliver_visibility_test.dart +++ b/packages/flutter/test/widgets/sliver_visibility_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; class TestState extends StatefulWidget { @@ -30,7 +30,7 @@ class _TestStateState extends State<TestState> { } void main() { - testWidgets('SliverVisibility', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverVisibility', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> log = <String>[]; const Key anchor = Key('drag'); diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart index ff3ec8c8030ba..45c7922b55ecf 100644 --- a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Sliver appBars - floating and pinned - correct elevation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appBars - floating and pinned - correct elevation', (WidgetTester tester) async { await tester.pumpWidget(Localizations( locale: const Locale('en', 'us'), delegates: const <LocalizationsDelegate<dynamic>>[ @@ -46,7 +47,7 @@ void main() { expect(renderObject.elevation, 0.0); }); - testWidgets('Sliver appbars - floating and pinned - correct semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating and pinned - correct semantics', (WidgetTester tester) async { await tester.pumpWidget( Localizations( locale: const Locale('en', 'us'), @@ -241,8 +242,10 @@ void main() { semantics.dispose(); }); - testWidgets('Sliver appbars - floating and pinned - second app bar stacks below', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating and pinned - second app bar stacks below', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -292,10 +295,12 @@ void main() { expect(tester.getTopLeft(find.text('E')), Offset(0.0, 200.0 + 56.0 + cSize.height * 2.0 + 500.0 - 600.0)); }); - testWidgets('Does not crash when there is less than minExtent remainingPaintExtent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Does not crash when there is less than minExtent remainingPaintExtent', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/21887. final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); const double availableHeight = 50.0; + await tester.pumpWidget( MaterialApp( home: Center( @@ -337,7 +342,7 @@ void main() { expect(render.geometry!.layoutExtent, 0.0); }); - testWidgets('Pinned and floating SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pinned and floating SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -371,7 +376,7 @@ void main() { expect(render.geometry!.paintOrigin, -scrollDistance); }); - testWidgets('Floating SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Floating SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -404,7 +409,7 @@ void main() { expect(render.geometry!.paintOrigin, -scrollDistance); }); - testWidgets('Pinned SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Pinned SliverAppBar sticks to top the content is scroll down', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_test.dart index 01e32544b3699..de3fb1b2b379b 100644 --- a/packages/flutter/test/widgets/slivers_appbar_floating_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_floating_test.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) { final RenderSliver target = key.currentContext!.findRenderObject()! as RenderSliver; @@ -25,7 +26,7 @@ void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect } void main() { - testWidgets("Sliver appbars - floating - scroll offset doesn't change", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Sliver appbars - floating - scroll offset doesn't change", (WidgetTester tester) async { const double bigHeight = 1000.0; await tester.pumpWidget( Directionality( @@ -53,7 +54,7 @@ void main() { expect(position.maxScrollExtent, max); }); - testWidgets('Sliver appbars - floating - normal behavior works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating - normal behavior works', (WidgetTester tester) async { final TestDelegate delegate = TestDelegate(); const double bigHeight = 1000.0; GlobalKey key1, key2, key3; @@ -125,7 +126,7 @@ void main() { verifyPaintPosition(key3, Offset.zero, true); }); - testWidgets('Sliver appbars - floating - no floating behavior when animating', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating - no floating behavior when animating', (WidgetTester tester) async { final TestDelegate delegate = TestDelegate(); const double bigHeight = 1000.0; GlobalKey key1, key2, key3; @@ -160,7 +161,7 @@ void main() { verifyPaintPosition(key3, Offset.zero, true); }); - testWidgets('Sliver appbars - floating - floating behavior when dragging down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating - floating behavior when dragging down', (WidgetTester tester) async { final TestDelegate delegate = TestDelegate(); const double bigHeight = 1000.0; GlobalKey key1, key2, key3; @@ -197,7 +198,7 @@ void main() { verifyPaintPosition(key3, Offset.zero, true); }); - testWidgets('Sliver appbars - floating - overscroll gap is below header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - floating - overscroll gap is below header', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -258,7 +259,7 @@ void main() { expect(geometry.paintExtent, paintExtent); } - testWidgets('SliverAppBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverAppBar', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildTest(SliverAppBar( key: appBarKey, @@ -312,7 +313,7 @@ void main() { verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true); }); - testWidgets('SliverPersistentHeader', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPersistentHeader', (WidgetTester tester) async { final GlobalKey headerKey = GlobalKey(); await tester.pumpWidget(buildTest(SliverPersistentHeader( key: headerKey, @@ -354,7 +355,7 @@ void main() { verifyGeometry(key: headerKey, paintExtent: 56.0, visible: true); }); - testWidgets('and snapping SliverAppBar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('and snapping SliverAppBar', (WidgetTester tester) async { final GlobalKey appBarKey = GlobalKey(); await tester.pumpWidget(buildTest(SliverAppBar( key: appBarKey, diff --git a/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart index 2651ec271d3c3..8c57dad28c3e8 100644 --- a/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) { final RenderSliver target = key.currentContext!.findRenderObject()! as RenderSliver; @@ -23,7 +24,7 @@ void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect } void main() { - testWidgets('Sliver appbars - pinned', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - pinned', (WidgetTester tester) async { const double bigHeight = 550.0; GlobalKey key1, key2, key3, key4, key5; await tester.pumpWidget( @@ -59,7 +60,7 @@ void main() { verifyPaintPosition(key5, const Offset(0.0, 50.0), true); }); - testWidgets('Sliver appbars - toStringDeep of maxExtent that throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - toStringDeep of maxExtent that throws', (WidgetTester tester) async { final TestDelegateThatCanThrow delegateThatCanThrow = TestDelegateThatCanThrow(); GlobalKey key; await tester.pumpWidget( @@ -121,7 +122,7 @@ void main() { ); }); - testWidgets('Sliver appbars - pinned with slow scroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - pinned with slow scroll', (WidgetTester tester) async { const double bigHeight = 550.0; GlobalKey key1, key2, key3, key4, key5; await tester.pumpWidget( @@ -214,7 +215,7 @@ void main() { verifyPaintPosition(key5, const Offset(0.0, 550.0), true); }); - testWidgets('Sliver appbars - pinned with less overlap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - pinned with less overlap', (WidgetTester tester) async { const double bigHeight = 650.0; GlobalKey key1, key2, key3, key4, key5; await tester.pumpWidget( @@ -250,7 +251,7 @@ void main() { verifyPaintPosition(key5, Offset.zero, true); }); - testWidgets('Sliver appbars - overscroll gap is below header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - overscroll gap is below header', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart b/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart index 9042575fc9b7b..5b2e7f77af663 100644 --- a/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void verifyPaintPosition(GlobalKey key, Offset ideal) { final RenderObject target = key.currentContext!.findRenderObject()!; @@ -15,7 +16,7 @@ void verifyPaintPosition(GlobalKey key, Offset ideal) { } void main() { - testWidgets('Sliver appbars - scrolling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - scrolling', (WidgetTester tester) async { GlobalKey key1, key2, key3, key4, key5; await tester.pumpWidget( Directionality( @@ -50,7 +51,7 @@ void main() { verifyPaintPosition(key5, const Offset(0.0, 50.0)); }); - testWidgets('Sliver appbars - scrolling off screen', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - scrolling off screen', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final TestDelegate delegate = TestDelegate(); await tester.pumpWidget( @@ -74,7 +75,7 @@ void main() { expect(rect, equals(const Rect.fromLTWH(0.0, -195.0, 800.0, 200.0))); }); - testWidgets('Sliver appbars - scrolling - overscroll gap is below header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars - scrolling - overscroll gap is below header', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -106,7 +107,7 @@ void main() { expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0)); }); - testWidgets('Sliver appbars const child delegate - scrolling - overscroll gap is below header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver appbars const child delegate - scrolling - overscroll gap is below header', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/slivers_appbar_stretch_test.dart b/packages/flutter/test/widgets/slivers_appbar_stretch_test.dart index 74137bf010549..6ffccf5e09ead 100644 --- a/packages/flutter/test/widgets/slivers_appbar_stretch_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_stretch_test.dart @@ -5,10 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { group('SliverAppBar - Stretch', () { - testWidgets('fills overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -43,7 +44,7 @@ void main() { expect(header.child!.size.height, equals(200.0)); }); - testWidgets('fills overscroll after reverse direction input - scrolling header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll after reverse direction input - scrolling header', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -92,7 +93,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('fills overscroll after reverse direction input - floating header', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll after reverse direction input - floating header', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -142,7 +143,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not stretch without overscroll physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -177,7 +178,7 @@ void main() { expect(header.child!.size.height, equals(100.0)); }); - testWidgets('default trigger offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('default trigger offset', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( @@ -215,7 +216,7 @@ void main() { expect(didTrigger, isTrue); }); - testWidgets('custom trigger offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('custom trigger offset', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( @@ -254,7 +255,7 @@ void main() { expect(didTrigger, isTrue); }); - testWidgets('stretch callback not triggered without overscroll physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('stretch callback not triggered without overscroll physics', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( @@ -293,7 +294,7 @@ void main() { expect(didTrigger, isFalse); }); - testWidgets('asserts reasonable trigger offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('asserts reasonable trigger offset', (WidgetTester tester) async { expect( () { return MaterialApp( @@ -325,7 +326,7 @@ void main() { }); group('SliverAppBar - Stretch, Pinned', () { - testWidgets('fills overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -360,7 +361,7 @@ void main() { expect(header.child!.size.height, equals(200.0)); }); - testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not stretch without overscroll physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -397,7 +398,7 @@ void main() { }); group('SliverAppBar - Stretch, Floating', () { - testWidgets('fills overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -432,7 +433,7 @@ void main() { expect(header.child!.size.height, equals(200.0)); }); - testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not fill overscroll without proper physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -469,7 +470,7 @@ void main() { }); group('SliverAppBar - Stretch, Floating, Pinned', () { - testWidgets('fills overscroll', (WidgetTester tester) async { + testWidgetsWithLeakTracking('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( @@ -505,7 +506,7 @@ void main() { expect(header.child!.size.height, equals(200.0)); }); - testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not fill overscroll without proper physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/slivers_block_global_key_test.dart b/packages/flutter/test/widgets/slivers_block_global_key_test.dart index 3b754cb602d6b..495d8c211d770 100644 --- a/packages/flutter/test/widgets/slivers_block_global_key_test.dart +++ b/packages/flutter/test/widgets/slivers_block_global_key_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; int globalGeneration = 0; @@ -25,13 +26,15 @@ class _GenerationTextState extends State<GenerationText> { // Creates a SliverList with `keys.length` children and each child having a key from `keys` and a text of `key:generation`. // The generation is increased with every call to this method. Future<void> test(WidgetTester tester, double offset, List<int> keys) { + final ViewportOffset viewportOffset = ViewportOffset.fixed(offset); + addTearDown(viewportOffset.dispose); globalGeneration += 1; return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( cacheExtent: 0.0, - offset: ViewportOffset.fixed(offset), + offset: viewportOffset, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(keys.map<Widget>((int key) { @@ -59,7 +62,7 @@ void verify(WidgetTester tester, List<Offset> answerKey, String text) { } void main() { - testWidgets('Viewport+SliverBlock with GlobalKey reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverBlock with GlobalKey reparenting', (WidgetTester tester) async { await test(tester, 0.0, <int>[1,2,3,4,5,6,7,8,9]); verify(tester, <Offset>[ Offset.zero, diff --git a/packages/flutter/test/widgets/slivers_block_test.dart b/packages/flutter/test/widgets/slivers_block_test.dart index 7b7e771458b90..b4b75211cc1ec 100644 --- a/packages/flutter/test/widgets/slivers_block_test.dart +++ b/packages/flutter/test/widgets/slivers_block_test.dart @@ -5,15 +5,16 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; Future<void> test(WidgetTester tester, double offset) { + final ViewportOffset viewportOffset = ViewportOffset.fixed(offset); + addTearDown(viewportOffset.dispose); return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(offset), + offset: viewportOffset, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(const <Widget>[ @@ -31,11 +32,13 @@ Future<void> test(WidgetTester tester, double offset) { } Future<void> testWithConstChildDelegate(WidgetTester tester, double offset) { + final ViewportOffset viewportOffset = ViewportOffset.fixed(offset); + addTearDown(viewportOffset.dispose); return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(offset), + offset: viewportOffset, slivers: const <Widget>[ SliverList( delegate: SliverChildListDelegate.fixed(<Widget>[ @@ -65,7 +68,7 @@ void verify(WidgetTester tester, List<Offset> answerKey, String text) { } void main() { - testWidgets('Viewport+SliverBlock basic test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverBlock basic test', (WidgetTester tester) async { await test(tester, 0.0); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); verify(tester, <Offset>[ @@ -98,7 +101,7 @@ void main() { ], 'ab'); }); - testWidgets('Viewport+SliverBlock basic test with constant SliverChildListDelegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverBlock basic test with constant SliverChildListDelegate', (WidgetTester tester) async { await testWithConstChildDelegate(tester, 0.0); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); verify(tester, <Offset>[ @@ -131,9 +134,10 @@ void main() { ], 'ab'); }); - testWidgets('Viewport with GlobalKey reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport with GlobalKey reparenting', (WidgetTester tester) async { final Key key1 = GlobalKey(); final ViewportOffset offset = ViewportOffset.zero(); + addTearDown(offset.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -244,12 +248,15 @@ void main() { ], 'acb'); }); - testWidgets('Viewport overflow clipping of SliverToBoxAdapter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport overflow clipping of SliverToBoxAdapter', (WidgetTester tester) async { + final ViewportOffset offset1 = ViewportOffset.zero(); + addTearDown(offset1.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.zero(), + offset: offset1, slivers: const <Widget>[ SliverToBoxAdapter( child: SizedBox(height: 400.0, child: Text('a')), @@ -261,11 +268,14 @@ void main() { expect(find.byType(Viewport), isNot(paints..clipRect())); + final ViewportOffset offset2 = ViewportOffset.fixed(100.0); + addTearDown(offset2.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(100.0), + offset: offset2, slivers: const <Widget>[ SliverToBoxAdapter( child: SizedBox(height: 400.0, child: Text('a')), @@ -277,11 +287,14 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); + final ViewportOffset offset3 = ViewportOffset.fixed(100.0); + addTearDown(offset3.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(100.0), + offset: offset3, slivers: const <Widget>[ SliverToBoxAdapter( child: SizedBox(height: 4000.0, child: Text('a')), @@ -293,11 +306,14 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); + final ViewportOffset offset4 = ViewportOffset.zero(); + addTearDown(offset4.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.zero(), + offset: offset4, slivers: const <Widget>[ SliverToBoxAdapter( child: SizedBox(height: 4000.0, child: Text('a')), @@ -310,12 +326,15 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); }); - testWidgets('Viewport overflow clipping of SliverBlock', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport overflow clipping of SliverBlock', (WidgetTester tester) async { + final ViewportOffset offset1 = ViewportOffset.zero(); + addTearDown(offset1.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.zero(), + offset: offset1, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(const <Widget>[ @@ -329,11 +348,14 @@ void main() { expect(find.byType(Viewport), isNot(paints..clipRect())); + final ViewportOffset offset2 = ViewportOffset.fixed(100.0); + addTearDown(offset2.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(100.0), + offset: offset2, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(const <Widget>[ @@ -347,11 +369,14 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); + final ViewportOffset offset3 = ViewportOffset.fixed(100.0); + addTearDown(offset3.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(100.0), + offset: offset3, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(const <Widget>[ @@ -365,11 +390,14 @@ void main() { expect(find.byType(Viewport), paints..clipRect()); + final ViewportOffset offset4 = ViewportOffset.zero(); + addTearDown(offset4.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.zero(), + offset: offset4, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(const <Widget>[ diff --git a/packages/flutter/test/widgets/slivers_evil_test.dart b/packages/flutter/test/widgets/slivers_evil_test.dart index 2c9aa0ef35366..d821c969ff47d 100644 --- a/packages/flutter/test/widgets/slivers_evil_test.dart +++ b/packages/flutter/test/widgets/slivers_evil_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { TestSliverPersistentHeaderDelegate(this._maxExtent); @@ -57,7 +58,7 @@ class TestScrollPhysics extends ClampingScrollPhysics { } void main() { - testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Evil test of sliver features - 1', (WidgetTester tester) async { final GlobalKey centerKey = GlobalKey(); await tester.pumpWidget( MediaQuery( @@ -184,7 +185,7 @@ void main() { }); - testWidgets('Removing offscreen items above and rescrolling does not crash', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Removing offscreen items above and rescrolling does not crash', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: CustomScrollView( cacheExtent: 0.0, diff --git a/packages/flutter/test/widgets/slivers_keepalive_test.dart b/packages/flutter/test/widgets/slivers_keepalive_test.dart index ae7dc37c4cc46..b75c5c405bc94 100644 --- a/packages/flutter/test/widgets/slivers_keepalive_test.dart +++ b/packages/flutter/test/widgets/slivers_keepalive_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Sliver with keep alive without key - should dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver with keep alive without key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0', keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), @@ -29,7 +30,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver without keep alive without key - should dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver without keep alive without key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0'), const WidgetTest1(text: 'child 1'), @@ -52,7 +53,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver without keep alive with key - should dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver without keep alive with key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey()), WidgetTest1(text: 'child 1', key: GlobalKey()), @@ -75,7 +76,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver with keep alive with key - should not dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver with keep alive with key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true), @@ -97,7 +98,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver with keep alive with Unique key - should not dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver with keep alive with Unique key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey(), keepAlive: true), @@ -119,7 +120,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver with keep alive with Value key - should not dispose after reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver with keep alive with Value key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0', key: ValueKey<int>(0), keepAlive: true), const WidgetTest1(text: 'child 1', key: ValueKey<int>(1), keepAlive: true), @@ -141,7 +142,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('Sliver complex case 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver complex case 1', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true), @@ -185,7 +186,7 @@ void main() { expect(state2.hasBeenDisposed, true); }); - testWidgets('Sliver complex case 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver complex case 2', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey()), @@ -228,7 +229,7 @@ void main() { expect(state2.hasBeenDisposed, true); }); - testWidgets('Sliver with SliverChildBuilderDelegate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver with SliverChildBuilderDelegate', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey()), @@ -271,7 +272,7 @@ void main() { expect(state2.hasBeenDisposed, true); }); - testWidgets('SliverFillViewport should not dispose widget with key during in screen reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFillViewport should not dispose widget with key during in screen reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey()), @@ -312,7 +313,7 @@ void main() { expect(state2.hasBeenDisposed, true); }); - testWidgets('SliverList should not dispose widget with key during in screen reordering', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList should not dispose widget with key during in screen reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), @@ -362,7 +363,7 @@ void main() { expect(state2.hasBeenDisposed, false); }); - testWidgets('SliverList remove child from child list', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList remove child from child list', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), diff --git a/packages/flutter/test/widgets/slivers_padding_test.dart b/packages/flutter/test/widgets/slivers_padding_test.dart index 619f024be53fd..8886cca1391a1 100644 --- a/packages/flutter/test/widgets/slivers_padding_test.dart +++ b/packages/flutter/test/widgets/slivers_padding_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class _MockRenderSliver extends RenderSliver { @override @@ -19,11 +20,13 @@ class _MockRenderSliver extends RenderSliver { } Future<void> test(WidgetTester tester, double offset, EdgeInsetsGeometry padding, AxisDirection axisDirection, TextDirection textDirection) { + final ViewportOffset viewportOffset = ViewportOffset.fixed(offset); + addTearDown(viewportOffset.dispose); return tester.pumpWidget( Directionality( textDirection: textDirection, child: Viewport( - offset: ViewportOffset.fixed(offset), + offset: viewportOffset, axisDirection: axisDirection, slivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(width: 400.0, height: 400.0, child: Text('before'))), @@ -50,7 +53,7 @@ void verify(WidgetTester tester, List<Rect> answerKey) { } void main() { - testWidgets('Viewport+SliverPadding basic test (VISUAL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding basic test (VISUAL)', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.fromLTRB(25.0, 20.0, 15.0, 35.0); await test(tester, 0.0, padding, AxisDirection.down, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -89,7 +92,7 @@ void main() { ]); }); - testWidgets('Viewport+SliverPadding basic test (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding basic test (LTR)', (WidgetTester tester) async { const EdgeInsetsDirectional padding = EdgeInsetsDirectional.fromSTEB(25.0, 20.0, 15.0, 35.0); await test(tester, 0.0, padding, AxisDirection.down, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -128,7 +131,7 @@ void main() { ]); }); - testWidgets('Viewport+SliverPadding basic test (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding basic test (RTL)', (WidgetTester tester) async { const EdgeInsetsDirectional padding = EdgeInsetsDirectional.fromSTEB(25.0, 20.0, 15.0, 35.0); await test(tester, 0.0, padding, AxisDirection.down, TextDirection.rtl); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -167,7 +170,7 @@ void main() { ]); }); - testWidgets('Viewport+SliverPadding hit testing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding hit testing', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); await test(tester, 350.0, padding, AxisDirection.down, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -189,7 +192,7 @@ void main() { expectIsTextSpan(result.path.first.target, 'after'); }); - testWidgets('Viewport+SliverPadding hit testing up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding hit testing up', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); await test(tester, 350.0, padding, AxisDirection.up, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -211,7 +214,7 @@ void main() { expectIsTextSpan(result.path.first.target, 'after'); }); - testWidgets('Viewport+SliverPadding hit testing left', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding hit testing left', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); await test(tester, 350.0, padding, AxisDirection.left, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -233,7 +236,7 @@ void main() { expectIsTextSpan(result.path.first.target, 'after'); }); - testWidgets('Viewport+SliverPadding hit testing right', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding hit testing right', (WidgetTester tester) async { const EdgeInsets padding = EdgeInsets.all(30.0); await test(tester, 350.0, padding, AxisDirection.right, TextDirection.ltr); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); @@ -255,12 +258,15 @@ void main() { expectIsTextSpan(result.path.first.target, 'after'); }); - testWidgets('Viewport+SliverPadding no child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding no child', (WidgetTester tester) async { + final ViewportOffset offset = ViewportOffset.fixed(0.0); + addTearDown(offset.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(0.0), + offset: offset, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.all(100.0)), SliverToBoxAdapter(child: SizedBox(width: 400.0, height: 400.0, child: Text('x'))), @@ -271,9 +277,10 @@ void main() { expect(tester.renderObject<RenderBox>(find.text('x')).localToGlobal(Offset.zero), const Offset(0.0, 200.0)); }); - testWidgets('SliverPadding with no child reports correct geometry as scroll offset changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPadding with no child reports correct geometry as scroll offset changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/64506 final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -299,13 +306,16 @@ void main() { ); }); - testWidgets('Viewport+SliverPadding changing padding', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding changing padding', (WidgetTester tester) async { + final ViewportOffset offset1 = ViewportOffset.fixed(0.0); + addTearDown(offset1.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.left, - offset: ViewportOffset.fixed(0.0), + offset: offset1, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(90.0, 1.0, 110.0, 2.0)), SliverToBoxAdapter(child: SizedBox(width: 201.0, child: Text('x'))), @@ -313,13 +323,18 @@ void main() { ), ), ); + expect(tester.renderObject<RenderBox>(find.text('x')).localToGlobal(Offset.zero), const Offset(399.0, 0.0)); + + final ViewportOffset offset2 = ViewportOffset.fixed(0.0); + addTearDown(offset2.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.left, - offset: ViewportOffset.fixed(0.0), + offset: offset2, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(110.0, 1.0, 80.0, 2.0)), SliverToBoxAdapter(child: SizedBox(width: 201.0, child: Text('x'))), @@ -327,77 +342,102 @@ void main() { ), ), ); + expect(tester.renderObject<RenderBox>(find.text('x')).localToGlobal(Offset.zero), const Offset(409.0, 0.0)); }); - testWidgets('Viewport+SliverPadding changing direction', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport+SliverPadding changing direction', (WidgetTester tester) async { + final ViewportOffset offset1 = ViewportOffset.fixed(0.0); + addTearDown(offset1.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.up, - offset: ViewportOffset.fixed(0.0), + offset: offset1, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(1.0, 2.0, 4.0, 8.0)), ], ), ), ); + expect(tester.renderObject<RenderSliverPadding>(find.byType(SliverPadding)).afterPadding, 2.0); + + final ViewportOffset offset2 = ViewportOffset.fixed(0.0); + addTearDown(offset2.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( - offset: ViewportOffset.fixed(0.0), + offset: offset2, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(1.0, 2.0, 4.0, 8.0)), ], ), ), ); + expect(tester.renderObject<RenderSliverPadding>(find.byType(SliverPadding)).afterPadding, 8.0); + + final ViewportOffset offset3 = ViewportOffset.fixed(0.0); + addTearDown(offset3.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.right, - offset: ViewportOffset.fixed(0.0), + offset: offset3, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(1.0, 2.0, 4.0, 8.0)), ], ), ), ); + expect(tester.renderObject<RenderSliverPadding>(find.byType(SliverPadding)).afterPadding, 4.0); + + final ViewportOffset offset4 = ViewportOffset.fixed(0.0); + addTearDown(offset4.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.left, - offset: ViewportOffset.fixed(0.0), + offset: offset4, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(1.0, 2.0, 4.0, 8.0)), ], ), ), ); + expect(tester.renderObject<RenderSliverPadding>(find.byType(SliverPadding)).afterPadding, 1.0); + + final ViewportOffset offset5 = ViewportOffset.fixed(99999.9); + addTearDown(offset5.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( axisDirection: AxisDirection.left, - offset: ViewportOffset.fixed(99999.9), + offset: offset5, slivers: const <Widget>[ SliverPadding(padding: EdgeInsets.fromLTRB(1.0, 2.0, 4.0, 8.0)), ], ), ), ); + expect(tester.renderObject<RenderSliverPadding>(find.byType(SliverPadding, skipOffstage: false)).afterPadding, 1.0); }); - testWidgets('SliverPadding propagates geometry offset corrections', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPadding propagates geometry offset corrections', (WidgetTester tester) async { Widget listBuilder(IndexedWidgetBuilder sliverChildBuilder) { return Directionality( textDirection: TextDirection.ltr, @@ -463,7 +503,7 @@ void main() { ); }); - testWidgets('SliverPadding includes preceding padding in the precedingScrollExtent provided to child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPadding includes preceding padding in the precedingScrollExtent provided to child', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/49195 final UniqueKey key = UniqueKey(); await tester.pumpWidget(Directionality( @@ -496,11 +536,13 @@ void main() { ); }); - testWidgets("SliverPadding consumes only its padding from the overlap of its parent's constraints", (WidgetTester tester) async { + testWidgetsWithLeakTracking("SliverPadding consumes only its padding from the overlap of its parent's constraints", (WidgetTester tester) async { final _MockRenderSliver mock = _MockRenderSliver(); + addTearDown(mock.dispose); final RenderSliverPadding renderObject = RenderSliverPadding( padding: const EdgeInsets.only(top: 20), ); + addTearDown(renderObject.dispose); renderObject.child = mock; renderObject.layout(const SliverConstraints( viewportMainAxisExtent: 100.0, @@ -521,11 +563,13 @@ void main() { expect(mock.constraints.overlap, 80.0); }); - testWidgets("SliverPadding passes the overlap to the child if it's negative", (WidgetTester tester) async { + testWidgetsWithLeakTracking("SliverPadding passes the overlap to the child if it's negative", (WidgetTester tester) async { final _MockRenderSliver mock = _MockRenderSliver(); + addTearDown(mock.dispose); final RenderSliverPadding renderObject = RenderSliverPadding( padding: const EdgeInsets.only(top: 20), ); + addTearDown(renderObject.dispose); renderObject.child = mock; renderObject.layout(const SliverConstraints( viewportMainAxisExtent: 100.0, @@ -546,11 +590,13 @@ void main() { expect(mock.constraints.overlap, -100.0); }); - testWidgets('SliverPadding passes the paintOrigin of the child on', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverPadding passes the paintOrigin of the child on', (WidgetTester tester) async { final _MockRenderSliver mock = _MockRenderSliver(); + addTearDown(mock.dispose); final RenderSliverPadding renderObject = RenderSliverPadding( padding: const EdgeInsets.only(top: 20), ); + addTearDown(renderObject.dispose); renderObject.child = mock; renderObject.layout(const SliverConstraints( viewportMainAxisExtent: 100.0, diff --git a/packages/flutter/test/widgets/slivers_protocol_test.dart b/packages/flutter/test/widgets/slivers_protocol_test.dart index 6758b7dd0e48a..c93c93daacd22 100644 --- a/packages/flutter/test/widgets/slivers_protocol_test.dart +++ b/packages/flutter/test/widgets/slivers_protocol_test.dart @@ -7,6 +7,7 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void verifyPaintPosition(GlobalKey key, Offset ideal) { final RenderObject target = key.currentContext!.findRenderObject()!; @@ -17,7 +18,7 @@ void verifyPaintPosition(GlobalKey key, Offset ideal) { } void main() { - testWidgets('Sliver protocol', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Sliver protocol', (WidgetTester tester) async { GlobalKey key1, key2, key3, key4, key5; await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index 8f7bab5655438..71769d00d6dad 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -6,17 +6,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; Future<void> test(WidgetTester tester, double offset, { double anchor = 0.0 }) { + final ViewportOffset viewportOffset = ViewportOffset.fixed(offset); + addTearDown(viewportOffset.dispose); return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Viewport( anchor: anchor / 600.0, - offset: ViewportOffset.fixed(offset), + offset: viewportOffset, slivers: const <Widget>[ SliverToBoxAdapter(child: SizedBox(height: 400.0)), SliverToBoxAdapter(child: SizedBox(height: 400.0)), @@ -71,7 +73,7 @@ void verify(WidgetTester tester, List<Offset> idealPositions, List<bool> idealVi } void main() { - testWidgets('Viewport basic test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport basic test', (WidgetTester tester) async { await test(tester, 0.0); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); verify(tester, <Offset>[ @@ -110,7 +112,7 @@ void main() { ], <bool>[false, false, true, true, false]); }); - testWidgets('Viewport anchor test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Viewport anchor test', (WidgetTester tester) async { await test(tester, 0.0, anchor: 100.0); expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); verify(tester, <Offset>[ @@ -149,7 +151,7 @@ void main() { ], <bool>[false, false, true, true, false]); }); - testWidgets('Multiple grids and lists', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Multiple grids and lists', (WidgetTester tester) async { await tester.pumpWidget( Center( child: SizedBox( @@ -230,7 +232,7 @@ void main() { expect(find.text('BOTTOM'), findsOneWidget); }); - testWidgets('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async { final List<String> items = <String>['1', '2', '3', '4', '5', '6']; await testSliverFixedExtentList(tester, items); // Keep alive widgets require 1 frame to notify their parents. Pumps in between @@ -271,7 +273,7 @@ void main() { expect(find.text('4'), findsOneWidget); }); - testWidgets('SliverFixedExtentList handles underflow when its children changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFixedExtentList handles underflow when its children changes', (WidgetTester tester) async { final List<String> items = <String>['1', '2', '3', '4', '5', '6']; final List<String> initializedChild = <String>[]; List<Widget> children = <Widget>[]; @@ -283,6 +285,8 @@ void main() { ); } final ScrollController controller = ScrollController(initialScrollOffset: 5400); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -336,7 +340,7 @@ void main() { expect(listEquals<String>(initializedChild, <String>['6']), isTrue); }); - testWidgets( + testWidgetsWithLeakTracking( 'SliverGrid Correctly layout children after rearranging', (WidgetTester tester) async { await tester.pumpWidget(const TestSliverGrid( @@ -369,7 +373,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'SliverGrid negative usableCrossAxisExtent', (WidgetTester tester) async { await tester.pumpWidget( @@ -407,7 +411,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'SliverList can handle inaccurate scroll offset due to changes in children list', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/59888. @@ -511,7 +515,7 @@ void main() { }, ); - testWidgets( + testWidgetsWithLeakTracking( 'SliverFixedExtentList Correctly layout children after rearranging', (WidgetTester tester) async { await tester.pumpWidget(const TestSliverFixedExtentList( @@ -549,7 +553,7 @@ void main() { }, ); - testWidgets('Can override ErrorWidget.build', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can override ErrorWidget.build', (WidgetTester tester) async { const Text errorText = Text('error'); final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder; ErrorWidget.builder = (FlutterErrorDetails details) => errorText; @@ -565,8 +569,10 @@ void main() { ErrorWidget.builder = oldBuilder; }); - testWidgets('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - super fast', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - super fast', (WidgetTester tester) async { final ScrollController controller = ScrollController(initialScrollOffset: 600); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -612,8 +618,10 @@ void main() { expect(controller.offset, 800.0); }); - testWidgets('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - reasonable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - reasonable', (WidgetTester tester) async { final ScrollController controller = ScrollController(initialScrollOffset: 600); + addTearDown(controller.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -662,7 +670,7 @@ void main() { } group('SliverOffstage - ', () { - testWidgets('offstage true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('offstage true', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(boilerPlate( const SliverOffstage( @@ -681,7 +689,7 @@ void main() { semantics.dispose(); }); - testWidgets('offstage false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('offstage false', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(boilerPlate( const SliverOffstage( @@ -703,7 +711,7 @@ void main() { }); group('SliverOpacity - ', () { - testWidgets('painting & semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('painting & semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // Opacity 1.0: Semantics and painting @@ -825,7 +833,7 @@ void main() { }); group('SliverIgnorePointer - ', () { - testWidgets('ignores pointer events', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores pointer events', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> events = <String>[]; await tester.pumpWidget(boilerPlate( @@ -847,7 +855,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignores semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> events = <String>[]; await tester.pumpWidget(boilerPlate( @@ -870,7 +878,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignoring only block semantics actions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignoring only block semantics actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(boilerPlate( SliverIgnorePointer( @@ -886,7 +894,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignores pointer events & semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores pointer events & semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> events = <String>[]; await tester.pumpWidget(boilerPlate( @@ -908,7 +916,7 @@ void main() { semantics.dispose(); }); - testWidgets('ignores nothing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores nothing', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> events = <String>[]; await tester.pumpWidget(boilerPlate( @@ -932,7 +940,7 @@ void main() { }); }); - testWidgets('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/62198 await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -957,7 +965,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('SliverGrid children can be arbitrarily placed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGrid children can be arbitrarily placed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/64006 int firstTapped = 0; int secondTapped = 0; @@ -1015,7 +1023,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverList.builder can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.builder can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1053,7 +1061,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverList.builder can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.builder can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1091,7 +1099,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverList.separated can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.separated can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1130,7 +1138,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverList.separated has correct number of children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.separated has correct number of children', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -1150,7 +1158,7 @@ void main() { expect(find.text('separator'), findsNWidgets(1)); }); - testWidgets('SliverList.list can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.list can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1192,7 +1200,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverFixedExtentList.builder can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverFixedExtentList.builder can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1230,7 +1238,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverList.list can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverList.list can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1273,7 +1281,7 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverGrid.builder can build children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGrid.builder can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0; final Key key = UniqueKey(); @@ -1312,9 +1320,10 @@ void main() { expect(secondTapped, 1); }); - testWidgets('SliverGridRegularTileLayout.computeMaxScrollOffset handles 0 children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverGridRegularTileLayout.computeMaxScrollOffset handles 0 children', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/59663 final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); // SliverGridDelegateWithFixedCrossAxisCount await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/widgets/slotted_render_object_widget_test.dart b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart index 7d61f844a8182..925684b5f24db 100644 --- a/packages/flutter/test/widgets/slotted_render_object_widget_test.dart +++ b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart @@ -7,14 +7,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const Color green = Color(0xFF00FF00); const Color yellow = Color(0xFFFFFF00); void main() { - testWidgets('SlottedRenderObjectWidget test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SlottedRenderObjectWidget test', (WidgetTester tester) async { await tester.pumpWidget(buildWidget( topLeft: Container( height: 100, @@ -139,7 +138,7 @@ void main() { expect(_RenderTest().publicNameForSlot(slot), slot.toString()); }); - testWidgets('key reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('key reparenting', (WidgetTester tester) async { const Widget widget1 = SizedBox(key: ValueKey<String>('smol'), height: 10, width: 10); const Widget widget2 = SizedBox(key: ValueKey<String>('big'), height: 100, width: 100); const Widget nullWidget = SizedBox(key: ValueKey<String>('null'), height: 50, width: 50); @@ -205,7 +204,7 @@ void main() { )); }); - testWidgets('debugDescribeChildren', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugDescribeChildren', (WidgetTester tester) async { await tester.pumpWidget(buildWidget( topLeft: const SizedBox( height: 100, @@ -222,16 +221,18 @@ void main() { equalsIgnoringHashCodes( '_RenderDiagonal#00000 relayoutBoundary=up1\n' ' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n' - ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' - ' │ TestFlutterView#00000] ← [root]\n' + ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n' + ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n' + ' │ [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ size: Size(190.0, 220.0)\n' ' │\n' ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' - ' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n' + ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' │ TestFlutterView#00000] ← View ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(unconstrained)\n' ' │ size: Size(80.0, 100.0)\n' @@ -239,8 +240,9 @@ void main() { ' │\n' ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' - ' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' - ' View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' + ' MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n' + ' _ViewScope ← _RawView-[_DeprecatedRawViewKey\n' + ' TestFlutterView#00000] ← View ← [root]\n' ' parentData: offset=Offset(80.0, 100.0) (can use size)\n' ' constraints: BoxConstraints(unconstrained)\n' ' size: Size(110.0, 120.0)\n' diff --git a/packages/flutter/test/widgets/snapshot_widget_test.dart b/packages/flutter/test/widgets/snapshot_widget_test.dart index 05512bae827d7..e6faaa97a055e 100644 --- a/packages/flutter/test/widgets/snapshot_widget_test.dart +++ b/packages/flutter/test/widgets/snapshot_widget_test.dart @@ -13,11 +13,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('SnapshotWidget can rasterize child', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnapshotWidget can rasterize child', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); final Key key = UniqueKey(); + await tester.pumpWidget(RepaintBoundary( key: key, child: TestDependencies( @@ -56,9 +59,11 @@ void main() { await expectLater(find.byKey(key), matchesGoldenFile('raster_widget.red.png')); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('Changing devicePixelRatio does not repaint if snapshotting is not enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing devicePixelRatio does not repaint if snapshotting is not enabled', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(); + addTearDown(controller.dispose); final TestPainter painter = TestPainter(); + addTearDown(painter.dispose); double devicePixelRatio = 1.0; late StateSetter localSetState; @@ -89,9 +94,11 @@ void main() { expect(painter.count, 1); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('Changing devicePixelRatio forces raster regeneration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing devicePixelRatio forces raster regeneration', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); final TestPainter painter = TestPainter(); + addTearDown(painter.dispose); double devicePixelRatio = 1.0; late StateSetter localSetState; @@ -126,8 +133,10 @@ void main() { expect(raster, isNot(newRaster)); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('SnapshotWidget paints its child as a single picture layer', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnapshotWidget paints its child as a single picture layer', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + await tester.pumpWidget(RepaintBoundary( child: Center( child: TestDependencies( @@ -153,14 +162,21 @@ void main() { expect(tester.layers.last, isA<PictureLayer>()); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('SnapshotWidget can update the painter type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SnapshotWidget can update the painter type', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + final TestPainter painter1 = TestPainter(); + addTearDown(painter1.dispose); + final TestPainter2 painter2 = TestPainter2(); + addTearDown(painter2.dispose); + + await tester.pumpWidget( Center( child: TestDependencies( child: SnapshotWidget( controller: controller, - painter: TestPainter(), + painter: painter1, child: const SizedBox(), ), ), @@ -172,7 +188,7 @@ void main() { child: TestDependencies( child: SnapshotWidget( controller: controller, - painter: TestPainter2(), + painter: painter2, child: const SizedBox(), ), ), @@ -182,8 +198,10 @@ void main() { expect(tester.takeException(), isNull); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('RenderSnapshotWidget does not error on rasterization of child with empty size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderSnapshotWidget does not error on rasterization of child with empty size', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + await tester.pumpWidget( Center( child: TestDependencies( @@ -201,6 +219,8 @@ void main() { testWidgets('RenderSnapshotWidget throws assertion if platform view is encountered', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + await tester.pumpWidget( Center( child: TestDependencies( @@ -220,8 +240,10 @@ void main() { .having((FlutterError error) => error.message, 'message', contains('SnapshotWidget used with a child that contains a PlatformView'))); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('RenderSnapshotWidget does not assert if SnapshotMode.forced', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderSnapshotWidget does not assert if SnapshotMode.forced', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + await tester.pumpWidget( Center( child: TestDependencies( @@ -241,8 +263,10 @@ void main() { expect(tester.takeException(), isNull); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - testWidgets('RenderSnapshotWidget does not take a snapshot if a platform view is encountered with SnapshotMode.permissive', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderSnapshotWidget does not take a snapshot if a platform view is encountered with SnapshotMode.permissive', (WidgetTester tester) async { final SnapshotController controller = SnapshotController(allowSnapshotting: true); + addTearDown(controller.dispose); + await tester.pumpWidget( Center( child: TestDependencies( @@ -261,9 +285,15 @@ void main() { expect(tester.takeException(), isNull); expect(tester.layers.last, isA<PlatformViewLayer>()); - }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 - - testWidgets('SnapshotWidget should have same result when enabled', (WidgetTester tester) async { + }, + skip: kIsWeb, // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/135141 + allowAllNotDisposed: true, + )); + + testWidgetsWithLeakTracking('SnapshotWidget should have same result when enabled', (WidgetTester tester) async { addTearDown(tester.view.reset); tester.view @@ -272,6 +302,8 @@ void main() { const ValueKey<String> repaintBoundaryKey = ValueKey<String>('boundary'); final SnapshotController controller = SnapshotController(); + addTearDown(controller.dispose); + await tester.pumpWidget(RepaintBoundary( key: repaintBoundaryKey, child: MaterialApp( @@ -291,12 +323,19 @@ void main() { )); final ui.Image imageWhenDisabled = (tester.renderObject(find.byKey(repaintBoundaryKey)) as RenderRepaintBoundary).toImageSync(); + addTearDown(imageWhenDisabled.dispose); controller.allowSnapshotting = true; await tester.pump(); await expectLater(find.byKey(repaintBoundaryKey), matchesReferenceImage(imageWhenDisabled)); - }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 + }, + skip: kIsWeb, // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 + leakTrackingTestConfig: const LeakTrackingTestConfig( + // TODO(ksokolovskyi): remove after fixing + // https://github.com/flutter/flutter/issues/135137 + notDisposedAllowList: <String, int> {'Image': 1}, + )); } class TestPlatformView extends SingleChildRenderObjectWidget { diff --git a/packages/flutter/test/widgets/spacer_test.dart b/packages/flutter/test/widgets/spacer_test.dart index 06fbda26d2451..47ee53b3c06ac 100644 --- a/packages/flutter/test/widgets/spacer_test.dart +++ b/packages/flutter/test/widgets/spacer_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Spacer takes up space.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Spacer takes up space.', (WidgetTester tester) async { await tester.pumpWidget(const Column( children: <Widget>[ SizedBox(width: 10.0, height: 10.0), @@ -19,7 +20,7 @@ void main() { expect(spacerRect.topLeft, const Offset(400.0, 10.0)); }); - testWidgets('Spacer takes up space proportional to flex.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Spacer takes up space proportional to flex.', (WidgetTester tester) async { const Spacer spacer1 = Spacer(); const Spacer spacer2 = Spacer(); const Spacer spacer3 = Spacer(flex: 2); @@ -53,7 +54,7 @@ void main() { expect(spacer4Rect.left, moreOrLessEquals(10.0, epsilon: 0.1)); }); - testWidgets('Spacer takes up space.', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Spacer takes up space.', (WidgetTester tester) async { await tester.pumpWidget(const UnconstrainedBox( constrainedAxis: Axis.vertical, child: Column( diff --git a/packages/flutter/test/widgets/spell_check_test.dart b/packages/flutter/test/widgets/spell_check_test.dart index f9b89dcde36b8..131f5e9b044e5 100644 --- a/packages/flutter/test/widgets/spell_check_test.dart +++ b/packages/flutter/test/widgets/spell_check_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; late TextStyle composingStyle; late TextStyle misspelledTextStyle; @@ -17,7 +18,7 @@ void main() { misspelledTextStyle = TextField.materialMisspelledTextStyle; }); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions ignores composing region when composing region out of range', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -46,7 +47,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions, isolated misspelled word with separate composing region example', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -77,7 +78,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions, composing region and misspelled words overlap example', (WidgetTester tester) async { const String text = 'Right worng worng right'; @@ -111,7 +112,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions, consecutive misspelled words example', (WidgetTester tester) async { const String text = 'Right worng worng right'; @@ -144,7 +145,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results text shorter than actual text example', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -174,7 +175,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results text longer with more misspelled words than actual text example', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -206,7 +207,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results text mismatched example', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -233,7 +234,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results shifted forward example', (WidgetTester tester) async { const String text = 'Hello, there wrold! Hey'; @@ -263,7 +264,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results shifted backwards example', (WidgetTester tester) async { const String text = 'Hello, wrold! Hey'; @@ -293,7 +294,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions corrects results when they lag, results shifted backwards and forwards example', (WidgetTester tester) async { const String text = 'Hello, wrold! And Hye!'; @@ -326,7 +327,7 @@ void main() { expect(textSpanTree, equals(expectedTextSpanTree)); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS })); - testWidgets( + testWidgetsWithLeakTracking( 'buildTextSpanWithSpellCheckSuggestions discards result when additions are made to misspelled word example', (WidgetTester tester) async { const String text = 'Hello, wroldd!'; diff --git a/packages/flutter/test/widgets/stack_test.dart b/packages/flutter/test/widgets/stack_test.dart index cd3174f5037ef..039b8c2567985 100644 --- a/packages/flutter/test/widgets/stack_test.dart +++ b/packages/flutter/test/widgets/stack_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../rendering/rendering_tester.dart' show TestCallbackPainter; @@ -18,7 +19,7 @@ class TestPaintingContext implements PaintingContext { } void main() { - testWidgets('Can construct an empty Stack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can construct an empty Stack', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -27,7 +28,7 @@ void main() { ); }); - testWidgets('Can construct an empty Centered Stack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can construct an empty Centered Stack', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -36,7 +37,7 @@ void main() { ); }); - testWidgets('Can change position data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can change position data', (WidgetTester tester) async { const Key key = Key('container'); await tester.pumpWidget( @@ -93,7 +94,7 @@ void main() { expect(parentData.height, isNull); }); - testWidgets('Can remove parent data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can remove parent data', (WidgetTester tester) async { const Key key = Key('container'); const SizedBox sizedBox = SizedBox(key: key, width: 10.0, height: 10.0); @@ -131,7 +132,7 @@ void main() { expect(parentData.height, isNull); }); - testWidgets('Can align non-positioned children (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align non-positioned children (LTR)', (WidgetTester tester) async { const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -177,7 +178,7 @@ void main() { expect(child1RenderObjectParentData.offset, equals(const Offset(10.0, 10.0))); }); - testWidgets('Can align non-positioned children (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can align non-positioned children (RTL)', (WidgetTester tester) async { const Key child0Key = Key('child0'); const Key child1Key = Key('child1'); @@ -223,7 +224,7 @@ void main() { expect(child1RenderObjectParentData.offset, equals(const Offset(0.0, 10.0))); }); - testWidgets('Can construct an empty IndexedStack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can construct an empty IndexedStack', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -232,7 +233,7 @@ void main() { ); }); - testWidgets('Can construct an empty Centered IndexedStack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can construct an empty Centered IndexedStack', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -241,7 +242,7 @@ void main() { ); }); - testWidgets('Can construct an IndexedStack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can construct an IndexedStack', (WidgetTester tester) async { const int itemCount = 3; late List<int> itemsPainted; @@ -289,7 +290,7 @@ void main() { expect(itemsPainted, equals(<int>[2])); }); - testWidgets('Can hit test an IndexedStack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can hit test an IndexedStack', (WidgetTester tester) async { const Key key = Key('indexedStack'); const int itemCount = 3; late List<int> itemsTapped; @@ -320,7 +321,7 @@ void main() { expect(itemsTapped, <int>[2]); }); - testWidgets('IndexedStack sets non-selected indexes to visible=false', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IndexedStack sets non-selected indexes to visible=false', (WidgetTester tester) async { Widget buildStack({required int itemCount, required int? selectedIndex}) { final List<Widget> children = List<Widget>.generate(itemCount, (int i) { return _ShowVisibility(index: i); @@ -355,7 +356,7 @@ void main() { expect(find.text('index 2 is visible ? true', skipOffstage: false), findsOneWidget); }); - testWidgets('Can set width and height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set width and height', (WidgetTester tester) async { const Key key = Key('container'); const BoxDecoration kBoxDecoration = BoxDecoration( @@ -423,7 +424,7 @@ void main() { expect(renderBox.size.height, equals(12.0)); }); - testWidgets('Can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(const Stack(textDirection: TextDirection.ltr)); final RenderStack renderObject = tester.allRenderObjects.whereType<RenderStack>().first; expect(renderObject.clipBehavior, equals(Clip.hardEdge)); @@ -432,7 +433,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.hardEdge)); }); - testWidgets('Clip.none is respected by describeApproximateClip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Clip.none is respected by describeApproximateClip', (WidgetTester tester) async { await tester.pumpWidget(const Stack( textDirection: TextDirection.ltr, children: <Widget>[Positioned(left: 1000, right: 2000, child: SizedBox(width: 2000, height: 2000))], @@ -455,7 +456,7 @@ void main() { expect(visited, true); }); - testWidgets('IndexedStack with null index', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IndexedStack with null index', (WidgetTester tester) async { bool? tapped; await tester.pumpWidget( @@ -485,7 +486,7 @@ void main() { expect(tapped, isNull); }); - testWidgets('IndexedStack reports hidden children as offstage', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IndexedStack reports hidden children as offstage', (WidgetTester tester) async { final List<Widget> children = <Widget>[ for (int i = 0; i < 5; i++) Text('child $i'), ]; @@ -519,7 +520,7 @@ void main() { } }); - testWidgets('Stack clip test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stack clip test', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -580,7 +581,7 @@ void main() { expect(context.invocations.first.memberName, equals(#paintChild)); }); - testWidgets('Stack sizing: default', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stack sizing: default', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( @@ -610,7 +611,7 @@ void main() { expect(logs, <String>['BoxConstraints(0.0<=w<=3.0, 0.0<=h<=7.0)']); }); - testWidgets('Stack sizing: explicit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stack sizing: explicit', (WidgetTester tester) async { final List<String> logs = <String>[]; Widget buildStack(StackFit sizing) { return Directionality( @@ -652,7 +653,7 @@ void main() { ]); }); - testWidgets('Positioned.directional control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Positioned.directional control test', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Directionality( @@ -689,7 +690,7 @@ void main() { expect(tester.getTopLeft(find.byKey(key)), const Offset(50.0, 0.0)); }); - testWidgets('PositionedDirectional control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('PositionedDirectional control test', (WidgetTester tester) async { final Key key = UniqueKey(); await tester.pumpWidget( Directionality( @@ -724,7 +725,7 @@ void main() { expect(tester.getTopLeft(find.byKey(key)), const Offset(50.0, 0.0)); }); - testWidgets('Can change the text direction of a Stack', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can change the text direction of a Stack', (WidgetTester tester) async { await tester.pumpWidget( const Stack( alignment: Alignment.center, @@ -742,7 +743,7 @@ void main() { ); }); - testWidgets('Alignment with partially-positioned children', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Alignment with partially-positioned children', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.rtl, @@ -887,7 +888,7 @@ void main() { )); }); - testWidgets('Can update clipBehavior of IndexedStack', + testWidgetsWithLeakTracking('Can update clipBehavior of IndexedStack', (WidgetTester tester) async { await tester.pumpWidget(const IndexedStack(textDirection: TextDirection.ltr)); final RenderIndexedStack renderObject = @@ -905,7 +906,7 @@ void main() { expect(renderIndexedObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('IndexedStack sizing: explicit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('IndexedStack sizing: explicit', (WidgetTester tester) async { final List<String> logs = <String>[]; Widget buildIndexedStack(StackFit sizing) { return Directionality( diff --git a/packages/flutter/test/widgets/state_setting_in_scrollables_test.dart b/packages/flutter/test/widgets/state_setting_in_scrollables_test.dart index 0f1ca8c15a535..c9ddff09179a1 100644 --- a/packages/flutter/test/widgets/state_setting_in_scrollables_test.dart +++ b/packages/flutter/test/widgets/state_setting_in_scrollables_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class Foo extends StatefulWidget { const Foo({ super.key }); @@ -12,7 +13,13 @@ class Foo extends StatefulWidget { } class FooState extends State<Foo> { - ScrollController scrollController = ScrollController(); + final ScrollController scrollController = ScrollController(); + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -81,7 +88,7 @@ class FooScrollBehavior extends ScrollBehavior { } void main() { - testWidgets('Can animate scroll after setState', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can animate scroll after setState', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/stateful_component_test.dart b/packages/flutter/test/widgets/stateful_component_test.dart index 3cebbe5829b26..3db736a133195 100644 --- a/packages/flutter/test/widgets/stateful_component_test.dart +++ b/packages/flutter/test/widgets/stateful_component_test.dart @@ -5,15 +5,15 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'test_widgets.dart'; void main() { - testWidgets('Stateful widget smoke test', (WidgetTester tester) async { - + testWidgetsWithLeakTracking('Stateful widget smoke test', (WidgetTester tester) async { void checkTree(BoxDecoration expectedDecoration) { final SingleChildRenderObjectElement element = tester.element( - find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement), + find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement && element.renderObject is! RenderView), ); expect(element, isNotNull); expect(element.renderObject, isA<RenderDecoratedBox>()); @@ -55,7 +55,7 @@ void main() { checkTree(kBoxDecorationB); }); - testWidgets("Don't rebuild subwidgets", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Don't rebuild subwidgets", (WidgetTester tester) async { await tester.pumpWidget( const FlipWidget( key: Key('rebuild test'), diff --git a/packages/flutter/test/widgets/stateful_components_test.dart b/packages/flutter/test/widgets/stateful_components_test.dart index 13f5a6ac7fd61..168d7424a8d59 100644 --- a/packages/flutter/test/widgets/stateful_components_test.dart +++ b/packages/flutter/test/widgets/stateful_components_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class InnerWidget extends StatefulWidget { const InnerWidget({ super.key }); @@ -44,7 +45,7 @@ class OuterContainerState extends State<OuterContainer> { } void main() { - testWidgets('resync stateful widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('resync stateful widget', (WidgetTester tester) async { const Key innerKey = Key('inner'); const Key outerKey = Key('outer'); diff --git a/packages/flutter/test/widgets/status_transitions_test.dart b/packages/flutter/test/widgets/status_transitions_test.dart index 42b91b1e938a4..0667398c26ca8 100644 --- a/packages/flutter/test/widgets/status_transitions_test.dart +++ b/packages/flutter/test/widgets/status_transitions_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestStatusTransitionWidget extends StatusTransitionWidget { const TestStatusTransitionWidget({ @@ -19,12 +20,13 @@ class TestStatusTransitionWidget extends StatusTransitionWidget { } void main() { - testWidgets('Status transition control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Status transition control test', (WidgetTester tester) async { bool didBuild = false; final AnimationController controller = AnimationController( duration: const Duration(seconds: 1), vsync: const TestVSync(), ); + addTearDown(controller.dispose); await tester.pumpWidget(TestStatusTransitionWidget( animation: controller, diff --git a/packages/flutter/test/widgets/syncing_test.dart b/packages/flutter/test/widgets/syncing_test.dart index 84745ca07cd30..ff514d6ae3168 100644 --- a/packages/flutter/test/widgets/syncing_test.dart +++ b/packages/flutter/test/widgets/syncing_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestWidget extends StatefulWidget { const TestWidget({ @@ -48,7 +49,7 @@ class TestWidgetState extends State<TestWidget> { void main() { - testWidgets('no change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('no change', (WidgetTester tester) async { await tester.pumpWidget( ColoredBox( color: Colors.blue, @@ -88,7 +89,7 @@ void main() { await tester.pumpWidget(Container()); }); - testWidgets('remove one', (WidgetTester tester) async { + testWidgetsWithLeakTracking('remove one', (WidgetTester tester) async { await tester.pumpWidget( ColoredBox( color: Colors.blue, @@ -127,7 +128,7 @@ void main() { await tester.pumpWidget(Container()); }); - testWidgets('swap instances around', (WidgetTester tester) async { + testWidgetsWithLeakTracking('swap instances around', (WidgetTester tester) async { const Widget a = TestWidget(persistentState: 0x61, syncedState: 0x41, child: Text('apple', textDirection: TextDirection.ltr)); const Widget b = TestWidget(persistentState: 0x62, syncedState: 0x42, child: Text('banana', textDirection: TextDirection.ltr)); await tester.pumpWidget(const Column()); diff --git a/packages/flutter/test/widgets/table_test.dart b/packages/flutter/test/widgets/table_test.dart index 727fd1040d621..b5531277d3ee8 100644 --- a/packages/flutter/test/widgets/table_test.dart +++ b/packages/flutter/test/widgets/table_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestStatefulWidget extends StatefulWidget { const TestStatefulWidget({ super.key }); @@ -37,7 +38,7 @@ class TestChildState extends State<TestChildWidget> { } void main() { - testWidgets('Table widget - empty', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - empty', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -46,7 +47,7 @@ void main() { ); }); - testWidgets('Table widget - control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - control test', (WidgetTester tester) async { Future<void> run(TextDirection textDirection) async { await tester.pumpWidget( Directionality( @@ -86,7 +87,7 @@ void main() { await run(TextDirection.rtl); }); - testWidgets('Table widget can be detached and re-attached', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget can be detached and re-attached', (WidgetTester tester) async { final Widget table = Table( key: GlobalKey(), children: const <TableRow>[ @@ -121,7 +122,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Table widget - column offset (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - column offset (LTR)', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -191,7 +192,7 @@ void main() { expect(c3.left, equals(c1.left)); }); - testWidgets('Table widget - column offset (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - column offset (RTL)', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, @@ -261,7 +262,7 @@ void main() { expect(c3.right, equals(c1.right)); }); - testWidgets('Table border - smoke test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table border - smoke test', (WidgetTester tester) async { Future<void> run(TextDirection textDirection) async { await tester.pumpWidget( Directionality( @@ -295,7 +296,7 @@ void main() { await run(TextDirection.rtl); }); - testWidgets('Table widget - changing table dimensions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - changing table dimensions', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -351,7 +352,7 @@ void main() { expect(boxG1, isNot(equals(boxG2))); }); - testWidgets('Really small deficit double precision error', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Really small deficit double precision error', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/27083 const SizedBox cell = SizedBox(width: 16, height: 16); await tester.pumpWidget( @@ -376,7 +377,7 @@ void main() { // If the above bug is present this test will never terminate. }); - testWidgets('Calculating flex columns with small width deficit', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Calculating flex columns with small width deficit', (WidgetTester tester) async { const SizedBox cell = SizedBox(width: 1, height: 1); // If the error is present, pumpWidget() will fail due to an unsatisfied // assertion during the layout phase. @@ -406,7 +407,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('Table widget - repump test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - repump test', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -464,7 +465,7 @@ void main() { expect(boxA.size, equals(boxB.size)); }); - testWidgets('Table widget - intrinsic sizing test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - intrinsic sizing test', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -500,7 +501,7 @@ void main() { expect(boxA.size.height, equals(boxB.size.height)); }); - testWidgets('Table widget - intrinsic sizing test, resizing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - intrinsic sizing test, resizing', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -561,7 +562,7 @@ void main() { expect(boxA.size.height, equals(boxB.size.height)); }); - testWidgets('Table widget - intrinsic sizing test, changing column widths', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - intrinsic sizing test, changing column widths', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -621,7 +622,7 @@ void main() { expect(boxA.size.height, equals(boxB.size.height)); }); - testWidgets('Table widget - moving test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - moving test', (WidgetTester tester) async { final List<BuildContext> contexts = <BuildContext>[]; await tester.pumpWidget( Directionality( @@ -677,7 +678,7 @@ void main() { expect(contexts[0], equals(contexts[1])); }); - testWidgets('Table widget - keyed rows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - keyed rows', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -735,7 +736,7 @@ void main() { expect(state22.mounted, isTrue); }); - testWidgets('Table widget - global key reparenting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - global key reparenting', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final Key tableKey = UniqueKey(); @@ -848,7 +849,7 @@ void main() { expect(table.row(0).length, 2); }); - testWidgets('Table widget diagnostics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget diagnostics', (WidgetTester tester) async { GlobalKey key0; final Widget table = Directionality( textDirection: TextDirection.ltr, @@ -904,7 +905,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/31473. - testWidgets( + testWidgetsWithLeakTracking( 'Does not crash if a child RenderObject is replaced by another RenderObject of a different type', (WidgetTester tester) async { await tester.pumpWidget( @@ -930,7 +931,7 @@ void main() { }, ); - testWidgets('Table widget - Default textBaseline is null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Table widget - Default textBaseline is null', (WidgetTester tester) async { expect( () => Table(defaultVerticalAlignment: TableCellVerticalAlignment.baseline), throwsA( @@ -940,7 +941,7 @@ void main() { ); }); - testWidgets( + testWidgetsWithLeakTracking( 'Table widget requires all TableRows to have same number of children', (WidgetTester tester) async { FlutterError? error; @@ -965,7 +966,7 @@ void main() { }, ); - testWidgets('Can replace child with a different RenderObject type', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can replace child with a different RenderObject type', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/69395. await tester.pumpWidget( Directionality( @@ -1001,7 +1002,7 @@ void main() { expect(table.column(2).last.runtimeType, isNot(toBeReplaced)); }); - testWidgets('Do not crash if a child that has not been layed out in a previous build is removed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Do not crash if a child that has not been layed out in a previous build is removed', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/60488. Widget buildTable(Key key) { return Directionality( @@ -1034,7 +1035,7 @@ void main() { expect(find.text('Hello'), findsOneWidget); }); - testWidgets('TableRow with no children throws an error message', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TableRow with no children throws an error message', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/119541. String result = 'no exception'; diff --git a/packages/flutter/test/widgets/tap_region_test.dart b/packages/flutter/test/widgets/tap_region_test.dart index 56625c666c6a3..a244518417d1f 100644 --- a/packages/flutter/test/widgets/tap_region_test.dart +++ b/packages/flutter/test/widgets/tap_region_test.dart @@ -7,9 +7,10 @@ import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('TapRegionSurface detects outside taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TapRegionSurface detects outside taps', (WidgetTester tester) async { final Set<String> tappedOutside = <String>{}; await tester.pumpWidget( Directionality( @@ -101,7 +102,7 @@ void main() { expect(tappedOutside, isEmpty); }); - testWidgets('TapRegionSurface detects inside taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TapRegionSurface detects inside taps', (WidgetTester tester) async { final Set<String> tappedInside = <String>{}; await tester.pumpWidget( Directionality( @@ -188,7 +189,7 @@ void main() { expect(tappedInside, isEmpty); }); - testWidgets('TapRegionSurface detects inside taps correctly with behavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TapRegionSurface detects inside taps correctly with behavior', (WidgetTester tester) async { final Set<String> tappedInside = <String>{}; const ValueKey<String> noGroupKey = ValueKey<String>('No Group'); const ValueKey<String> group1AKey = ValueKey<String>('Group 1 A'); @@ -275,7 +276,7 @@ void main() { tappedInside.clear(); }); - testWidgets('Setting the group updates the registration', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting the group updates the registration', (WidgetTester tester) async { final Set<String> tappedOutside = <String>{}; await tester.pumpWidget( Directionality( diff --git a/packages/flutter/test/widgets/text_golden_test.dart b/packages/flutter/test/widgets/text_golden_test.dart index 4267da2d79a18..35a2afc0a2fba 100644 --- a/packages/flutter/test/widgets/text_golden_test.dart +++ b/packages/flutter/test/widgets/text_golden_test.dart @@ -10,9 +10,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Centered text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Centered text', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -63,7 +64,7 @@ void main() { }); - testWidgets('Text Foreground', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Foreground', (WidgetTester tester) async { const Color black = Color(0xFF000000); const Color red = Color(0xFFFF0000); const Color blue = Color(0xFF0000FF); @@ -141,7 +142,7 @@ void main() { // TODO(garyq): This test requires an update when the background // drawing from the beginning of the line bug is fixed. The current // tested version is not completely correct. - testWidgets('Text Background', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Background', (WidgetTester tester) async { const Color red = Colors.red; const Color blue = Colors.blue; const Color translucentGreen = Color(0x5000F000); @@ -188,7 +189,7 @@ void main() { ); }); - testWidgets('Text Fade', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Fade', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -225,7 +226,7 @@ void main() { ); }); - testWidgets('Default Strut text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Default Strut text', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -250,7 +251,7 @@ void main() { ); }); - testWidgets('Strut text 1', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Strut text 1', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -277,7 +278,7 @@ void main() { ); }); - testWidgets('Strut text 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Strut text 2', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -305,7 +306,7 @@ void main() { ); }); - testWidgets('Strut text rich', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Strut text rich', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -356,7 +357,7 @@ void main() { ); }); - testWidgets('Strut text font fallback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Strut text font fallback', (WidgetTester tester) async { // Font Fallback await tester.pumpWidget( Center( @@ -391,7 +392,7 @@ void main() { ); }); - testWidgets('Strut text rich forceStrutHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Strut text rich forceStrutHeight', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( @@ -442,7 +443,7 @@ void main() { ); }); - testWidgets('Decoration thickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( <TextDecoration>[ TextDecoration.underline, @@ -480,7 +481,7 @@ void main() { ); }); - testWidgets('Decoration thickness', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Decoration thickness', (WidgetTester tester) async { final TextDecoration allDecorations = TextDecoration.combine( <TextDecoration>[ TextDecoration.underline, @@ -519,7 +520,7 @@ void main() { ); }); - testWidgets('Text Inline widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget', (WidgetTester tester) async { await tester.pumpWidget( Theme(data: ThemeData(useMaterial3: false), child: Center( child: RepaintBoundary( @@ -613,7 +614,7 @@ void main() { ); }); - testWidgets('Text Inline widget textfield', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget textfield', (WidgetTester tester) async { await tester.pumpWidget( Center( child: MaterialApp( @@ -660,7 +661,7 @@ void main() { }); // This tests if multiple Text.rich widgets are able to inline nest within each other. - testWidgets('Text Inline widget nesting', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget nesting', (WidgetTester tester) async { await tester.pumpWidget( Center( child: MaterialApp( @@ -789,7 +790,7 @@ void main() { ); }); - testWidgets('Text Inline widget baseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget baseline', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -899,7 +900,7 @@ void main() { ); }); - testWidgets('Text Inline widget aboveBaseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget aboveBaseline', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -1009,7 +1010,7 @@ void main() { ); }); - testWidgets('Text Inline widget belowBaseline', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget belowBaseline', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -1119,7 +1120,7 @@ void main() { ); }); - testWidgets('Text Inline widget top', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget top', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -1229,7 +1230,7 @@ void main() { ); }); - testWidgets('Text Inline widget middle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text Inline widget middle', (WidgetTester tester) async { await tester.pumpWidget( Theme( data: ThemeData(useMaterial3: false), @@ -1339,7 +1340,7 @@ void main() { ); }); - testWidgets('Text TextHeightBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text TextHeightBehavior', (WidgetTester tester) async { await tester.pumpWidget( Center( child: RepaintBoundary( diff --git a/packages/flutter/test/widgets/text_scaler_backward_compatibility_test.dart b/packages/flutter/test/widgets/text_scaler_backward_compatibility_test.dart new file mode 100644 index 0000000000000..724fcbdb7f2c4 --- /dev/null +++ b/packages/flutter/test/widgets/text_scaler_backward_compatibility_test.dart @@ -0,0 +1,281 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(LongCatIsLooong): Remove this file once textScaleFactor is removed. +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + group('TextStyle', () { + test('getTextSyle is backward compatible', () { + expect( + const TextStyle(fontSize: 14).getTextStyle(textScaleFactor: 2.0).toString(), + contains('fontSize: 28'), + ); + }, skip: kIsWeb); // [intended] CkTextStyle doesn't have a custom toString implementation. + }); + group('TextPainter', () { + test('textScaleFactor translates to textScaler', () { + final TextPainter textPainter = TextPainter( + text: const TextSpan(text: 'text'), + textDirection: TextDirection.ltr, + textScaleFactor: 42, + ); + + expect(textPainter.textScaler, const TextScaler.linear(42.0)); + + // Linear TextScaler translates to textScaleFactor. + textPainter.textScaler = const TextScaler.linear(12.0); + expect(textPainter.textScaleFactor, 12.0); + + textPainter.textScaleFactor = 10; + expect(textPainter.textScaler, const TextScaler.linear(10)); + }); + }); + + group('MediaQuery', () { + test('specifying both textScaler and textScalingFactor asserts', () { + expect( + () => MediaQueryData(textScaleFactor: 2, textScaler: const TextScaler.linear(2.0)), + throwsAssertionError, + ); + }); + + test('copyWith is backward compatible', () { + const MediaQueryData data = MediaQueryData(textScaler: TextScaler.linear(2.0)); + + final MediaQueryData data1 = data.copyWith(textScaleFactor: 42); + expect(data1.textScaler, const TextScaler.linear(42)); + expect(data1.textScaleFactor, 42); + + final MediaQueryData data2 = data.copyWith(textScaler: TextScaler.noScaling); + expect(data2.textScaler, TextScaler.noScaling); + expect(data2.textScaleFactor, 1.0); + }); + + test('copyWith specifying both textScaler and textScalingFactor asserts', () { + const MediaQueryData data = MediaQueryData(); + expect( + () => data.copyWith(textScaleFactor: 2, textScaler: const TextScaler.linear(2.0)), + throwsAssertionError, + ); + }); + + testWidgetsWithLeakTracking('MediaQuery.textScaleFactorOf overriding compatibility', (WidgetTester tester) async { + late final double outsideTextScaleFactor; + late final TextScaler outsideTextScaler; + late final double insideTextScaleFactor; + late final TextScaler insideTextScaler; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + outsideTextScaler = MediaQuery.textScalerOf(context); + return MediaQuery( + data: const MediaQueryData( + textScaleFactor: 4.0, + ), + child: Builder( + builder: (BuildContext context) { + insideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + insideTextScaler = MediaQuery.textScalerOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + // Overriding textScaleFactor should work for unmigrated widgets that are + // still using MediaQuery.textScaleFactorOf. Also if a unmigrated widget + // overrides MediaQuery.textScaleFactor, migrated widgets in the subtree + // should get the correct TextScaler. + expect(outsideTextScaleFactor, 1.0); + expect(outsideTextScaler.textScaleFactor, 1.0); + expect(outsideTextScaler, TextScaler.noScaling); + expect(insideTextScaleFactor, 4.0); + expect(insideTextScaler.textScaleFactor, 4.0); + expect(insideTextScaler, const TextScaler.linear(4.0)); + }); + + testWidgetsWithLeakTracking('textScaleFactor overriding backward compatibility', (WidgetTester tester) async { + late final double outsideTextScaleFactor; + late final TextScaler outsideTextScaler; + late final double insideTextScaleFactor; + late final TextScaler insideTextScaler; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + outsideTextScaler = MediaQuery.textScalerOf(context); + return MediaQuery( + data: const MediaQueryData(textScaler: TextScaler.linear(4.0)), + child: Builder( + builder: (BuildContext context) { + insideTextScaleFactor = MediaQuery.textScaleFactorOf(context); + insideTextScaler = MediaQuery.textScalerOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + expect(outsideTextScaleFactor, 1.0); + expect(outsideTextScaler.textScaleFactor, 1.0); + expect(outsideTextScaler, TextScaler.noScaling); + expect(insideTextScaleFactor, 4.0); + expect(insideTextScaler.textScaleFactor, 4.0); + expect(insideTextScaler, const TextScaler.linear(4.0)); + }); + }); + + group('RenderObjects backward compatibility', () { + test('RenderEditable', () { + final RenderEditable renderObject = RenderEditable( + backgroundCursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), + textDirection: TextDirection.ltr, + cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), + offset: ViewportOffset.zero(), + textSelectionDelegate: _FakeEditableTextState(), + text: const TextSpan( + text: 'test', + style: TextStyle(height: 1.0, fontSize: 10.0), + ), + startHandleLayerLink: LayerLink(), + endHandleLayerLink: LayerLink(), + selection: const TextSelection.collapsed(offset: 0), + ); + expect(renderObject.textScaleFactor, 1.0); + + renderObject.textScaleFactor = 3.0; + expect(renderObject.textScaleFactor, 3.0); + expect(renderObject.textScaler, const TextScaler.linear(3.0)); + + renderObject.textScaler = const TextScaler.linear(4.0); + expect(renderObject.textScaleFactor, 4.0); + }); + + test('RenderParagraph', () { + final RenderParagraph renderObject = RenderParagraph( + const TextSpan( + text: 'test', + style: TextStyle(height: 1.0, fontSize: 10.0), + ), + textDirection: TextDirection.ltr, + ); + expect(renderObject.textScaleFactor, 1.0); + + renderObject.textScaleFactor = 3.0; + expect(renderObject.textScaleFactor, 3.0); + expect(renderObject.textScaler, const TextScaler.linear(3.0)); + + renderObject.textScaler = const TextScaler.linear(4.0); + expect(renderObject.textScaleFactor, 4.0); + }); + }); + + group('Widgets backward compatibility', () { + testWidgetsWithLeakTracking('RichText', (WidgetTester tester) async { + await tester.pumpWidget( + RichText( + textDirection: TextDirection.ltr, + text: const TextSpan(), + textScaleFactor: 2.0, + ), + ); + + expect( + tester.renderObject<RenderParagraph>(find.byType(RichText)).textScaler, + const TextScaler.linear(2.0), + ); + expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).textScaleFactor, 2.0); + }); + + testWidgetsWithLeakTracking('Text', (WidgetTester tester) async { + await tester.pumpWidget( + const Text( + 'text', + textDirection: TextDirection.ltr, + textScaleFactor: 2.0, + ), + ); + + expect( + tester.renderObject<RenderParagraph>(find.text('text')).textScaler, + const TextScaler.linear(2.0), + ); + }); + + testWidgetsWithLeakTracking('EditableText', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node'); + addTearDown(focusNode.dispose); + const TextStyle textStyle = TextStyle(); + const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.rtl, + child: EditableText( + backgroundCursorColor: cursorColor, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + textScaleFactor: 2.0, + ), + ), + ), + ); + + final RenderEditable renderEditable = tester.allRenderObjects.whereType<RenderEditable>().first; + expect( + renderEditable.textScaler, + const TextScaler.linear(2.0), + ); + }); + }); +} + +class _FakeEditableTextState with TextSelectionDelegate { + @override + TextEditingValue textEditingValue = TextEditingValue.empty; + + TextSelection? selection; + + @override + void hideToolbar([bool hideHandles = true]) { } + + @override + void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { + selection = value.selection; + } + + @override + void bringIntoView(TextPosition position) { } + + @override + void cutSelection(SelectionChangedCause cause) { } + + @override + Future<void> pasteText(SelectionChangedCause cause) { + return Future<void>.value(); + } + + @override + void selectAll(SelectionChangedCause cause) { } + + @override + void copySelection(SelectionChangedCause cause) { } +} diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index 41fa39207b7af..b1b51f021a412 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show ValueListenable, defaultTargetPlatform; -import 'package:flutter/gestures.dart' show PointerDeviceKind, kSecondaryButton; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -99,10 +99,6 @@ void main() { ); } - test('TextSelectionOverlay.fadeDuration exist', () async { - expect(TextSelectionOverlay.fadeDuration, SelectionOverlay.fadeDuration); - }); - testWidgets('a series of taps all call onTaps', (WidgetTester tester) async { await pumpGestureDetector(tester); await tester.tapAt(const Offset(200, 200)); diff --git a/packages/flutter/test/widgets/text_semantics_test.dart b/packages/flutter/test/widgets/text_semantics_test.dart index 190d2630d1725..9bc76eeea8335 100644 --- a/packages/flutter/test/widgets/text_semantics_test.dart +++ b/packages/flutter/test/widgets/text_semantics_test.dart @@ -6,11 +6,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('SemanticsNode ids are stable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SemanticsNode ids are stable', (WidgetTester tester) async { // Regression test for b/151732341. final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( diff --git a/packages/flutter/test/widgets/text_test.dart b/packages/flutter/test/widgets/text_test.dart index b501a783f079d..c29ebdc7341fe 100644 --- a/packages/flutter/test/widgets/text_test.dart +++ b/packages/flutter/test/widgets/text_test.dart @@ -9,12 +9,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; void main() { - testWidgets('Text respects media query', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text respects media query', (WidgetTester tester) async { await tester.pumpWidget(const MediaQuery( data: MediaQueryData(textScaleFactor: 1.3), child: Center( @@ -24,7 +24,7 @@ void main() { RichText text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.3); + expect(text.textScaler, const TextScaler.linear(1.3)); await tester.pumpWidget(const Center( child: Text('Hello', textDirection: TextDirection.ltr), @@ -32,17 +32,17 @@ void main() { text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.0); + expect(text.textScaler, TextScaler.noScaling); }); - testWidgets('Text respects textScaleFactor with default font size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text respects textScaleFactor with default font size', (WidgetTester tester) async { await tester.pumpWidget( const Center(child: Text('Hello', textDirection: TextDirection.ltr)), ); RichText text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.0); + expect(text.textScaler, TextScaler.noScaling); final Size baseSize = tester.getSize(find.byType(RichText)); expect(baseSize.width, equals(70.0)); expect(baseSize.height, equals(14.0)); @@ -57,13 +57,13 @@ void main() { text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.5); + expect(text.textScaler, const TextScaler.linear(1.5)); final Size largeSize = tester.getSize(find.byType(RichText)); expect(largeSize.width, 105.0); expect(largeSize.height, equals(21.0)); }); - testWidgets('Text respects textScaleFactor with explicit font size', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text respects textScaleFactor with explicit font size', (WidgetTester tester) async { await tester.pumpWidget(const Center( child: Text( 'Hello', @@ -74,7 +74,7 @@ void main() { RichText text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.0); + expect(text.textScaler, TextScaler.noScaling); final Size baseSize = tester.getSize(find.byType(RichText)); expect(baseSize.width, equals(100.0)); expect(baseSize.height, equals(20.0)); @@ -90,7 +90,7 @@ void main() { text = tester.firstWidget(find.byType(RichText)); expect(text, isNotNull); - expect(text.textScaleFactor, 1.3); + expect(text.textScaler, const TextScaler.linear(1.3)); final Size largeSize = tester.getSize(find.byType(RichText)); expect(largeSize.width, 130.0); expect(largeSize.height, equals(26.0)); @@ -103,7 +103,7 @@ void main() { expect(message, contains(' Text ')); }); - testWidgets('Text can be created from TextSpans and uses defaultTextStyle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text can be created from TextSpans and uses defaultTextStyle', (WidgetTester tester) async { await tester.pumpWidget( const DefaultTextStyle( style: TextStyle( @@ -133,7 +133,7 @@ void main() { expect(text.text.style!.fontSize, 20.0); }); - testWidgets('inline widgets works with ellipsis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inline widgets works with ellipsis', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/35869 const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -166,7 +166,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('inline widgets hitTest works with ellipsis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inline widgets hitTest works with ellipsis', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/68559 const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -202,7 +202,7 @@ void main() { expect(tester.takeException(), null); }); - testWidgets('inline widgets works with textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inline widgets works with textScaleFactor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/59316 final UniqueKey key = UniqueKey(); double textScaleFactor = 1.0; @@ -266,7 +266,7 @@ void main() { expect(renderText.size.height, singleLineHeight * textScaleFactor * 3); }); - testWidgets("Inline widgets' scaled sizes are constrained", (WidgetTester tester) async { + testWidgetsWithLeakTracking("Inline widgets' scaled sizes are constrained", (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/130588 await tester.pumpWidget( const Directionality( @@ -283,7 +283,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('semanticsLabel can override text label', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semanticsLabel can override text label', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( const Text( @@ -329,7 +329,7 @@ void main() { semantics.dispose(); }); - testWidgets('semantics label is in order when uses widget span', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantics label is in order when uses widget span', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -364,7 +364,7 @@ void main() { ); }); - testWidgets('semantics can handle some widget spans without semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantics can handle some widget spans without semantics', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -406,7 +406,7 @@ void main() { matchesSemantics(label: 'before \n mid\nfoo\n after')); }); - testWidgets('semantics can handle all widget spans without semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantics can handle all widget spans without semantics', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -448,7 +448,7 @@ void main() { matchesSemantics(label: 'before \n mid\n after')); }); - testWidgets('semantics can handle widget spans with explicit semantics node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantics can handle widget spans with explicit semantics node', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -483,7 +483,7 @@ void main() { ); }); - testWidgets('semanticsLabel can be shorter than text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semanticsLabel can be shorter than text', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -531,7 +531,7 @@ void main() { semantics.dispose(); }); - testWidgets('recognizers split semantic node', (WidgetTester tester) async { + testWidgetsWithLeakTracking('recognizers split semantic node', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -585,13 +585,14 @@ void main() { semantics.dispose(); }); - testWidgets('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async { + testWidgetsWithLeakTracking('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/100395. final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(fontSize: 200); const String onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'; const String offScreenText = 'off screen'; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( SingleChildScrollView( controller: controller, @@ -654,7 +655,7 @@ void main() { semantics.dispose(); }); - testWidgets('recognizers split semantic node when TextSpan overflows', (WidgetTester tester) async { + testWidgetsWithLeakTracking('recognizers split semantic node when TextSpan overflows', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -705,7 +706,7 @@ void main() { semantics.dispose(); }); - testWidgets('recognizers split semantic nodes with text span labels', (WidgetTester tester) async { + testWidgetsWithLeakTracking('recognizers split semantic nodes with text span labels', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -763,7 +764,7 @@ void main() { }); - testWidgets('recognizers split semantic node - bidi', (WidgetTester tester) async { + testWidgetsWithLeakTracking('recognizers split semantic node - bidi', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -844,7 +845,7 @@ void main() { semantics.dispose(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945 - testWidgets('TapGesture recognizers contribute link semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TapGesture recognizers contribute link semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -884,7 +885,7 @@ void main() { semantics.dispose(); }); - testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inline widgets generate semantic nodes', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -958,7 +959,7 @@ void main() { semantics.dispose(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945 - testWidgets('inline widgets semantic nodes scale', (WidgetTester tester) async { + testWidgetsWithLeakTracking('inline widgets semantic nodes scale', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const TextStyle textStyle = TextStyle(); await tester.pumpWidget( @@ -1038,7 +1039,7 @@ void main() { semantics.dispose(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945 - testWidgets('receives fontFamilyFallback and package from root ThemeData', (WidgetTester tester) async { + testWidgetsWithLeakTracking('receives fontFamilyFallback and package from root ThemeData', (WidgetTester tester) async { const String fontFamily = 'fontFamily'; const String package = 'package_name'; final List<String> fontFamilyFallback = <String>['font', 'family', 'fallback']; @@ -1071,7 +1072,7 @@ void main() { } }); - testWidgets('Overflow is clipping correctly - short text with overflow: clip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - short text with overflow: clip', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.clip, @@ -1081,7 +1082,7 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); - testWidgets('Overflow is clipping correctly - long text with overflow: ellipsis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - long text with overflow: ellipsis', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.ellipsis, @@ -1094,7 +1095,7 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87878 - testWidgets('Overflow is clipping correctly - short text with overflow: ellipsis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - short text with overflow: ellipsis', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.ellipsis, @@ -1104,7 +1105,7 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); - testWidgets('Overflow is clipping correctly - long text with overflow: fade', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - long text with overflow: fade', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.fade, @@ -1117,7 +1118,7 @@ void main() { ); }); - testWidgets('Overflow is clipping correctly - short text with overflow: fade', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - short text with overflow: fade', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.fade, @@ -1127,7 +1128,7 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); - testWidgets('Overflow is clipping correctly - long text with overflow: visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - long text with overflow: visible', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.visible, @@ -1137,7 +1138,7 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); - testWidgets('Overflow is clipping correctly - short text with overflow: visible', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Overflow is clipping correctly - short text with overflow: visible', (WidgetTester tester) async { await _pumpTextWidget( tester: tester, overflow: TextOverflow.visible, @@ -1147,7 +1148,7 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); - testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async { + testWidgetsWithLeakTracking('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async { Future<void> createText(TextWidthBasis textWidthBasis) { return tester.pumpWidget( MaterialApp( @@ -1184,7 +1185,7 @@ void main() { expect(textSizeLongestLine.height, equals(fontHeight * 2)); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020 - testWidgets('textWidthBasis with textAlign still obeys parent alignment', (WidgetTester tester) async { + testWidgetsWithLeakTracking('textWidthBasis with textAlign still obeys parent alignment', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( @@ -1234,7 +1235,7 @@ void main() { expect(tester.getSize(find.text('RIGHT ALIGNED, LONGEST LINE')).width, equals(width)); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020 - testWidgets( + testWidgetsWithLeakTracking( 'textWidthBasis.longestLine confines the width of the paragraph ' 'when given loose constraints', (WidgetTester tester) async { @@ -1274,7 +1275,7 @@ void main() { skip: isBrowser, // https://github.com/flutter/flutter/issues/44020 ); - testWidgets('Paragraph.getBoxesForRange returns nothing when selection range is zero length', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Paragraph.getBoxesForRange returns nothing when selection range is zero length', (WidgetTester tester) async { final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle()); builder.addText('hello'); final ui.Paragraph paragraph = builder.build(); @@ -1284,7 +1285,7 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/65818 - testWidgets('WidgetSpans with no semantic information are elided from semantics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetSpans with no semantic information are elided from semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); // Without the fix for this bug the pump widget will throw a RangeError. await tester.pumpWidget( @@ -1332,7 +1333,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 // Regression test for https://github.com/flutter/flutter/issues/69787 - testWidgets('WidgetSpans with no semantic information are elided from semantics - case 2', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetSpans with no semantic information are elided from semantics - case 2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -1383,7 +1384,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 // Regression test for https://github.com/flutter/flutter/issues/69787 - testWidgets('WidgetSpans with no semantic information are elided from semantics - case 3', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetSpans with no semantic information are elided from semantics - case 3', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -1446,7 +1447,7 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 // Regression test for https://github.com/flutter/flutter/issues/69787 - testWidgets('WidgetSpans with no semantic information are elided from semantics - case 4', (WidgetTester tester) async { + testWidgetsWithLeakTracking('WidgetSpans with no semantic information are elided from semantics - case 4', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( Directionality( @@ -1516,7 +1517,7 @@ void main() { semantics.dispose(); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 - testWidgets('RenderParagraph intrinsic width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderParagraph intrinsic width', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1557,7 +1558,7 @@ void main() { expect(paragraph.getMinIntrinsicWidth(0.0), 200); }); - testWidgets('can compute intrinsic width and height for widget span with text scaling', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can compute intrinsic width and height for widget span with text scaling', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/59316 const Key textKey = Key('RichText'); Widget textWithNestedInlineSpans({ required double textScaleFactor, required double screenWidth }) { @@ -1610,7 +1611,7 @@ void main() { ); }); - testWidgets('Text uses TextStyle.overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Text uses TextStyle.overflow', (WidgetTester tester) async { const TextOverflow overflow = TextOverflow.fade; await tester.pumpWidget(const Text( @@ -1625,7 +1626,7 @@ void main() { expect(richText.text.style!.overflow, overflow); }); - testWidgets( + testWidgetsWithLeakTracking( 'Text can be hit-tested without layout or paint being called in a frame', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/85108. @@ -1661,7 +1662,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('Mouse hovering over selectable Text uses SystemMouseCursor.text', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse hovering over selectable Text uses SystemMouseCursor.text', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: SelectionArea( child: Text('Flutter'), @@ -1676,7 +1677,7 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); }); - testWidgets('Mouse hovering over selectable Text uses default selection style mouse cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse hovering over selectable Text uses default selection style mouse cursor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: SelectionArea( child: DefaultSelectionStyle.merge( diff --git a/packages/flutter/test/widgets/texture_test.dart b/packages/flutter/test/widgets/texture_test.dart index 4a5b1b774aa42..e0bf2dc09e930 100644 --- a/packages/flutter/test/widgets/texture_test.dart +++ b/packages/flutter/test/widgets/texture_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Texture with freeze set to true', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Texture with freeze set to true', (WidgetTester tester) async { await tester.pumpWidget( const Center(child: Texture(textureId: 1, freeze: true)), ); @@ -25,6 +26,7 @@ void main() { expect(textureBox.freeze, true); final ContainerLayer containerLayer = ContainerLayer(); + addTearDown(containerLayer.dispose); final PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero); textureBox.paint(paintingContext, Offset.zero); final Layer layer = containerLayer.lastChild!; @@ -35,7 +37,7 @@ void main() { expect(textureLayer.freeze, true); }); - testWidgets('Texture with default FilterQuality', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Texture with default FilterQuality', (WidgetTester tester) async { await tester.pumpWidget( const Center(child: Texture(textureId: 1)), ); @@ -53,6 +55,7 @@ void main() { expect(textureBox.filterQuality, FilterQuality.low); final ContainerLayer containerLayer = ContainerLayer(); + addTearDown(containerLayer.dispose); final PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero); textureBox.paint(paintingContext, Offset.zero); final Layer layer = containerLayer.lastChild!; @@ -64,7 +67,7 @@ void main() { }); - testWidgets('Texture with FilterQuality.none', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Texture with FilterQuality.none', (WidgetTester tester) async { await tester.pumpWidget( const Center(child: Texture(textureId: 1, filterQuality: FilterQuality.none)), ); @@ -82,6 +85,7 @@ void main() { expect(textureBox.filterQuality, FilterQuality.none); final ContainerLayer containerLayer = ContainerLayer(); + addTearDown(containerLayer.dispose); final PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero); textureBox.paint(paintingContext, Offset.zero); final Layer layer = containerLayer.lastChild!; @@ -92,7 +96,7 @@ void main() { expect(textureLayer.filterQuality, FilterQuality.none); }); - testWidgets('Texture with FilterQuality.low', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Texture with FilterQuality.low', (WidgetTester tester) async { await tester.pumpWidget( const Center(child: Texture(textureId: 1)), ); @@ -110,6 +114,7 @@ void main() { expect(textureBox.filterQuality, FilterQuality.low); final ContainerLayer containerLayer = ContainerLayer(); + addTearDown(containerLayer.dispose); final PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero); textureBox.paint(paintingContext, Offset.zero); final Layer layer = containerLayer.lastChild!; diff --git a/packages/flutter/test/widgets/ticker_mode_test.dart b/packages/flutter/test/widgets/ticker_mode_test.dart index aa09b8c9c2e6c..75ac25e8db430 100644 --- a/packages/flutter/test/widgets/ticker_mode_test.dart +++ b/packages/flutter/test/widgets/ticker_mode_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Nested TickerMode cannot turn tickers back on', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Nested TickerMode cannot turn tickers back on', (WidgetTester tester) async { int outerTickCount = 0; int innerTickCount = 0; @@ -99,7 +100,7 @@ void main() { expect(innerTickCount, 0); }); - testWidgets('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async { Widget widgetUnderTest({required bool tickerEnabled}) { return TickerMode( enabled: tickerEnabled, @@ -121,7 +122,7 @@ void main() { expect(state().buildCount, 1); }); - testWidgets('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async { Widget widgetUnderTest({required bool tickerEnabled}) { return TickerMode( enabled: tickerEnabled, @@ -143,7 +144,7 @@ void main() { expect(state().buildCount, 1); }); - testWidgets('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { final GlobalKey tickingWidgetKey = GlobalKey(); Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) { return TickerMode( @@ -164,7 +165,7 @@ void main() { expect(tickingState.ticker.isTicking, isFalse); }); - testWidgets('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { final GlobalKey tickingWidgetKey = GlobalKey(); Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) { return TickerMode( @@ -185,7 +186,7 @@ void main() { expect(tickingState.ticker.isTicking, isFalse); }); - testWidgets('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( routes: <String, WidgetBuilder>{ '/foo' : (BuildContext context) => const Text('New route'), diff --git a/packages/flutter/test/widgets/ticker_provider_test.dart b/packages/flutter/test/widgets/ticker_provider_test.dart index 2f342dae0bab4..1c65f075d7ef2 100644 --- a/packages/flutter/test/widgets/ticker_provider_test.dart +++ b/packages/flutter/test/widgets/ticker_provider_test.dart @@ -6,9 +6,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('TickerMode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TickerMode', (WidgetTester tester) async { const Widget widget = TickerMode( enabled: false, child: CircularProgressIndicator(), @@ -34,7 +35,7 @@ void main() { expect(tester.binding.transientCallbackCount, 0); }); - testWidgets('Navigation with TickerMode', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Navigation with TickerMode', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: const LinearProgressIndicator(), routes: <String, WidgetBuilder>{ @@ -56,7 +57,7 @@ void main() { expect(tester.binding.transientCallbackCount, 1); }); - testWidgets('SingleTickerProviderStateMixin can handle not being used', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleTickerProviderStateMixin can handle not being used', (WidgetTester tester) async { const Widget widget = BoringTickerTest(); expect(widget.toString, isNot(throwsException)); @@ -96,7 +97,7 @@ void main() { )); }); - testWidgets('SingleTickerProviderStateMixin dispose while active', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleTickerProviderStateMixin dispose while active', (WidgetTester tester) async { final GlobalKey<_SingleTickerTestState> key = GlobalKey<_SingleTickerTestState>(); final Widget widget = _SingleTickerTest(key: key); await tester.pumpWidget(widget); @@ -136,7 +137,7 @@ void main() { } }); - testWidgets('SingleTickerProviderStateMixin dispose while active', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleTickerProviderStateMixin dispose while active', (WidgetTester tester) async { final GlobalKey<_SingleTickerTestState> key = GlobalKey<_SingleTickerTestState>(); final Widget widget = _SingleTickerTest(key: key); await tester.pumpWidget(widget); @@ -176,7 +177,7 @@ void main() { } }); - testWidgets('TickerProviderStateMixin dispose while any ticker is active', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TickerProviderStateMixin dispose while any ticker is active', (WidgetTester tester) async { final GlobalKey<_MultipleTickerTestState> key = GlobalKey<_MultipleTickerTestState>(); final Widget widget = _MultipleTickerTest(key: key); await tester.pumpWidget(widget); @@ -216,12 +217,12 @@ void main() { }); }); - testWidgets('SingleTickerProviderStateMixin does not call State.toString', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SingleTickerProviderStateMixin does not call State.toString', (WidgetTester tester) async { await tester.pumpWidget(const _SingleTickerTest()); expect(tester.state<_SingleTickerTestState>(find.byType(_SingleTickerTest)).toStringCount, 0); }); - testWidgets('TickerProviderStateMixin does not call State.toString', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TickerProviderStateMixin does not call State.toString', (WidgetTester tester) async { await tester.pumpWidget(const _MultipleTickerTest()); expect(tester.state<_MultipleTickerTestState>(find.byType(_MultipleTickerTest)).toStringCount, 0); }); diff --git a/packages/flutter/test/widgets/title_test.dart b/packages/flutter/test/widgets/title_test.dart index 71dbf6f9e1c79..e61a2a86ec324 100644 --- a/packages/flutter/test/widgets/title_test.dart +++ b/packages/flutter/test/widgets/title_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('toString control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toString control test', (WidgetTester tester) async { final Widget widget = Title( color: const Color(0xFF00FF00), title: 'Awesome app', @@ -16,7 +17,7 @@ void main() { expect(widget.toString, isNot(throwsException)); }); - testWidgets('should handle having no title', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should handle having no title', (WidgetTester tester) async { final Title widget = Title( color: const Color(0xFF00FF00), child: Container(), @@ -26,14 +27,14 @@ void main() { expect(widget.color, equals(const Color(0xFF00FF00))); }); - testWidgets('should not allow non-opaque color', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should not allow non-opaque color', (WidgetTester tester) async { expect(() => Title( color: const Color(0x00000000), child: Container(), ), throwsAssertionError); }); - testWidgets('should not pass "null" to setApplicationSwitcherDescription', (WidgetTester tester) async { + testWidgetsWithLeakTracking('should not pass "null" to setApplicationSwitcherDescription', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { diff --git a/packages/flutter/test/widgets/tracking_scroll_controller_test.dart b/packages/flutter/test/widgets/tracking_scroll_controller_test.dart index 11a19f5362a0a..fd76a6d0c4daf 100644 --- a/packages/flutter/test/widgets/tracking_scroll_controller_test.dart +++ b/packages/flutter/test/widgets/tracking_scroll_controller_test.dart @@ -4,10 +4,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('TrackingScrollController saves offset', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TrackingScrollController saves offset', (WidgetTester tester) async { final TrackingScrollController controller = TrackingScrollController(); + addTearDown(controller.dispose); const double listItemHeight = 100.0; await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/transformed_scrollable_test.dart b/packages/flutter/test/widgets/transformed_scrollable_test.dart index 5a57556d78dd8..a11485de5d286 100644 --- a/packages/flutter/test/widgets/transformed_scrollable_test.dart +++ b/packages/flutter/test/widgets/transformed_scrollable_test.dart @@ -6,10 +6,13 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Scrollable scaled up', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollable scaled up', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Transform.scale( @@ -52,8 +55,10 @@ void main() { expect(controller.offset, 42.5); // 85.0 - (85.0 / 2) }); - testWidgets('Scrollable scaled down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollable scaled down', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Transform.scale( @@ -96,8 +101,10 @@ void main() { expect(controller.offset, 0.0); // 340.0 - (170.0 * 2) }); - testWidgets('Scrollable rotated 90 degrees', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Scrollable rotated 90 degrees', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Transform.rotate( @@ -136,8 +143,10 @@ void main() { expect(controller.offset, 30.0); // 100.0 - 70.0 }); - testWidgets('Perspective transform on scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Perspective transform on scrollable', (WidgetTester tester) async { final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: Transform( diff --git a/packages/flutter/test/widgets/transitions_test.dart b/packages/flutter/test/widgets/transitions_test.dart index 9b1a33bc9a9d2..92f74baf604af 100644 --- a/packages/flutter/test/widgets/transitions_test.dart +++ b/packages/flutter/test/widgets/transitions_test.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('toString control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('toString control test', (WidgetTester tester) async { const Widget widget = FadeTransition( opacity: kAlwaysCompleteAnimation, child: Text('Ready', textDirection: TextDirection.ltr), @@ -47,7 +48,7 @@ void main() { controller = AnimationController(vsync: const TestVSync()); }); - testWidgets('decoration test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('decoration test', (WidgetTester tester) async { final DecoratedBoxTransition transitionUnderTest = DecoratedBoxTransition( decoration: decorationTween.animate(controller), @@ -95,7 +96,7 @@ void main() { expect(actualDecoration.boxShadow, null); }); - testWidgets('animations work with curves test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animations work with curves test', (WidgetTester tester) async { final Animation<Decoration> curvedDecorationAnimation = decorationTween.animate(CurvedAnimation( parent: controller, @@ -144,7 +145,7 @@ void main() { }); }); - testWidgets('AlignTransition animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlignTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<Alignment> alignmentTween = AlignmentTween( begin: Alignment.centerLeft, @@ -168,7 +169,7 @@ void main() { expect(actualAlignment, const Alignment(0.0, 0.5)); }); - testWidgets('RelativePositionedTransition animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RelativePositionedTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<Rect?> rectTween = RectTween( begin: const Rect.fromLTWH(0, 0, 30, 40), @@ -214,7 +215,7 @@ void main() { expect(renderBox.size, equals(const Size(665, 420))); }); - testWidgets('AlignTransition keeps width and height factors', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AlignTransition keeps width and height factors', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<Alignment> alignmentTween = AlignmentTween( begin: Alignment.centerLeft, @@ -235,7 +236,7 @@ void main() { expect(actualAlign.heightFactor, 0.4); }); - testWidgets('SizeTransition clamps negative size factors - vertical axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizeTransition clamps negative size factors - vertical axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller); @@ -265,7 +266,7 @@ void main() { expect(actualPositionedBox.heightFactor, 1.0); }); - testWidgets('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller); @@ -296,7 +297,68 @@ void main() { expect(actualPositionedBox.widthFactor, 1.0); }); - testWidgets('RotationTransition animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('MatrixTransition animates', (WidgetTester tester) async { + final AnimationController controller = AnimationController(vsync: const TestVSync()); + final Widget widget = MatrixTransition( + alignment: Alignment.topRight, + onTransform: (double value) => Matrix4.translationValues(value, value, value), + animation: controller, + child: const Text( + 'Matrix', + textDirection: TextDirection.ltr, + ), + ); + + await tester.pumpWidget(widget); + Transform actualTransformedBox = tester.widget(find.byType(Transform)); + Matrix4 actualTransform = actualTransformedBox.transform; + expect(actualTransform, equals(Matrix4.rotationZ(0.0))); + + controller.value = 0.5; + await tester.pump(); + actualTransformedBox = tester.widget(find.byType(Transform)); + actualTransform = actualTransformedBox.transform; + expect(actualTransform, Matrix4.fromList(<double>[ + 1.0, 0.0, 0.0, 0.5, + 0.0, 1.0, 0.0, 0.5, + 0.0, 0.0, 1.0, 0.5, + 0.0, 0.0, 0.0, 1.0, + ])..transpose()); + + controller.value = 0.75; + await tester.pump(); + actualTransformedBox = tester.widget(find.byType(Transform)); + actualTransform = actualTransformedBox.transform; + expect(actualTransform, Matrix4.fromList(<double>[ + 1.0, 0.0, 0.0, 0.75, + 0.0, 1.0, 0.0, 0.75, + 0.0, 0.0, 1.0, 0.75, + 0.0, 0.0, 0.0, 1.0, + ])..transpose()); + }); + + testWidgetsWithLeakTracking('MatrixTransition maintains chosen alignment during animation', (WidgetTester tester) async { + final AnimationController controller = AnimationController(vsync: const TestVSync()); + final Widget widget = MatrixTransition( + alignment: Alignment.topRight, + onTransform: (double value) => Matrix4.identity(), + animation: controller, + child: const Text('Matrix', textDirection: TextDirection.ltr), + ); + + await tester.pumpWidget(widget); + MatrixTransition actualTransformedBox = tester.widget(find.byType(MatrixTransition)); + Alignment actualAlignment = actualTransformedBox.alignment; + expect(actualAlignment, Alignment.topRight); + + controller.value = 0.5; + await tester.pump(); + actualTransformedBox = tester.widget(find.byType(MatrixTransition)); + actualAlignment = actualTransformedBox.alignment; + expect(actualAlignment, Alignment.topRight); + }); + + testWidgetsWithLeakTracking('RotationTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Widget widget = RotationTransition( alignment: Alignment.topRight, @@ -316,26 +378,26 @@ void main() { await tester.pump(); actualRotatedBox = tester.widget(find.byType(Transform)); actualTurns = actualRotatedBox.transform; - expect(actualTurns, Matrix4.fromList(<double>[ + expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[ -1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ])..transpose()); + ])..transpose())); controller.value = 0.75; await tester.pump(); actualRotatedBox = tester.widget(find.byType(Transform)); actualTurns = actualRotatedBox.transform; - expect(actualTurns, Matrix4.fromList(<double>[ + expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[ 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ])..transpose()); + ])..transpose())); }); - testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Widget widget = RotationTransition( alignment: Alignment.topRight, @@ -365,7 +427,7 @@ void main() { ); return opacityWidget.opacity.value; } - testWidgets('animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( @@ -408,7 +470,7 @@ void main() { ); return opacityWidget.opacity.value; } - testWidgets('animates', (WidgetTester tester) async { + testWidgetsWithLeakTracking('animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Localizations( @@ -457,8 +519,71 @@ void main() { }); }); + group('MatrixTransition', () { + testWidgetsWithLeakTracking('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { + final AnimationController controller = AnimationController(vsync: const TestVSync()); + final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); + final Widget widget = Directionality( + textDirection: TextDirection.ltr, + child: MatrixTransition( + animation: animation, + onTransform: (double value) => Matrix4.identity(), + filterQuality: FilterQuality.none, + child: const Text('Matrix Transition'), + ), + ); + + await tester.pumpWidget(widget); + + // Validate that expensive layer is not left in tree before animation has started. + expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); + + controller.value = 0.25; + await tester.pump(); + + expect( + tester.layers, + contains(isA<ImageFilterLayer>().having( + (ImageFilterLayer layer) => layer.imageFilter.toString(), + 'image filter', + startsWith('ImageFilter.matrix('), + )), + ); + + controller.value = 0.5; + await tester.pump(); + + expect( + tester.layers, + contains(isA<ImageFilterLayer>().having( + (ImageFilterLayer layer) => layer.imageFilter.toString(), + 'image filter', + startsWith('ImageFilter.matrix('), + )), + ); + + controller.value = 0.75; + await tester.pump(); + + expect( + tester.layers, + contains(isA<ImageFilterLayer>().having( + (ImageFilterLayer layer) => layer.imageFilter.toString(), + 'image filter', + startsWith('ImageFilter.matrix('), + )), + ); + + controller.value = 1; + await tester.pump(); + + // Validate that expensive layer is not left in tree after animation has finished. + expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); + }); + }); + group('ScaleTransition', () { - testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { + testWidgetsWithLeakTracking('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( @@ -511,7 +636,7 @@ void main() { }); group('RotationTransition', () { - testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { + testWidgetsWithLeakTracking('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( @@ -564,9 +689,11 @@ void main() { }); group('Builders', () { - testWidgets('AnimatedBuilder rebuilds when changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('AnimatedBuilder rebuilds when changed', (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -592,10 +719,12 @@ void main() { expect(redrawKey.currentState!.redraws, equals(2)); }); - testWidgets("AnimatedBuilder doesn't rebuild the child", (WidgetTester tester) async { + testWidgetsWithLeakTracking("AnimatedBuilder doesn't rebuild the child", (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -626,9 +755,11 @@ void main() { expect(redrawKeyChild.currentState!.redraws, equals(1)); }); - testWidgets('ListenableBuilder rebuilds when changed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ListenableBuilder rebuilds when changed', (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -654,10 +785,12 @@ void main() { expect(redrawKey.currentState!.redraws, equals(2)); }); - testWidgets("ListenableBuilder doesn't rebuild the child", (WidgetTester tester) async { + testWidgetsWithLeakTracking("ListenableBuilder doesn't rebuild the child", (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); + addTearDown(notifier.dispose); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/widgets/tree_shape_test.dart b/packages/flutter/test/widgets/tree_shape_test.dart new file mode 100644 index 0000000000000..ab1c97ff922bd --- /dev/null +++ b/packages/flutter/test/widgets/tree_shape_test.dart @@ -0,0 +1,1162 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + testWidgetsWithLeakTracking('Providing a RenderObjectWidget directly to the RootWidget fails', (WidgetTester tester) async { + // No render tree exists to attach the RenderObjectWidget to. + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: const ColoredBox(color: Colors.red), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + startsWith('The render object for ColoredBox cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('Moving a RenderObjectWidget to the RootWidget via GlobalKey fails', (WidgetTester tester) async { + final Widget globalKeyedWidget = ColoredBox( + key: GlobalKey(), + color: Colors.red, + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: globalKeyedWidget, + ), + ); + expect(tester.takeException(), isNull); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: globalKeyedWidget, + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('A View cannot be a child of a render object widget', (WidgetTester tester) async { + await tester.pumpWidget(Center( + child: View( + view: FakeView(tester.view), + child: Container(), + ), + )); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot maintain an independent render tree at its current location.'), + )); + }); + + testWidgetsWithLeakTracking('The child of a ViewAnchor cannot be a View', (WidgetTester tester) async { + await tester.pumpWidget( + ViewAnchor( + child: View( + view: FakeView(tester.view), + child: Container(), + ), + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot maintain an independent render tree at its current location.'), + )); + }); + + testWidgetsWithLeakTracking('A View can not be moved via GlobalKey to be a child of a RenderObject', (WidgetTester tester) async { + final Widget globalKeyedView = View( + key: GlobalKey(), + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.red), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: globalKeyedView, + ); + expect(tester.takeException(), isNull); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: globalKeyedView, + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot maintain an independent render tree at its current location.'), + )); + }); + + testWidgetsWithLeakTracking('The view property of a ViewAnchor cannot be a render object widget', (WidgetTester tester) async { + await tester.pumpWidget( + ViewAnchor( + view: const ColoredBox(color: Colors.red), + child: Container(), + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + startsWith('The render object for ColoredBox cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('A RenderObject cannot be moved into the view property of a ViewAnchor via GlobalKey', (WidgetTester tester) async { + final Widget globalKeyedWidget = ColoredBox( + key: GlobalKey(), + color: Colors.red, + ); + + await tester.pumpWidget( + ViewAnchor( + child: globalKeyedWidget, + ), + ); + expect(tester.takeException(), isNull); + + await tester.pumpWidget( + ViewAnchor( + view: globalKeyedWidget, + child: const SizedBox(), + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('ViewAnchor cannot be used at the top of the widget tree (outside of View)', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: const ViewAnchor( + child: SizedBox(), + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + startsWith('The render object for SizedBox cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('ViewAnchor cannot be moved to the top of the widget tree (outside of View) via GlobalKey', (WidgetTester tester) async { + final Widget globalKeyedViewAnchor = ViewAnchor( + key: GlobalKey(), + child: const SizedBox(), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: globalKeyedViewAnchor, + ), + ); + expect(tester.takeException(), isNull); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: globalKeyedViewAnchor, + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('View can be used at the top of the widget tree', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: Container(), + ), + ); + + expect(tester.takeException(), isNull); + }); + + testWidgetsWithLeakTracking('View can be moved to the top of the widget tree view GlobalKey', (WidgetTester tester) async { + final Widget globalKeyView = View( + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.red), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: ViewAnchor( + view: globalKeyView, // This one has trouble when deactivating + child: const SizedBox(), + ), + ), + ); + expect(tester.takeException(), isNull); + expect(find.byType(SizedBox), findsOneWidget); + expect(find.byType(ColoredBox), findsOneWidget); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: globalKeyView, + ); + expect(tester.takeException(), isNull); + expect(find.byType(SizedBox), findsNothing); + expect(find.byType(ColoredBox), findsOneWidget); + }); + + testWidgetsWithLeakTracking('ViewCollection can be used at the top of the widget tree', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: tester.view, + child: Container(), + ), + ], + ), + ); + + expect(tester.takeException(), isNull); + }); + + testWidgetsWithLeakTracking('ViewCollection cannot be used inside a View', (WidgetTester tester) async { + await tester.pumpWidget( + ViewCollection( + views: <Widget>[ + View( + view: FakeView(tester.view), + child: Container(), + ), + ], + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + startsWith('The Element for ViewCollection cannot be inserted into slot "null" of its ancestor.'), + )); + }); + + testWidgetsWithLeakTracking('ViewCollection can be used as ViewAnchor.view', (WidgetTester tester) async { + await tester.pumpWidget( + ViewAnchor( + view: ViewCollection( + views: <Widget>[ + View( + view: FakeView(tester.view), + child: Container(), + ) + ], + ), + child: Container(), + ), + ); + + expect(tester.takeException(), isNull); + }); + + testWidgetsWithLeakTracking('ViewCollection cannot have render object widgets as children', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: const <Widget>[ + ColoredBox(color: Colors.red), + ], + ), + ); + + expect(tester.takeException(), isFlutterError.having( + (FlutterError error) => error.message, + 'message', + startsWith('The render object for ColoredBox cannot find ancestor render object to attach to.'), + )); + }); + + testWidgetsWithLeakTracking('Views can be moved in and out of ViewCollections via GlobalKey', (WidgetTester tester) async { + final Widget greenView = View( + key: GlobalKey(debugLabel: 'green'), + view: tester.view, + child: const ColoredBox(color: Colors.green), + ); + final Widget redView = View( + key: GlobalKey(debugLabel: 'red'), + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.red), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + greenView, + ViewCollection( + views: <Widget>[ + redView, + ], + ), + ] + ), + ); + expect(tester.takeException(), isNull); + expect(find.byType(ColoredBox), findsNWidgets(2)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + redView, + ViewCollection( + views: <Widget>[ + greenView, + ], + ), + ] + ), + ); + expect(tester.takeException(), isNull); + expect(find.byType(ColoredBox), findsNWidgets(2)); + }); + + testWidgetsWithLeakTracking('Can move stuff between views via global key: viewA -> viewB', (WidgetTester tester) async { + final FlutterView greenView = tester.view; + final FlutterView redView = FakeView(tester.view); + final Widget globalKeyChild = SizedBox( + key: GlobalKey(), + ); + + Map<int, RenderObject> collectLeafRenderObjects() { + final Map<int, RenderObject> result = <int, RenderObject>{}; + for (final RenderView renderView in RendererBinding.instance.renderViews) { + void visit(RenderObject object) { + result[renderView.flutterView.viewId] = object; + object.visitChildren(visit); + } + visit(renderView); + } + return result; + } + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: greenView, + child: ColoredBox( + color: Colors.green, + child: globalKeyChild, + ), + ), + View( + view: redView, + child: const ColoredBox( + color: Colors.red, + ), + ), + ], + ), + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); + + Map<int, RenderObject> leafRenderObject = collectLeafRenderObjects(); + expect(leafRenderObject[greenView.viewId], isA<RenderConstrainedBox>()); + expect(leafRenderObject[redView.viewId], isNot(isA<RenderConstrainedBox>())); + + // Move the child. + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: greenView, + child: const ColoredBox( + color: Colors.green, + ), + ), + View( + view: redView, + child: ColoredBox( + color: Colors.red, + child: globalKeyChild, + ), + ), + ], + ), + ); + + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + tester.renderObject(find.byKey(globalKeyChild.key!)), + equals(boxWithGlobalKey), + ); + + leafRenderObject = collectLeafRenderObjects(); + expect(leafRenderObject[greenView.viewId], isNot(isA<RenderConstrainedBox>())); + expect(leafRenderObject[redView.viewId], isA<RenderConstrainedBox>()); + }); + + testWidgetsWithLeakTracking('Can move stuff between views via global key: viewB -> viewA', (WidgetTester tester) async { + final FlutterView greenView = tester.view; + final FlutterView redView = FakeView(tester.view); + final Widget globalKeyChild = SizedBox( + key: GlobalKey(), + ); + + Map<int, RenderObject> collectLeafRenderObjects() { + final Map<int, RenderObject> result = <int, RenderObject>{}; + for (final RenderView renderView in RendererBinding.instance.renderViews) { + void visit(RenderObject object) { + result[renderView.flutterView.viewId] = object; + object.visitChildren(visit); + } + visit(renderView); + } + return result; + } + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: greenView, + child: const ColoredBox( + color: Colors.green, + ), + ), + View( + view: redView, + child: ColoredBox( + color: Colors.red, + child: globalKeyChild, + ), + ), + ], + ), + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); + + Map<int, RenderObject> leafRenderObject = collectLeafRenderObjects(); + expect(leafRenderObject[redView.viewId], isA<RenderConstrainedBox>()); + expect(leafRenderObject[greenView.viewId], isNot(isA<RenderConstrainedBox>())); + + // Move the child. + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: greenView, + child: ColoredBox( + color: Colors.green, + child: globalKeyChild, + ), + ), + View( + view: redView, + child: const ColoredBox( + color: Colors.red, + ), + ), + ], + ), + ); + + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + tester.renderObject(find.byKey(globalKeyChild.key!)), + equals(boxWithGlobalKey), + ); + + leafRenderObject = collectLeafRenderObjects(); + expect(leafRenderObject[redView.viewId], isNot(isA<RenderConstrainedBox>())); + expect(leafRenderObject[greenView.viewId], isA<RenderConstrainedBox>()); + }); + + testWidgetsWithLeakTracking('Can move stuff out of a view that is going away, viewA -> ViewB', (WidgetTester tester) async { + final FlutterView greenView = tester.view; + final Key greenKey = UniqueKey(); + final FlutterView redView = FakeView(tester.view); + final Key redKey = UniqueKey(); + final Widget globalKeyChild = SizedBox( + key: GlobalKey(), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + key: greenKey, + view: greenView, + child: const ColoredBox( + color: Colors.green, + ), + ), + View( + key: redKey, + view: redView, + child: ColoredBox( + color: Colors.red, + child: globalKeyChild, + ), + ), + ], + ), + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); + + // Move the child and remove its view. + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + key: greenKey, + view: greenView, + child: ColoredBox( + color: Colors.green, + child: globalKeyChild, + ), + ), + ], + ), + ); + + expect( + findsColoredBox(Colors.red), + findsNothing, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + tester.renderObject(find.byKey(globalKeyChild.key!)), + equals(boxWithGlobalKey), + ); + }); + + testWidgetsWithLeakTracking('Can move stuff out of a view that is going away, viewB -> ViewA', (WidgetTester tester) async { + final FlutterView greenView = tester.view; + final Key greenKey = UniqueKey(); + final FlutterView redView = FakeView(tester.view); + final Key redKey = UniqueKey(); + final Widget globalKeyChild = SizedBox( + key: GlobalKey(), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + key: greenKey, + view: greenView, + child: ColoredBox( + color: Colors.green, + child: globalKeyChild, + ), + ), + View( + key: redKey, + view: redView, + child: const ColoredBox( + color: Colors.red, + ), + ), + ], + ), + ); + expect( + find.descendant( + of: findsColoredBox(Colors.green), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); + + // Move the child and remove its view. + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + key: redKey, + view: redView, + child: ColoredBox( + color: Colors.red, + child: globalKeyChild, + ), + ), + ], + ), + ); + + expect( + findsColoredBox(Colors.green), + findsNothing, + ); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect( + tester.renderObject(find.byKey(globalKeyChild.key!)), + equals(boxWithGlobalKey), + ); + }); + + testWidgetsWithLeakTracking('Can move stuff out of a view that is moving itself, stuff ends up before view', (WidgetTester tester) async { + final Key key1 = UniqueKey(); + final Key key2 = UniqueKey(); + final Key key3 = UniqueKey(); + final Key key4 = UniqueKey(); + + final GlobalKey viewKey = GlobalKey(); + final GlobalKey childKey = GlobalKey(); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox(key: key1), + ViewAnchor( + key: key2, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: SizedBox( + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ), + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + child: const SizedBox(), + ), + SizedBox(key: key4), + ], + )); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox( + key: key1, + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ViewAnchor( + key: key2, + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: const SizedBox(), + ), + child: const SizedBox(), + ), + SizedBox(key: key4), + ], + )); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox(key: key1), + ViewAnchor( + key: key2, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: SizedBox( + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ), + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + child: const SizedBox(), + ), + SizedBox(key: key4), + ], + )); + }); + + testWidgetsWithLeakTracking('Can move stuff out of a view that is moving itself, stuff ends up after view', (WidgetTester tester) async { + final Key key1 = UniqueKey(); + final Key key2 = UniqueKey(); + final Key key3 = UniqueKey(); + final Key key4 = UniqueKey(); + + final GlobalKey viewKey = GlobalKey(); + final GlobalKey childKey = GlobalKey(); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox(key: key1), + ViewAnchor( + key: key2, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: SizedBox( + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ), + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + child: const SizedBox(), + ), + SizedBox(key: key4), + ], + )); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox( + key: key1, + ), + ViewAnchor( + key: key2, + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: const SizedBox(), + ), + child: const SizedBox(), + ), + SizedBox( + key: key4, + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ], + )); + + await tester.pumpWidget(Column( + children: <Widget>[ + SizedBox(key: key1), + ViewAnchor( + key: key2, + view: View( + key: viewKey, + view: FakeView(tester.view), + child: SizedBox( + child: ColoredBox( + key: childKey, + color: Colors.green, + ), + ), + ), + child: const SizedBox(), + ), + ViewAnchor( + key: key3, + child: const SizedBox(), + ), + SizedBox(key: key4), + ], + )); + }); + + testWidgetsWithLeakTracking('Can globalkey move down the tree from a view that is going away', (WidgetTester tester) async { + final FlutterView anchorView = FakeView(tester.view); + final Widget globalKeyChild = SizedBox( + key: GlobalKey(), + ); + + await tester.pumpWidget( + ColoredBox( + color: Colors.green, + child: ViewAnchor( + view: View( + view: anchorView, + child: ColoredBox( + color: Colors.yellow, + child: globalKeyChild, + ), + ), + child: const ColoredBox(color: Colors.red), + ), + ), + ); + + expect(findsColoredBox(Colors.green), findsOneWidget); + expect(findsColoredBox(Colors.yellow), findsOneWidget); + expect( + find.descendant( + of: findsColoredBox(Colors.yellow), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect(findsColoredBox(Colors.red), findsOneWidget); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + expect(find.byType(SizedBox), findsOneWidget); + final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); + + await tester.pumpWidget( + ColoredBox( + color: Colors.green, + child: ViewAnchor( + child: ColoredBox( + color: Colors.red, + child: globalKeyChild, + ), + ), + ), + ); + expect(findsColoredBox(Colors.green), findsOneWidget); + expect(findsColoredBox(Colors.yellow), findsNothing); + expect( + find.descendant( + of: findsColoredBox(Colors.yellow), + matching: find.byType(SizedBox), + ), + findsNothing, + ); + expect(findsColoredBox(Colors.red), findsOneWidget); + expect( + find.descendant( + of: findsColoredBox(Colors.red), + matching: find.byType(SizedBox), + ), + findsOneWidget, + ); + expect(find.byType(SizedBox), findsOneWidget); + expect( + tester.renderObject(find.byKey(globalKeyChild.key!)), + boxWithGlobalKey, + ); + }); + + testWidgetsWithLeakTracking('RenderObjects are disposed when a view goes away from a ViewAnchor', (WidgetTester tester) async { + final FlutterView anchorView = FakeView(tester.view); + + await tester.pumpWidget( + ColoredBox( + color: Colors.green, + child: ViewAnchor( + view: View( + view: anchorView, + child: const ColoredBox(color: Colors.yellow), + ), + child: const ColoredBox(color: Colors.red), + ), + ), + ); + + final RenderObject box = tester.renderObject(findsColoredBox(Colors.yellow)); + + await tester.pumpWidget( + const ColoredBox( + color: Colors.green, + child: ViewAnchor( + child: ColoredBox(color: Colors.red), + ), + ), + ); + + expect(box.debugDisposed, isTrue); + }); + + testWidgetsWithLeakTracking('RenderObjects are disposed when a view goes away from a ViewCollection', (WidgetTester tester) async { + final FlutterView redView = tester.view; + final FlutterView greenView = FakeView(tester.view); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: redView, + child: const ColoredBox(color: Colors.red), + ), + View( + view: greenView, + child: const ColoredBox(color: Colors.green), + ), + ], + ), + ); + + expect(findsColoredBox(Colors.green), findsOneWidget); + expect(findsColoredBox(Colors.red), findsOneWidget); + final RenderObject box = tester.renderObject(findsColoredBox(Colors.green)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: redView, + child: const ColoredBox(color: Colors.red), + ), + ], + ), + ); + + expect(findsColoredBox(Colors.green), findsNothing); + expect(findsColoredBox(Colors.red), findsOneWidget); + expect(box.debugDisposed, isTrue); + }); + + testWidgetsWithLeakTracking('View can be wrapped and unwrapped', (WidgetTester tester) async { + final Widget view = View( + view: tester.view, + child: const SizedBox(), + ); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: view, + ); + + final RenderObject renderView = tester.renderObject(find.byType(View)); + final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[view], + ), + ); + + expect(tester.renderObject(find.byType(View)), same(renderView)); + expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: view, + ); + + expect(tester.renderObject(find.byType(View)), same(renderView)); + expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox)); + }); + + testWidgetsWithLeakTracking('ViewAnchor with View can be wrapped and unwrapped', (WidgetTester tester) async { + final Widget viewAnchor = ViewAnchor( + view: View( + view: FakeView(tester.view), + child: const SizedBox(), + ), + child: const ColoredBox(color: Colors.green), + ); + + await tester.pumpWidget(viewAnchor); + + final List<RenderObject> renderViews = tester.renderObjectList(find.byType(View)).toList(); + final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox)); + + await tester.pumpWidget(ColoredBox(color: Colors.yellow, child: viewAnchor)); + + expect(tester.renderObjectList(find.byType(View)), renderViews); + expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox)); + + await tester.pumpWidget(viewAnchor); + + expect(tester.renderObjectList(find.byType(View)), renderViews); + expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox)); + }); + + testWidgetsWithLeakTracking('Moving a View keeps its semantics tree stable', (WidgetTester tester) async { + final Widget view = View( + // No explicit key, we rely on the implicit key of the underlying RawView. + view: tester.view, + child: Semantics( + textDirection: TextDirection.ltr, + label: 'Hello', + child: const SizedBox(), + ) + ); + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: view, + ); + + final RenderObject renderSemantics = tester.renderObject(find.bySemanticsLabel('Hello')); + final SemanticsNode semantics = tester.getSemantics(find.bySemanticsLabel('Hello')); + expect(semantics.id, 1); + expect(renderSemantics.debugSemantics, same(semantics)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + view, + ], + ), + ); + + final RenderObject renderSemanticsAfterMove = tester.renderObject(find.bySemanticsLabel('Hello')); + final SemanticsNode semanticsAfterMove = tester.getSemantics(find.bySemanticsLabel('Hello')); + expect(renderSemanticsAfterMove, same(renderSemantics)); + expect(semanticsAfterMove.id, 1); + expect(semanticsAfterMove, same(semantics)); + }); +} + +Finder findsColoredBox(Color color) { + return find.byWidgetPredicate((Widget widget) => widget is ColoredBox && widget.color == color); +} + +Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { + tester.binding.attachRootWidget(widget); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} + +class FakeView extends TestFlutterView{ + FakeView(FlutterView view, { this.viewId = 100 }) : super( + view: view, + platformDispatcher: view.platformDispatcher as TestPlatformDispatcher, + display: view.display as TestDisplay, + ); + + @override + final int viewId; +} diff --git a/packages/flutter/test/widgets/tween_animation_builder_test.dart b/packages/flutter/test/widgets/tween_animation_builder_test.dart index afad96ecfe1d5..4c19cda2dfcdf 100644 --- a/packages/flutter/test/widgets/tween_animation_builder_test.dart +++ b/packages/flutter/test/widgets/tween_animation_builder_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Animates forward when built', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Animates forward when built', (WidgetTester tester) async { final List<int> values = <int>[]; int endCount = 0; await tester.pumpWidget( @@ -37,7 +38,7 @@ void main() { expect(values, <int>[10, 60, 110]); }); - testWidgets('No initial animation when begin=null', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No initial animation when begin=null', (WidgetTester tester) async { final List<int> values = <int>[]; int endCount = 0; await tester.pumpWidget( @@ -61,7 +62,7 @@ void main() { }); - testWidgets('No initial animation when begin=end', (WidgetTester tester) async { + testWidgetsWithLeakTracking('No initial animation when begin=end', (WidgetTester tester) async { final List<int> values = <int>[]; int endCount = 0; await tester.pumpWidget( @@ -84,7 +85,7 @@ void main() { expect(values, <int>[100]); }); - testWidgets('Replace tween animates new tween', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Replace tween animates new tween', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -112,7 +113,7 @@ void main() { expect(values, <int>[0, 100, 100, 150, 200]); }); - testWidgets('Curve is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Curve is respected', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween, required Curve curve}) { return TweenAnimationBuilder<int>( @@ -142,7 +143,7 @@ void main() { expect(values, <int>[100, 150]); }); - testWidgets('Duration is respected', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Duration is respected', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween, required Duration duration}) { return TweenAnimationBuilder<int>( @@ -170,7 +171,7 @@ void main() { expect(values, <int>[100, 125]); }); - testWidgets('Child is integrated into tree', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Child is integrated into tree', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -189,7 +190,7 @@ void main() { }); group('Change tween gapless while', () { - testWidgets('running forward', (WidgetTester tester) async { + testWidgetsWithLeakTracking('running forward', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -224,7 +225,7 @@ void main() { values.clear(); }); - testWidgets('running forward and then reverse with same tween instance', (WidgetTester tester) async { + testWidgetsWithLeakTracking('running forward and then reverse with same tween instance', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -254,7 +255,7 @@ void main() { }); }); - testWidgets('Changing tween while gapless tween change is in progress', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing tween while gapless tween change is in progress', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -294,7 +295,7 @@ void main() { expect(values, <int>[175, 338, 501]); }); - testWidgets('Changing curve while no animation is running does not trigger animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Changing curve while no animation is running does not trigger animation', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required Curve curve}) { return TweenAnimationBuilder<int>( @@ -323,7 +324,7 @@ void main() { expect(values, <int>[100]); }); - testWidgets('Setting same tween and direction does not trigger animation', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting same tween and direction does not trigger animation', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -352,7 +353,7 @@ void main() { expect(values, everyElement(100)); }); - testWidgets('Setting same tween and direction while gapless animation is in progress works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Setting same tween and direction while gapless animation is in progress works', (WidgetTester tester) async { final List<int> values = <int>[]; Widget buildWidget({required IntTween tween}) { return TweenAnimationBuilder<int>( @@ -388,7 +389,7 @@ void main() { expect(values, everyElement(300)); }); - testWidgets('Works with nullable tweens', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Works with nullable tweens', (WidgetTester tester) async { final List<Size?> values = <Size?>[]; await tester.pumpWidget( TweenAnimationBuilder<Size?>( diff --git a/packages/flutter/test/widgets/two_dimensional_scroll_view_test.dart b/packages/flutter/test/widgets/two_dimensional_scroll_view_test.dart index 9da89311ceb4b..f1cb8d5c30131 100644 --- a/packages/flutter/test/widgets/two_dimensional_scroll_view_test.dart +++ b/packages/flutter/test/widgets/two_dimensional_scroll_view_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/gestures/monodrag.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'two_dimensional_utils.dart'; @@ -19,34 +20,40 @@ Widget? _testChildBuilder(BuildContext context, ChildVicinity vicinity) { void main() { group('TwoDimensionalScrollView',() { - testWidgets('asserts the axis directions do not conflict with one another', (WidgetTester tester) async { + testWidgetsWithLeakTracking('asserts the axis directions do not conflict with one another', (WidgetTester tester) async { final List<Object> exceptions = <Object>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { exceptions.add(details.exception); }; // Horizontal wrong + late final TwoDimensionalChildBuilderDelegate delegate1; + addTearDown(() => delegate1.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( - delegate: TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), + delegate: delegate1 = TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), horizontalDetails: const ScrollableDetails.vertical(), // Horizontal has default const ScrollableDetails.horizontal() ), )); // Vertical wrong + late final TwoDimensionalChildBuilderDelegate delegate2; + addTearDown(() => delegate2.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( - delegate: TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), + delegate: delegate2 = TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), verticalDetails: const ScrollableDetails.horizontal(), // Horizontal has default const ScrollableDetails.horizontal() ), )); // Both wrong + late final TwoDimensionalChildBuilderDelegate delegate3; + addTearDown(() => delegate3.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( - delegate: TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), + delegate: delegate3 = TwoDimensionalChildBuilderDelegate(builder: (_, __) => null), verticalDetails: const ScrollableDetails.horizontal(), horizontalDetails: const ScrollableDetails.vertical(), ), @@ -60,15 +67,19 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('ScrollableDetails.controller can set initial scroll positions, modify within bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ScrollableDetails.controller can set initial scroll positions, modify within bounds', (WidgetTester tester) async { final ScrollController verticalController = ScrollController(initialScrollOffset: 100); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(initialScrollOffset: 50); + addTearDown(horizontalController.dispose); + late final TwoDimensionalChildBuilderDelegate delegate; + addTearDown(() => delegate.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( verticalDetails: ScrollableDetails.vertical(controller: verticalController), horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController), - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate = TwoDimensionalChildBuilderDelegate( builder: _testChildBuilder, maxXIndex: 99, maxYIndex: 99, @@ -99,13 +110,20 @@ void main() { expect(horizontalController.position.pixels, 19200); }, variant: TargetPlatformVariant.all()); - testWidgets('Properly assigns the PrimaryScrollController to the main axis on the correct platform', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Properly assigns the PrimaryScrollController to the main axis on the correct platform', (WidgetTester tester) async { late ScrollController controller; Widget buildForPrimaryScrollController({ bool? explicitPrimary, Axis mainAxis = Axis.vertical, bool addControllerConflict = false, }) { + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); + late final TwoDimensionalChildBuilderDelegate delegate; + addTearDown(() => delegate.dispose()); + return MaterialApp( home: PrimaryScrollController( controller: controller, @@ -114,15 +132,15 @@ void main() { primary: explicitPrimary, verticalDetails: ScrollableDetails.vertical( controller: addControllerConflict && mainAxis == Axis.vertical - ? ScrollController() + ? verticalController : null ), horizontalDetails: ScrollableDetails.horizontal( controller: addControllerConflict && mainAxis == Axis.horizontal - ? ScrollController() + ? horizontalController : null ), - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate = TwoDimensionalChildBuilderDelegate( builder: _testChildBuilder, maxXIndex: 99, maxYIndex: 99, @@ -134,6 +152,7 @@ void main() { // Horizontal default - horizontal never automatically adopts PSC controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( mainAxis: Axis.horizontal, )); @@ -151,6 +170,7 @@ void main() { // Horizontal explicitly true controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( mainAxis: Axis.horizontal, explicitPrimary: true, @@ -171,6 +191,7 @@ void main() { // Horizontal explicitly false controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( mainAxis: Axis.horizontal, explicitPrimary: false, @@ -190,6 +211,7 @@ void main() { // Vertical default controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController()); await tester.pumpAndSettle(); @@ -209,6 +231,7 @@ void main() { // Vertical explicitly true controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( explicitPrimary: true, )); @@ -228,6 +251,7 @@ void main() { // Vertical explicitly false controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( explicitPrimary: false, )); @@ -253,6 +277,7 @@ void main() { // Vertical asserts ScrollableDetails.controller has not been provided if // primary is explicitly set controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( explicitPrimary: true, addControllerConflict: true, @@ -268,6 +293,7 @@ void main() { // Horizontal asserts ScrollableDetails.controller has not been provided // if primary is explicitly set true controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget(buildForPrimaryScrollController( mainAxis: Axis.horizontal, explicitPrimary: true, @@ -282,12 +308,14 @@ void main() { FlutterError.onError = oldHandler; }, variant: TargetPlatformVariant.all()); - testWidgets('TwoDimensionalScrollable receives the correct details from TwoDimensionalScrollView', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TwoDimensionalScrollable receives the correct details from TwoDimensionalScrollView', (WidgetTester tester) async { late BuildContext capturedContext; // Default + late final TwoDimensionalChildBuilderDelegate delegate1; + addTearDown(() => delegate1.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate1 = TwoDimensionalChildBuilderDelegate( builder: (BuildContext context, ChildVicinity vicinity) { capturedContext = context; return Text(vicinity.toString()); @@ -305,13 +333,15 @@ void main() { expect(scrollable.widget.dragStartBehavior, DragStartBehavior.start); // Customized + late final TwoDimensionalChildBuilderDelegate delegate2; + addTearDown(() => delegate2.dispose()); await tester.pumpWidget(MaterialApp( home: SimpleBuilderTableView( verticalDetails: const ScrollableDetails.vertical(reverse: true), horizontalDetails: const ScrollableDetails.horizontal(reverse: true), diagonalDragBehavior: DiagonalDragBehavior.weightedContinuous, dragStartBehavior: DragStartBehavior.down, - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate2 = TwoDimensionalChildBuilderDelegate( builder: _testChildBuilder, ), ), diff --git a/packages/flutter/test/widgets/two_dimensional_utils.dart b/packages/flutter/test/widgets/two_dimensional_utils.dart index 5eb3bf2602082..a518f52170a13 100644 --- a/packages/flutter/test/widgets/two_dimensional_utils.dart +++ b/packages/flutter/test/widgets/two_dimensional_utils.dart @@ -17,6 +17,7 @@ final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBu maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { return Container( + key: ValueKey<ChildVicinity>(vicinity), color: vicinity.xIndex.isEven && vicinity.yIndex.isEven ? Colors.amber[100] : (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd @@ -192,6 +193,18 @@ class RenderSimpleBuilderTableViewport extends RenderTwoDimensionalViewport { RenderBox? testGetChildFor(ChildVicinity vicinity) => getChildFor(vicinity); + @override + TestExtendedParentData parentDataOf(RenderBox child) { + return child.parentData! as TestExtendedParentData; + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! TestExtendedParentData) { + child.parentData = TestExtendedParentData(); + } + } + @override void layoutChildSequence() { // Really simple table implementation for testing. @@ -393,18 +406,18 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport { final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate; final int rowCount; final int columnCount; - rowCount = listDelegate.children.length - 1; - columnCount = listDelegate.children[0].length - 1; + rowCount = listDelegate.children.length; + columnCount = listDelegate.children[0].length; final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0); final int leadingRow = math.max((verticalPixels / 200).floor(), 0); final int trailingColumn = math.min( ((horizontalPixels + viewportDimension.width) / 200).ceil(), - columnCount, + columnCount - 1, ); final int trailingRow = math.min( ((verticalPixels + viewportDimension.height) / 200).ceil(), - rowCount, + rowCount - 1, ); double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels; @@ -420,7 +433,80 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport { } xLayoutOffset += 200; } - verticalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.height); - horizontalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.width); + + verticalOffset.applyContentDimensions( + 0.0, + math.max(200 * rowCount - viewportDimension.height, 0.0), + ); + horizontalOffset.applyContentDimensions( + 0, + math.max(200 * columnCount - viewportDimension.width, 0.0), + ); } } + +class KeepAliveCheckBox extends StatefulWidget { + const KeepAliveCheckBox({ super.key }); + + @override + KeepAliveCheckBoxState createState() => KeepAliveCheckBoxState(); +} + +class KeepAliveCheckBoxState extends State<KeepAliveCheckBox> with AutomaticKeepAliveClientMixin { + bool checkValue = false; + + @override + bool get wantKeepAlive => _wantKeepAlive; + bool _wantKeepAlive = false; + set wantKeepAlive(bool value) { + if (_wantKeepAlive != value) { + _wantKeepAlive = value; + updateKeepAlive(); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Checkbox( + value: checkValue, + onChanged: (bool? value) { + if (checkValue != value) { + setState(() { + checkValue = value!; + wantKeepAlive = value; + }); + } + }, + ); + } +} + +// TwoDimensionalViewportParentData already mixes in KeepAliveParentDataMixin, +// and so should be compatible with both the KeepAlive and +// TestParentDataWidget ParentDataWidgets. +// This ParentData is set up above as part of the +// RenderSimpleBuilderTableViewport for testing. +class TestExtendedParentData extends TwoDimensionalViewportParentData { + int? testValue; +} + +class TestParentDataWidget extends ParentDataWidget<TestExtendedParentData> { + const TestParentDataWidget({ + super.key, + required super.child, + this.testValue, + }); + + final int? testValue; + + @override + void applyParentData(RenderObject renderObject) { + assert(renderObject.parentData is TestExtendedParentData); + final TestExtendedParentData parentData = renderObject.parentData! as TestExtendedParentData; + parentData.testValue = testValue; + } + + @override + Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport; +} diff --git a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart index 53fb879e0759b..b7432de826f3e 100644 --- a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart +++ b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart @@ -7,16 +7,19 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'two_dimensional_utils.dart'; void main() { group('TwoDimensionalChildDelegate', () { group('TwoDimensionalChildBuilderDelegate', () { - testWidgets('repaintBoundaries', (WidgetTester tester) async { + testWidgetsWithLeakTracking('repaintBoundaries', (WidgetTester tester) async { // Default - adds repaint boundaries + late final TwoDimensionalChildBuilderDelegate delegate1; + addTearDown(() => delegate1.dispose()); await tester.pumpWidget(simpleBuilderTest( - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate1 = TwoDimensionalChildBuilderDelegate( // Only build 1 child maxXIndex: 0, maxYIndex: 0, @@ -43,8 +46,10 @@ void main() { } // None + late final TwoDimensionalChildBuilderDelegate delegate2; + addTearDown(() => delegate2.dispose()); await tester.pumpWidget(simpleBuilderTest( - delegate: TwoDimensionalChildBuilderDelegate( + delegate: delegate2 = TwoDimensionalChildBuilderDelegate( // Only build 1 child maxXIndex: 0, maxYIndex: 0, @@ -72,7 +77,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('will return null from build for exceeding maxXIndex and maxYIndex', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will return null from build for exceeding maxXIndex and maxYIndex', (WidgetTester tester) async { late BuildContext capturedContext; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( // Only build 1 child @@ -88,6 +93,8 @@ void main() { ); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -185,7 +192,7 @@ void main() { ); }); - testWidgets('throws an error when builder throws', (WidgetTester tester) async { + testWidgetsWithLeakTracking('throws an error when builder throws', (WidgetTester tester) async { final List<Object> exceptions = <Object>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { @@ -200,6 +207,8 @@ void main() { throw 'Builder error!'; } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -211,13 +220,297 @@ void main() { expect(exceptions[0] as String, contains('Builder error!')); }, variant: TargetPlatformVariant.all()); - testWidgets('shouldRebuild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shouldRebuild', (WidgetTester tester) async { expect(builderDelegate.shouldRebuild(builderDelegate), isTrue); }, variant: TargetPlatformVariant.all()); + + testWidgetsWithLeakTracking('builder delegate supports automatic keep alive - default true', (WidgetTester tester) async { + const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final UniqueKey checkBoxKey = UniqueKey(); + final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate( + maxXIndex: 5, + maxYIndex: 5, + builder: (BuildContext context, ChildVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Center(child: vicinity == firstCell + ? KeepAliveCheckBox(key: checkBoxKey) + : Text('R${vicinity.xIndex}:C${vicinity.yIndex}') + ), + ); + } + ); + addTearDown(builderDelegate.dispose); + + await tester.pumpWidget(simpleBuilderTest( + delegate: builderDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + + expect(verticalController.hasClients, isTrue); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + // Scroll away, disposing of the checkbox. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, still unchecked, not kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + await tester.tap(find.byKey(checkBoxKey)); + await tester.pumpAndSettle(); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Scroll away again, checkbox should be kept alive now. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Bring back into view, still checked, after being kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + }); + + testWidgets('Keep alive works with additional parent data widgets', (WidgetTester tester) async { + const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0); + final ScrollController verticalController = ScrollController(); + final UniqueKey checkBoxKey = UniqueKey(); + final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate( + maxXIndex: 5, + maxYIndex: 5, + addRepaintBoundaries: false, + builder: (BuildContext context, ChildVicinity vicinity) { + // The delegate will add a KeepAlive ParentDataWidget, this add an + // additional ParentDataWidget. + return TestParentDataWidget( + testValue: 20, + child: SizedBox.square( + dimension: 200, + child: Center(child: vicinity == firstCell + ? KeepAliveCheckBox(key: checkBoxKey) + : Text('R${vicinity.xIndex}:C${vicinity.yIndex}') + ), + ), + ); + } + ); + + await tester.pumpWidget(simpleBuilderTest( + delegate: builderDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + + expect(verticalController.hasClients, isTrue); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + RenderSimpleBuilderTableViewport viewport = getViewport(tester, checkBoxKey) as RenderSimpleBuilderTableViewport; + TestExtendedParentData parentData = viewport.parentDataOf(viewport.testGetChildFor(firstCell)!); + // Check parent data from both ParentDataWidgets + expect(parentData.testValue, 20); + expect(parentData.keepAlive, isFalse); + + // Scroll away, disposing of the checkbox. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, still unchecked, not kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + // Check the box to set keep alive to true. + await tester.tap(find.byKey(checkBoxKey)); + await tester.pumpAndSettle(); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + viewport = getViewport(tester, checkBoxKey) as RenderSimpleBuilderTableViewport; + parentData = viewport.parentDataOf(viewport.testGetChildFor(firstCell)!); + // Check parent data from both ParentDataWidgets + expect(parentData.testValue, 20); + expect(parentData.keepAlive, isTrue); + + // Scroll away again, checkbox should be kept alive now. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + viewport = getViewport(tester, checkBoxKey) as RenderSimpleBuilderTableViewport; + parentData = viewport.parentDataOf(viewport.testGetChildFor(firstCell)!); + // Check parent data from both ParentDataWidgets + expect(parentData.testValue, 20); + expect(parentData.keepAlive, isTrue); + + // Bring back into view, still checked, after being kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + viewport = getViewport(tester, checkBoxKey) as RenderSimpleBuilderTableViewport; + parentData = viewport.parentDataOf(viewport.testGetChildFor(firstCell)!); + // Check parent data from both ParentDataWidgets + expect(parentData.testValue, 20); + expect(parentData.keepAlive, isTrue); + }); + + testWidgetsWithLeakTracking('builder delegate will not add automatic keep alives', (WidgetTester tester) async { + const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final UniqueKey checkBoxKey = UniqueKey(); + final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate( + maxXIndex: 5, + maxYIndex: 5, + addAutomaticKeepAlives: false, // No keeping alive this time + builder: (BuildContext context, ChildVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Center(child: vicinity == firstCell + ? KeepAliveCheckBox(key: checkBoxKey) + : Text('R${vicinity.xIndex}:C${vicinity.yIndex}') + ), + ); + } + ); + addTearDown(builderDelegate.dispose); + + await tester.pumpWidget(simpleBuilderTest( + delegate: builderDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + + expect(verticalController.hasClients, isTrue); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + // Scroll away, disposing of the checkbox. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, still unchecked, not kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + await tester.tap(find.byKey(checkBoxKey)); + await tester.pumpAndSettle(); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Scroll away again, checkbox should not be kept alive since the + // delegate did not add automatic keep alive. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 600.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, not checked, having not been kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + }); }); group('TwoDimensionalChildListDelegate', () { - testWidgets('repaintBoundaries', (WidgetTester tester) async { + testWidgetsWithLeakTracking('repaintBoundaries', (WidgetTester tester) async { final List<List<Widget>> children = <List<Widget>>[]; children.add(<Widget>[ const SizedBox( @@ -227,8 +520,10 @@ void main() { ) ]); // Default - adds repaint boundaries + late final TwoDimensionalChildListDelegate delegate1; + addTearDown(() => delegate1.dispose()); await tester.pumpWidget(simpleListTest( - delegate: TwoDimensionalChildListDelegate( + delegate: delegate1 = TwoDimensionalChildListDelegate( // Only builds 1 child children: children, ) @@ -260,8 +555,10 @@ void main() { } // None + late final TwoDimensionalChildListDelegate delegate2; + addTearDown(() => delegate2.dispose()); await tester.pumpWidget(simpleListTest( - delegate: TwoDimensionalChildListDelegate( + delegate: delegate2 = TwoDimensionalChildListDelegate( // Different children triggers rebuild children: <List<Widget>>[<Widget>[Container()]], addRepaintBoundaries: false, @@ -285,7 +582,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('will return null for a ChildVicinity outside of list bounds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will return null for a ChildVicinity outside of list bounds', (WidgetTester tester) async { final List<List<Widget>> children = <List<Widget>>[]; children.add(<Widget>[ const SizedBox( @@ -298,6 +595,7 @@ void main() { // Only builds 1 child children: children, ); + addTearDown(delegate.dispose); // X index expect( @@ -317,7 +615,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('shouldRebuild', (WidgetTester tester) async { + testWidgetsWithLeakTracking('shouldRebuild', (WidgetTester tester) async { final List<List<Widget>> children = <List<Widget>>[]; children.add(<Widget>[ const SizedBox( @@ -330,20 +628,194 @@ void main() { // Only builds 1 child children: children, ); + addTearDown(delegate.dispose); expect(delegate.shouldRebuild(delegate), isFalse); final List<List<Widget>> newChildren = <List<Widget>>[]; final TwoDimensionalChildListDelegate oldDelegate = TwoDimensionalChildListDelegate( children: newChildren, ); + addTearDown(oldDelegate.dispose); expect(delegate.shouldRebuild(oldDelegate), isTrue); }, variant: TargetPlatformVariant.all()); }); + + testWidgetsWithLeakTracking('list delegate supports automatic keep alive - default true', (WidgetTester tester) async { + final UniqueKey checkBoxKey = UniqueKey(); + final Widget originCell = SizedBox.square( + dimension: 200, + child: Center(child: KeepAliveCheckBox(key: checkBoxKey) + ), + ); + const Widget otherCell = SizedBox.square(dimension: 200); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate( + children: <List<Widget>>[ + <Widget>[originCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + ], + ); + addTearDown(listDelegate.dispose); + + await tester.pumpWidget(simpleListTest( + delegate: listDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + + expect(verticalController.hasClients, isTrue); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + // Scroll away, disposing of the checkbox. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 400.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, still unchecked, not kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + await tester.tap(find.byKey(checkBoxKey)); + await tester.pumpAndSettle(); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Scroll away again, checkbox should be kept alive now. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 400.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Bring back into view, still checked, after being kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + }); + + testWidgetsWithLeakTracking('list delegate will not add automatic keep alives', (WidgetTester tester) async { + final UniqueKey checkBoxKey = UniqueKey(); + final Widget originCell = SizedBox.square( + dimension: 200, + child: Center(child: KeepAliveCheckBox(key: checkBoxKey) + ), + ); + const Widget otherCell = SizedBox.square(dimension: 200); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate( + addAutomaticKeepAlives: false, + children: <List<Widget>>[ + <Widget>[originCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + <Widget>[otherCell, otherCell, otherCell, otherCell, otherCell], + ], + ); + addTearDown(listDelegate.dispose); + + await tester.pumpWidget(simpleListTest( + delegate: listDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + + expect(verticalController.hasClients, isTrue); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + // Scroll away, disposing of the checkbox. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 400.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, still unchecked, not kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + await tester.tap(find.byKey(checkBoxKey)); + await tester.pumpAndSettle(); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isTrue, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isTrue, + ); + + // Scroll away again, checkbox should not be kept alive since the + // delegate did not add automatic keep alive. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(verticalController.position.pixels, 400.0); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view, not checked, having not been kept alive. + verticalController.jumpTo(0.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(find.byKey(checkBoxKey), findsOneWidget); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue, + isFalse, + ); + expect( + tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive, + isFalse, + ); + }); }); group('TwoDimensionalScrollable', () { - testWidgets('.of, .maybeOf', (WidgetTester tester) async { + testWidgetsWithLeakTracking('.of, .maybeOf', (WidgetTester tester) async { late BuildContext capturedContext; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -353,6 +825,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -380,7 +854,7 @@ void main() { expect(TwoDimensionalScrollable.maybeOf(capturedContext), isNull); }, variant: TargetPlatformVariant.all()); - testWidgets('horizontal and vertical getters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('horizontal and vertical getters', (WidgetTester tester) async { late BuildContext capturedContext; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -390,6 +864,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -400,7 +876,7 @@ void main() { expect(scrollable.horizontalScrollable.position.pixels, 0.0); }, variant: TargetPlatformVariant.all()); - testWidgets('creates fallback ScrollControllers if not provided by ScrollableDetails', (WidgetTester tester) async { + testWidgetsWithLeakTracking('creates fallback ScrollControllers if not provided by ScrollableDetails', (WidgetTester tester) async { late BuildContext capturedContext; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -410,6 +886,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -423,7 +901,7 @@ void main() { expect(horizontal.widget.controller, isNotNull); }, variant: TargetPlatformVariant.all()); - testWidgets('asserts the axis directions do not conflict with one another', (WidgetTester tester) async { + testWidgetsWithLeakTracking('asserts the axis directions do not conflict with one another', (WidgetTester tester) async { final List<Object> exceptions = <Object>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { @@ -464,7 +942,7 @@ void main() { FlutterError.onError = oldHandler; }, variant: TargetPlatformVariant.all()); - testWidgets('correctly sets restorationIds', (WidgetTester tester) async { + testWidgetsWithLeakTracking('correctly sets restorationIds', (WidgetTester tester) async { late BuildContext capturedContext; // with restorationID set await tester.pumpWidget(WidgetsApp( @@ -534,7 +1012,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('Restoration works', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Restoration works', (WidgetTester tester) async { await tester.pumpWidget(WidgetsApp( color: const Color(0xFFFFFFFF), restorationScopeId: 'Test ID', @@ -559,7 +1037,7 @@ void main() { await restoreScrollAndVerify(tester); }, variant: TargetPlatformVariant.all()); - testWidgets('Inner Scrollables receive the correct details from TwoDimensionalScrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Inner Scrollables receive the correct details from TwoDimensionalScrollable', (WidgetTester tester) async { // Default late BuildContext capturedContext; await tester.pumpWidget(TwoDimensionalScrollable( @@ -605,7 +1083,9 @@ void main() { // Customized final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); double calculator(_) => 0.0; await tester.pumpWidget(TwoDimensionalScrollable( incrementCalculator: calculator, @@ -675,10 +1155,12 @@ void main() { }, variant: TargetPlatformVariant.all()); group('DiagonalDragBehavior', () { - testWidgets('none (default)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('none (default)', (WidgetTester tester) async { // Vertical and horizontal axes are locked. final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: simpleBuilderTest( @@ -725,11 +1207,13 @@ void main() { expect(horizontalController.position.pixels, 140.0); }, variant: TargetPlatformVariant.all()); - testWidgets('weightedEvent', (WidgetTester tester) async { + testWidgetsWithLeakTracking('weightedEvent', (WidgetTester tester) async { // For weighted event, the winning axis is locked for the duration of // the gesture. final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: simpleBuilderTest( @@ -847,13 +1331,15 @@ void main() { await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.all()); - testWidgets('weightedContinuous', (WidgetTester tester) async { + testWidgetsWithLeakTracking('weightedContinuous', (WidgetTester tester) async { // For weighted continuous, the winning axis can change if the axis // differential for the gesture exceeds kTouchSlop. So it can lock, and // remain locked, if the user maintains a generally straight gesture, // otherwise it will unlock and re-evaluate. final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: simpleBuilderTest( @@ -904,10 +1390,12 @@ void main() { await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.all()); - testWidgets('free', (WidgetTester tester) async { + testWidgetsWithLeakTracking('free', (WidgetTester tester) async { // For free, anything goes. final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: simpleBuilderTest( @@ -949,7 +1437,7 @@ void main() { }); }); - testWidgets('TwoDimensionalViewport asserts against axes mismatch', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TwoDimensionalViewport asserts against axes mismatch', (WidgetTester tester) async { // Horizontal mismatch expect( () { @@ -1027,7 +1515,7 @@ void main() { expect( parentData.toString(), 'vicinity=(xIndex: 10, yIndex: 10); layoutOffset=Offset(20.0, 20.0); ' - 'paintOffset=Offset(20.0, 20.0); not visible ', + 'paintOffset=Offset(20.0, 20.0); not visible; ', ); }); @@ -1124,7 +1612,7 @@ void main() { ); }); - testWidgets('getters', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getters', (WidgetTester tester) async { final UniqueKey childKey = UniqueKey(); final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -1133,6 +1621,7 @@ void main() { return SizedBox.square(key: childKey, dimension: 200); } ); + addTearDown(delegate.dispose); final RenderSimpleBuilderTableViewport renderViewport = RenderSimpleBuilderTableViewport( verticalOffset: ViewportOffset.fixed(10.0), verticalAxisDirection: AxisDirection.down, @@ -1142,6 +1631,7 @@ void main() { mainAxis: Axis.vertical, childManager: _NullBuildContext(), ); + addTearDown(renderViewport.dispose); expect(renderViewport.clipBehavior, Clip.hardEdge); expect(renderViewport.cacheExtent, RenderAbstractViewport.defaultCacheExtent); @@ -1176,16 +1666,17 @@ void main() { expect(viewport.viewportDimension, const Size(800.0, 600.0)); }, variant: TargetPlatformVariant.all()); - testWidgets('Children are organized according to mainAxis', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Children are organized according to mainAxis', (WidgetTester tester) async { final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); TwoDimensionalViewportParentData parentDataOf(RenderBox child) { return child.parentData! as TwoDimensionalViewportParentData; } @@ -1262,7 +1753,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('sets up parent data', (WidgetTester tester) async { + testWidgetsWithLeakTracking('sets up parent data', (WidgetTester tester) async { // Also tests computeAbsolutePaintOffsetFor & computeChildPaintExtent // Regression test for https://github.com/flutter/flutter/issues/128723 final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; @@ -1270,10 +1761,11 @@ void main() { maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); // parent data is TwoDimensionalViewportParentData TwoDimensionalViewportParentData parentDataOf(RenderBox child) { @@ -1373,7 +1865,9 @@ void main() { // Change the scroll positions to test partially visible. final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); final ScrollController horizontalController = ScrollController(); + addTearDown(horizontalController.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController), @@ -1393,16 +1887,17 @@ void main() { expect(childParentData.layoutOffset, const Offset(-50.0, -50.0)); }, variant: TargetPlatformVariant.all()); - testWidgets('debugDescribeChildren', (WidgetTester tester) async { + testWidgetsWithLeakTracking('debugDescribeChildren', (WidgetTester tester) async { final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1464,7 +1959,7 @@ void main() { expect((exceptions[0] as FlutterError).message, contains('unbounded')); }, variant: TargetPlatformVariant.all()); - testWidgets('computeDryLayout asserts axes are bounded', (WidgetTester tester) async { + testWidgetsWithLeakTracking('computeDryLayout asserts axes are bounded', (WidgetTester tester) async { final UniqueKey childKey = UniqueKey(); final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -1473,6 +1968,8 @@ void main() { return SizedBox.square(key: childKey, dimension: 200); } ); + addTearDown(delegate.dispose); + // Call computeDryLayout with unbounded constraints await tester.pumpWidget(simpleBuilderTest(delegate: delegate)); final RenderTwoDimensionalViewport viewport = getViewport( @@ -1493,7 +1990,7 @@ void main() { ); }, variant: TargetPlatformVariant.all()); - testWidgets('correctly resizes dimensions', (WidgetTester tester) async { + testWidgetsWithLeakTracking('correctly resizes dimensions', (WidgetTester tester) async { final UniqueKey childKey = UniqueKey(); final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -1502,6 +1999,8 @@ void main() { return SizedBox.square(key: childKey, dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -1523,7 +2022,7 @@ void main() { tester.view.resetDevicePixelRatio(); }, variant: TargetPlatformVariant.all()); - testWidgets('Rebuilds when delegate changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Rebuilds when delegate changes', (WidgetTester tester) async { final UniqueKey firstChildKey = UniqueKey(); final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 0, @@ -1533,6 +2032,8 @@ void main() { return SizedBox.square(key: firstChildKey, dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, )); @@ -1548,6 +2049,8 @@ void main() { return Container(key: newChildKey, height: 300, width: 300, color: const Color(0xFFFFFFFF)); } ); + addTearDown(() => newDelegate.dispose()); + await tester.pumpWidget(simpleBuilderTest( delegate: newDelegate, )); @@ -1558,14 +2061,14 @@ void main() { expect(viewport.firstChild, tester.renderObject<RenderBox>(find.byKey(newChildKey))); }, variant: TargetPlatformVariant.all()); - testWidgets('hitTestChildren', (WidgetTester tester) async { + testWidgetsWithLeakTracking('hitTestChildren', (WidgetTester tester) async { final List<ChildVicinity> taps = <ChildVicinity>[]; final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 19, maxYIndex: 19, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square( dimension: 200, child: Center( @@ -1579,6 +2082,7 @@ void main() { ); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1627,16 +2131,17 @@ void main() { expect(taps.contains(const ChildVicinity(xIndex: 5, yIndex: 5)), isFalse); }, variant: TargetPlatformVariant.all()); - testWidgets('getChildFor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('getChildFor', (WidgetTester tester) async { final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1669,10 +2174,11 @@ void main() { maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1705,6 +2211,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, // Will cause the test implementation to not set dimensions @@ -1714,9 +2222,10 @@ void main() { expect(error.message, contains('was not given content dimensions')); }, variant: TargetPlatformVariant.all()); - testWidgets('will not rebuild a child if it can be reused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('will not rebuild a child if it can be reused', (WidgetTester tester) async { final List<ChildVicinity> builtChildren = <ChildVicinity>[]; final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, @@ -1725,6 +2234,7 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1753,6 +2263,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, // Will cause the test implementation to not set the layoutOffset of @@ -1771,6 +2283,8 @@ void main() { return const SizedBox.square(dimension: 200); } ); + addTearDown(delegate.dispose); + await tester.pumpWidget(simpleBuilderTest( delegate: delegate, // Will cause the test implementation to not actually layout the @@ -1781,16 +2295,17 @@ void main() { expect(error.toString(), contains('child.hasSize')); }, variant: TargetPlatformVariant.all()); - testWidgets('does not support intrinsics', (WidgetTester tester) async { + testWidgetsWithLeakTracking('does not support intrinsics', (WidgetTester tester) async { final Map<ChildVicinity, UniqueKey> childKeys = <ChildVicinity, UniqueKey>{}; final TwoDimensionalChildBuilderDelegate delegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { - childKeys[vicinity] = UniqueKey(); + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); return SizedBox.square(key: childKeys[vicinity], dimension: 200); } ); + addTearDown(delegate.dispose); await tester.pumpWidget(simpleBuilderTest( delegate: delegate, @@ -1850,6 +2365,317 @@ void main() { ), ); }, variant: TargetPlatformVariant.all()); + + group('showOnScreen & showInViewport', () { + Finder findKey(ChildVicinity vicinity) { + return find.byKey(ValueKey<ChildVicinity>(vicinity)); + } + + testWidgets('getOffsetToReveal', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first; + final RevealedOffset verticalOffset = viewport.getOffsetToReveal( + tester.renderObject(findKey(const ChildVicinity(xIndex: 5, yIndex: 5))), + 1.0, + axis: Axis.vertical, + ); + final RevealedOffset horizontalOffset = viewport.getOffsetToReveal( + tester.renderObject(findKey(const ChildVicinity(xIndex: 5, yIndex: 5))), + 1.0, + axis: Axis.horizontal, + ); + expect(verticalOffset.offset, 600.0); + expect(verticalOffset.rect, const Rect.fromLTRB(1000.0, 400.0, 1200.0, 600.0)); + expect(horizontalOffset.offset, 400.0); + expect(horizontalOffset.rect, const Rect.fromLTRB(600.0, 1000.0, 800.0, 1200.0)); + + // default is to use mainAxis when axis is not provided, mainAxis + // defaults to Axis.vertical. + RevealedOffset defaultOffset = viewport.getOffsetToReveal( + tester.renderObject(findKey(const ChildVicinity(xIndex: 5, yIndex: 5))), + 1.0, + ); + expect(defaultOffset.offset, verticalOffset.offset); + expect(defaultOffset.rect, verticalOffset.rect); + + // mainAxis as Axis.horizontal + await tester.pumpWidget(simpleBuilderTest( + useCacheExtent: true, + mainAxis: Axis.horizontal, + )); + viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first; + defaultOffset = viewport.getOffsetToReveal( + tester.renderObject(findKey(const ChildVicinity(xIndex: 5, yIndex: 5))), + 1.0, + ); + expect(defaultOffset.offset, horizontalOffset.offset); + expect(defaultOffset.rect, horizontalOffset.rect); + }); + + testWidgets('Axis.vertical', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + // Child visible at origin + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(0.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(0.0), + ); + // (0, 3) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(600.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 3)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Now in view + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(400.0), + ); + + // If already visible, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 3)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(400.0), + ); + }); + + testWidgets('Axis.horizontal', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 1, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 1, yIndex: 0))).dx, + equals(200.0), // No change since already fully visible + ); + // (5, 0) is now in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(1000.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Now in view + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(600.0), + ); + + // If already in position, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, + equals(600.0), + ); + }); + + testWidgets('both axes', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); + + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 1, yIndex: 1)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), + const Rect.fromLTRB(200.0, 200.0, 400.0, 400.0), + ); + // (5, 4) is in the cache extent, and will be brought into view next + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 4)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Now in view + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(600.0, 200.0, 800.0, 400.0), + ); + + // If already visible, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 4)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(600.0, 200.0, 800.0, 400.0), + ); + }); + + testWidgets('Axis.vertical reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + verticalDetails: const ScrollableDetails.vertical(reverse: true), + useCacheExtent: true, + )); + + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(400.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Already visible so no change. + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, + equals(400.0), + ); + // (0, 3) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(-200.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 3)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Now in view + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(0.0), + ); + + // If already visible, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 3)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, + equals(0.0), + ); + }); + + testWidgets('Axis.horizontal reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + horizontalDetails: const ScrollableDetails.horizontal(reverse: true), + useCacheExtent: true, + )); + + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, + equals(600.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 0, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Already visible so no change. + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, + equals(600.0), + ); + // (4, 0) is in the cache extent, and will be brought into view next + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(-200.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 4, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(0.0), + ); + + // If already visible, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 4, yIndex: 0)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, + equals(0.0), + ); + }); + + testWidgets('both axes reverse', (WidgetTester tester) async { + await tester.pumpWidget(simpleBuilderTest( + verticalDetails: const ScrollableDetails.vertical(reverse: true), + horizontalDetails: const ScrollableDetails.horizontal(reverse: true), + useCacheExtent: true, + )); + + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 1, yIndex: 1)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), + const Rect.fromLTRB(400.0, 200.0, 600.0, 400.0), + ); + // (5, 4) is in the cache extent, and will be brought into view next + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(-400.0, -400.0, -200.0, -200.0), + ); + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 4)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + // Now in view + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(0.0, 200.0, 200.0, 400.0), + ); + + // If already visible, no change + tester.renderObject(find.byKey( + const ValueKey<ChildVicinity>(ChildVicinity(xIndex: 5, yIndex: 4)), + skipOffstage: false, + )).showOnScreen(); + await tester.pump(); + expect( + tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), + const Rect.fromLTRB(0.0, 200.0, 200.0, 400.0), + ); + }); + }); }); } diff --git a/packages/flutter/test/widgets/undo_history_test.dart b/packages/flutter/test/widgets/undo_history_test.dart index ce09b652e4d62..b5a80d09b4fed 100644 --- a/packages/flutter/test/widgets/undo_history_test.dart +++ b/packages/flutter/test/widgets/undo_history_test.dart @@ -6,10 +6,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'editable_text_utils.dart'; -final FocusNode focusNode = FocusNode(debugLabel: 'UndoHistory Node'); +final FocusNode _focusNode = FocusNode(debugLabel: 'UndoHistory Node'); void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -30,9 +31,12 @@ void main() { Future<void> sendUndo(WidgetTester tester) => sendUndoRedo(tester); Future<void> sendRedo(WidgetTester tester) => sendUndoRedo(tester, true); - testWidgets('allows undo and redo to be called programmatically from the UndoHistoryController', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows undo and redo to be called programmatically from the UndoHistoryController', (WidgetTester tester) async { final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -41,7 +45,7 @@ void main() { onTriggered: (int newValue) { value.value = newValue; }, - focusNode: focusNode, + focusNode: _focusNode, child: Container(), ), ), @@ -57,7 +61,7 @@ void main() { controller.redo(); expect(value.value, 0); - focusNode.requestFocus(); + _focusNode.requestFocus(); await tester.pump(); expect(controller.value.canUndo, false); expect(controller.value.canRedo, false); @@ -119,9 +123,12 @@ void main() { expect(controller.value.canRedo, false); }, variant: TargetPlatformVariant.all()); - testWidgets('allows undo and redo to be called using the keyboard', (WidgetTester tester) async { + testWidgetsWithLeakTracking('allows undo and redo to be called using the keyboard', (WidgetTester tester) async { final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -130,9 +137,9 @@ void main() { onTriggered: (int newValue) { value.value = newValue; }, - focusNode: focusNode, + focusNode: _focusNode, child: Focus( - focusNode: focusNode, + focusNode: _focusNode, child: Container(), ), ), @@ -149,7 +156,7 @@ void main() { await sendRedo(tester); expect(value.value, 0); - focusNode.requestFocus(); + _focusNode.requestFocus(); await tester.pump(); expect(controller.value.canUndo, false); expect(controller.value.canRedo, false); @@ -211,9 +218,12 @@ void main() { expect(controller.value.canRedo, false); }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] - testWidgets('duplicate changes do not affect the undo history', (WidgetTester tester) async { + testWidgetsWithLeakTracking('duplicate changes do not affect the undo history', (WidgetTester tester) async { final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -222,13 +232,13 @@ void main() { onTriggered: (int newValue) { value.value = newValue; }, - focusNode: focusNode, + focusNode: _focusNode, child: Container(), ), ), ); - focusNode.requestFocus(); + _focusNode.requestFocus(); // Wait for the throttling. await tester.pump(const Duration(milliseconds: 500)); @@ -261,11 +271,14 @@ void main() { expect(controller.value.canRedo, true); }, variant: TargetPlatformVariant.all()); - testWidgets('ignores value changes pushed during onTriggered', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ignores value changes pushed during onTriggered', (WidgetTester tester) async { final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); int Function(int newValue) valueToUse = (int value) => value; final GlobalKey<UndoHistoryState<int>> key = GlobalKey<UndoHistoryState<int>>(); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -275,7 +288,7 @@ void main() { onTriggered: (int newValue) { value.value = valueToUse(newValue); }, - focusNode: focusNode, + focusNode: _focusNode, child: Container(), ), ), @@ -291,7 +304,7 @@ void main() { controller.redo(); expect(value.value, 0); - focusNode.requestFocus(); + _focusNode.requestFocus(); await tester.pump(); expect(controller.value.canUndo, false); expect(controller.value.canRedo, false); @@ -309,16 +322,20 @@ void main() { expect(() => key.currentState!.undo(), throwsAssertionError); }, variant: TargetPlatformVariant.all()); - testWidgets('changes should send setUndoState to the UndoManagerConnection on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('changes should send setUndoState to the UndoManagerConnection on iOS', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.undoManager, (MethodCall methodCall) async { log.add(methodCall); return null; }); final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -378,9 +395,12 @@ void main() { expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': false, 'canRedo': true}); }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), skip: kIsWeb); // [intended] - testWidgets('handlePlatformUndo should undo or redo appropriately on iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('handlePlatformUndo should undo or redo appropriately on iOS', (WidgetTester tester) async { final ValueNotifier<int> value = ValueNotifier<int>(0); + addTearDown(value.dispose); final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); + await tester.pumpWidget( MaterialApp( home: UndoHistory<int>( @@ -389,9 +409,9 @@ void main() { onTriggered: (int newValue) { value.value = newValue; }, - focusNode: focusNode, + focusNode: _focusNode, child: Focus( - focusNode: focusNode, + focusNode: _focusNode, child: Container(), ), ), @@ -399,7 +419,7 @@ void main() { ); await tester.pump(const Duration(milliseconds: 500)); - focusNode.requestFocus(); + _focusNode.requestFocus(); await tester.pump(); // Undo/redo have no effect if the value has never changed. @@ -465,9 +485,10 @@ void main() { }); group('UndoHistoryController', () { - testWidgets('UndoHistoryController notifies onUndo listeners onUndo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UndoHistoryController notifies onUndo listeners onUndo', (WidgetTester tester) async { int calls = 0; final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); controller.onUndo.addListener(() { calls++; }); @@ -482,9 +503,10 @@ void main() { expect(calls, 1); }); - testWidgets('UndoHistoryController notifies onRedo listeners onRedo', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UndoHistoryController notifies onRedo listeners onRedo', (WidgetTester tester) async { int calls = 0; final UndoHistoryController controller = UndoHistoryController(); + addTearDown(controller.dispose); controller.onRedo.addListener(() { calls++; }); @@ -499,9 +521,10 @@ void main() { expect(calls, 1); }); - testWidgets('UndoHistoryController notifies listeners on value change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('UndoHistoryController notifies listeners on value change', (WidgetTester tester) async { int calls = 0; final UndoHistoryController controller = UndoHistoryController(value: const UndoHistoryValue(canUndo: true)); + addTearDown(controller.dispose); controller.addListener(() { calls++; }); diff --git a/packages/flutter/test/widgets/unique_widget_test.dart b/packages/flutter/test/widgets/unique_widget_test.dart index be060bc0ee1bb..8fa7b3f174dea 100644 --- a/packages/flutter/test/widgets/unique_widget_test.dart +++ b/packages/flutter/test/widgets/unique_widget_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestUniqueWidget extends UniqueWidget<TestUniqueWidgetState> { const TestUniqueWidget({ required super.key }); @@ -18,7 +19,7 @@ class TestUniqueWidgetState extends State<TestUniqueWidget> { } void main() { - testWidgets('Unique widget control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Unique widget control test', (WidgetTester tester) async { final TestUniqueWidget widget = TestUniqueWidget(key: GlobalKey<TestUniqueWidgetState>()); await tester.pumpWidget(widget); diff --git a/packages/flutter/test/widgets/value_listenable_builder_test.dart b/packages/flutter/test/widgets/value_listenable_builder_test.dart index 4384ed408efaa..4354f0636e0c7 100644 --- a/packages/flutter/test/widgets/value_listenable_builder_test.dart +++ b/packages/flutter/test/widgets/value_listenable_builder_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { late SpyStringValueNotifier valueListenable; @@ -32,21 +33,26 @@ void main() { textBuilderUnderTest = builderForValueListenable(valueListenable); }); - testWidgets('Null value is ok', (WidgetTester tester) async { + tearDown(() { + valueListenable.dispose(); + }); + + testWidgetsWithLeakTracking('Null value is ok', (WidgetTester tester) async { await tester.pumpWidget(textBuilderUnderTest); expect(find.byType(Placeholder), findsOneWidget); }); - testWidgets('Widget builds with initial value', (WidgetTester tester) async { - valueListenable = SpyStringValueNotifier('Bachman'); + testWidgetsWithLeakTracking('Widget builds with initial value', (WidgetTester tester) async { + final SpyStringValueNotifier valueListenable = SpyStringValueNotifier('Bachman'); + addTearDown(valueListenable.dispose); await tester.pumpWidget(builderForValueListenable(valueListenable)); expect(find.text('Bachman'), findsOneWidget); }); - testWidgets('Widget updates when value changes', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widget updates when value changes', (WidgetTester tester) async { await tester.pumpWidget(textBuilderUnderTest); valueListenable.value = 'Gilfoyle'; @@ -59,15 +65,15 @@ void main() { expect(find.text('Dinesh'), findsOneWidget); }); - testWidgets('Can change listenable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Can change listenable', (WidgetTester tester) async { await tester.pumpWidget(textBuilderUnderTest); valueListenable.value = 'Gilfoyle'; await tester.pump(); expect(find.text('Gilfoyle'), findsOneWidget); - final ValueListenable<String?> differentListenable = - SpyStringValueNotifier('Hendricks'); + final SpyStringValueNotifier differentListenable = SpyStringValueNotifier('Hendricks'); + addTearDown(differentListenable.dispose); await tester.pumpWidget(builderForValueListenable(differentListenable)); @@ -75,15 +81,15 @@ void main() { expect(find.text('Hendricks'), findsOneWidget); }); - testWidgets('Stops listening to old listenable after changing listenable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stops listening to old listenable after changing listenable', (WidgetTester tester) async { await tester.pumpWidget(textBuilderUnderTest); valueListenable.value = 'Gilfoyle'; await tester.pump(); expect(find.text('Gilfoyle'), findsOneWidget); - final ValueListenable<String?> differentListenable = - SpyStringValueNotifier('Hendricks'); + final SpyStringValueNotifier differentListenable = SpyStringValueNotifier('Hendricks'); + addTearDown(differentListenable.dispose); await tester.pumpWidget(builderForValueListenable(differentListenable)); @@ -98,7 +104,7 @@ void main() { expect(find.text('Hendricks'), findsOneWidget); }); - testWidgets('Self-cleans when removed', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Self-cleans when removed', (WidgetTester tester) async { await tester.pumpWidget(textBuilderUnderTest); valueListenable.value = 'Gilfoyle'; diff --git a/packages/flutter/test/widgets/view_test.dart b/packages/flutter/test/widgets/view_test.dart index 6da63c137dced..2fe695f0ae4c0 100644 --- a/packages/flutter/test/widgets/view_test.dart +++ b/packages/flutter/test/widgets/view_test.dart @@ -4,11 +4,13 @@ import 'dart:ui'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('Widgets running with runApp can find View', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widgets running with runApp can find View', (WidgetTester tester) async { FlutterView? viewOf; FlutterView? viewMaybeOf; @@ -28,7 +30,7 @@ void main() { expect(viewMaybeOf, isA<FlutterView>()); }); - testWidgets('Widgets running with pumpWidget can find View', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Widgets running with pumpWidget can find View', (WidgetTester tester) async { FlutterView? view; FlutterView? viewMaybeOf; @@ -48,7 +50,7 @@ void main() { expect(viewMaybeOf, isA<FlutterView>()); }); - testWidgets('cannot find View behind a LookupBoundary', (WidgetTester tester) async { + testWidgetsWithLeakTracking('cannot find View behind a LookupBoundary', (WidgetTester tester) async { await tester.pumpWidget( LookupBoundary( child: Container(), @@ -67,4 +69,436 @@ void main() { )), ); }); + + testWidgetsWithLeakTracking('child of view finds view, parentPipelineOwner, mediaQuery', (WidgetTester tester) async { + FlutterView? outsideView; + FlutterView? insideView; + PipelineOwner? outsideParent; + PipelineOwner? insideParent; + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: Builder( + builder: (BuildContext context) { + outsideView = View.maybeOf(context); + outsideParent = View.pipelineOwnerOf(context); + return View( + view: tester.view, + child: Builder( + builder: (BuildContext context) { + insideView = View.maybeOf(context); + insideParent = View.pipelineOwnerOf(context); + return const SizedBox(); + }, + ), + ); + }, + ), + ); + expect(outsideView, isNull); + expect(insideView, equals(tester.view)); + + expect(outsideParent, isNotNull); + expect(insideParent, isNotNull); + expect(outsideParent, isNot(equals(insideParent))); + + expect(outsideParent, tester.binding.rootPipelineOwner); + expect(insideParent, equals(tester.renderObject(find.byType(SizedBox)).owner)); + + final List<PipelineOwner> pipelineOwners = <PipelineOwner> []; + tester.binding.rootPipelineOwner.visitChildren((PipelineOwner child) { + pipelineOwners.add(child); + }); + expect(pipelineOwners.single, equals(insideParent)); + }); + + testWidgetsWithLeakTracking('cannot have multiple views with same FlutterView', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: tester.view, + child: const SizedBox(), + ), + View( + view: tester.view, + child: const SizedBox(), + ), + ], + ), + ); + + expect( + tester.takeException(), + isFlutterError.having( + (FlutterError e) => e.message, + 'message', + contains('Multiple widgets used the same GlobalKey'), + ), + ); + }); + + testWidgetsWithLeakTracking('ViewCollection must have one view', (WidgetTester tester) async { + expect(() => ViewCollection(views: const <Widget>[]), throwsAssertionError); + }); + + testWidgetsWithLeakTracking('ViewAnchor.child does not see surrounding view', (WidgetTester tester) async { + FlutterView? inside; + FlutterView? outside; + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outside = View.maybeOf(context); + return ViewAnchor( + view: Builder( + builder: (BuildContext context) { + inside = View.maybeOf(context); + return View(view: FakeView(tester.view), child: const SizedBox()); + }, + ), + child: const SizedBox(), + ); + }, + ), + ); + expect(inside, isNull); + expect(outside, isNotNull); + }); + + testWidgetsWithLeakTracking('ViewAnchor layout order', (WidgetTester tester) async { + Finder findSpyWidget(int label) { + return find.byWidgetPredicate((Widget w) => w is SpyRenderWidget && w.label == label); + } + + final List<String> log = <String>[]; + await tester.pumpWidget( + SpyRenderWidget( + label: 1, + log: log, + child: ViewAnchor( + view: View( + view: FakeView(tester.view), + child: SpyRenderWidget(label: 2, log: log), + ), + child: SpyRenderWidget(label: 3, log: log), + ), + ), + ); + log.clear(); + tester.renderObject(findSpyWidget(3)).markNeedsLayout(); + tester.renderObject(findSpyWidget(2)).markNeedsLayout(); + tester.renderObject(findSpyWidget(1)).markNeedsLayout(); + await tester.pump(); + expect(log, <String>['layout 1', 'layout 3', 'layout 2']); + }); + + testWidgetsWithLeakTracking('visitChildren of ViewAnchor visits both children', (WidgetTester tester) async { + await tester.pumpWidget( + ViewAnchor( + view: View( + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.green), + ), + child: const SizedBox(), + ), + ); + final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement')); + final List<Element> children = <Element>[]; + viewAnchorElement.visitChildren((Element element) { + children.add(element); + }); + expect(children, hasLength(2)); + + await tester.pumpWidget( + const ViewAnchor( + child: SizedBox(), + ), + ); + children.clear(); + viewAnchorElement.visitChildren((Element element) { + children.add(element); + }); + expect(children, hasLength(1)); + }); + + testWidgetsWithLeakTracking('visitChildren of ViewCollection visits all children', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: tester.view, + child: const SizedBox(), + ), + View( + view: FakeView(tester.view), + child: const SizedBox(), + ), + View( + view: FakeView(tester.view, viewId: 423), + child: const SizedBox(), + ), + ], + ), + ); + final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement')); + final List<Element> children = <Element>[]; + viewAnchorElement.visitChildren((Element element) { + children.add(element); + }); + expect(children, hasLength(3)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: ViewCollection( + views: <Widget>[ + View( + view: tester.view, + child: const SizedBox(), + ), + ], + ), + ); + children.clear(); + viewAnchorElement.visitChildren((Element element) { + children.add(element); + }); + expect(children, hasLength(1)); + }); + + group('renderObject getter', () { + testWidgetsWithLeakTracking('ancestors of view see RenderView as renderObject', (WidgetTester tester) async { + late BuildContext builderContext; + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: Builder( + builder: (BuildContext context) { + builderContext = context; + return View( + view: tester.view, + child: const SizedBox(), + ); + }, + ), + ); + + final RenderObject? renderObject = builderContext.findRenderObject(); + expect(renderObject, isNotNull); + expect(renderObject, isA<RenderView>()); + expect(renderObject, tester.renderObject(find.byType(View))); + expect(tester.element(find.byType(Builder)).renderObject, renderObject); + }); + + testWidgetsWithLeakTracking('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async { + late BuildContext builderContext; + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: Builder( + builder: (BuildContext context) { + builderContext = context; + return ViewCollection( + views: <Widget>[ + View( + view: tester.view, + child: const SizedBox(), + ), + View( + view: FakeView(tester.view), + child: const SizedBox(), + ), + ], + ); + }, + ), + ); + + final RenderObject? renderObject = builderContext.findRenderObject(); + expect(renderObject, isNull); + expect(tester.element(find.byType(Builder)).renderObject, isNull); + }); + + testWidgetsWithLeakTracking('ancestors of a ViewAnchor see the right RenderObject', (WidgetTester tester) async { + late BuildContext builderContext; + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + builderContext = context; + return ViewAnchor( + view: View( + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.green), + ), + child: const SizedBox(), + ); + }, + ), + ); + + final RenderObject? renderObject = builderContext.findRenderObject(); + expect(renderObject, isNotNull); + expect(renderObject, isA<RenderConstrainedBox>()); + expect(renderObject, tester.renderObject(find.byType(SizedBox))); + expect(tester.element(find.byType(Builder)).renderObject, renderObject); + }); + }); + + testWidgetsWithLeakTracking('correctly switches between view configurations', (WidgetTester tester) async { + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, + deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, + child: const SizedBox(), + ), + ); + RenderObject renderView = tester.renderObject(find.byType(View)); + expect(renderView, same(tester.binding.renderView)); + expect(renderView.owner, same(tester.binding.pipelineOwner)); + expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner)); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + child: const SizedBox(), + ), + ); + renderView = tester.renderObject(find.byType(View)); + expect(renderView, isNot(same(tester.binding.renderView))); + expect(renderView.owner, isNot(same(tester.binding.pipelineOwner))); + expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner))); + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: tester.view, + deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, + deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, + child: const SizedBox(), + ), + ); + renderView = tester.renderObject(find.byType(View)); + expect(renderView, same(tester.binding.renderView)); + expect(renderView.owner, same(tester.binding.pipelineOwner)); + expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner)); + + expect(() => View( + view: tester.view, + deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, + child: const SizedBox(), + ), throwsAssertionError); + expect(() => View( + view: tester.view, + deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, + child: const SizedBox(), + ), throwsAssertionError); + expect(() => View( + view: FakeView(tester.view), + deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, + deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, + child: const SizedBox(), + ), throwsAssertionError); + }); + + testWidgetsWithLeakTracking('attaches itself correctly', (WidgetTester tester) async { + final Key viewKey = UniqueKey(); + late final PipelineOwner parentPipelineOwner; + await tester.pumpWidget( + ViewAnchor( + view: Builder( + builder: (BuildContext context) { + parentPipelineOwner = View.pipelineOwnerOf(context); + return View( + key: viewKey, + view: FakeView(tester.view), + child: const SizedBox(), + ); + }, + ), + child: const ColoredBox(color: Colors.green), + ), + ); + + expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner)); + + final RenderView rawView = tester.renderObject<RenderView>(find.byKey(viewKey)); + expect(RendererBinding.instance.renderViews, contains(rawView)); + + final List<PipelineOwner> children = <PipelineOwner>[]; + parentPipelineOwner.visitChildren((PipelineOwner child) { + children.add(child); + }); + final PipelineOwner rawViewOwner = rawView.owner!; + expect(children, contains(rawViewOwner)); + + // Remove that View from the tree. + await tester.pumpWidget( + const ViewAnchor( + child: ColoredBox(color: Colors.green), + ), + ); + + expect(rawView.owner, isNull); + expect(RendererBinding.instance.renderViews, isNot(contains(rawView))); + children.clear(); + parentPipelineOwner.visitChildren((PipelineOwner child) { + children.add(child); + }); + expect(children, isNot(contains(rawViewOwner))); + }); +} + +Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { + tester.binding.attachRootWidget(widget); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} + +class FakeView extends TestFlutterView{ + FakeView(FlutterView view, { this.viewId = 100 }) : super( + view: view, + platformDispatcher: view.platformDispatcher as TestPlatformDispatcher, + display: view.display as TestDisplay, + ); + + @override + final int viewId; +} + +class SpyRenderWidget extends SizedBox { + const SpyRenderWidget({super.key, required this.label, required this.log, super.child}); + + final int label; + final List<String> log; + + @override + RenderSpy createRenderObject(BuildContext context) { + return RenderSpy( + additionalConstraints: const BoxConstraints(), + label: label, + log: log, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderSpy renderObject) { + renderObject + ..label = label + ..log = log; + } +} + +class RenderSpy extends RenderConstrainedBox { + RenderSpy({required super.additionalConstraints, required this.label, required this.log}); + + int label; + List<String> log; + + @override + void performLayout() { + log.add('layout $label'); + super.performLayout(); + } } diff --git a/packages/flutter/test/widgets/visibility_test.dart b/packages/flutter/test/widgets/visibility_test.dart index 7f433146fbb47..0b2b6330f30b0 100644 --- a/packages/flutter/test/widgets/visibility_test.dart +++ b/packages/flutter/test/widgets/visibility_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; class TestState extends StatefulWidget { @@ -30,7 +30,7 @@ class _TestStateState extends State<TestState> { } void main() { - testWidgets('Visibility', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Visibility', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List<String> log = <String>[]; @@ -440,7 +440,7 @@ void main() { semantics.dispose(); }); - testWidgets('Visibility does not force compositing when visible and maintain*', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Visibility does not force compositing when visible and maintain*', (WidgetTester tester) async { await tester.pumpWidget( const Visibility( maintainSize: true, @@ -456,7 +456,7 @@ void main() { expect(tester.layers.last, isA<PictureLayer>()); }); - testWidgets('SliverVisibility does not force compositing when visible and maintain*', (WidgetTester tester) async { + testWidgetsWithLeakTracking('SliverVisibility does not force compositing when visible and maintain*', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -486,7 +486,7 @@ void main() { expect(tester.layers.last, isA<PictureLayer>()); }); - testWidgets('Visibility.of returns correct value', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Visibility.of returns correct value', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, @@ -519,7 +519,7 @@ void main() { expect(find.text('is visible ? false', skipOffstage: false), findsOneWidget); }); - testWidgets('Visibility.of works when multiple Visibility widgets are in hierarchy', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Visibility.of works when multiple Visibility widgets are in hierarchy', (WidgetTester tester) async { bool didChangeDependencies = false; void handleDidChangeDependencies() { didChangeDependencies = true; diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 743b4da59ea8f..29d6b33b36635 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -19,6 +19,7 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker/leak_tracker.dart'; import 'widget_inspector_test_utils.dart'; @@ -283,7 +284,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { static void runTests() { final TestWidgetInspectorService service = TestWidgetInspectorService(); WidgetInspectorService.instance = service; - + setUp(() { + WidgetInspectorService.instance.isSelectMode.value = true; + }); tearDown(() async { service.resetAllState(); @@ -299,6 +302,21 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(WidgetInspectorService.objectToDiagnosticsNode(Alignment.bottomCenter), isNull); }); + test('WidgetInspector does not hold objects from GC', () async { + List<DateTime>? someObject = <DateTime>[DateTime.now(), DateTime.now()]; + final String? id = service.toId(someObject, 'group_name'); + + expect(id, isNotNull); + + final WeakReference<Object> ref = WeakReference<Object>(someObject); + someObject = null; + + // 1 should be enough for [fullGcCycles], but it is 3 to make sure tests are not flaky. + await forceGC(fullGcCycles: 3); + + expect(ref.target, null); + }); + testWidgets('WidgetInspector smoke test', (WidgetTester tester) async { // This is a smoke test to verify that adding the inspector doesn't crash. await tester.pumpWidget( @@ -342,8 +360,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) { return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null)); } - // State type is private, hence using dynamic. - dynamic getInspectorState() => inspectorKey.currentState; + String paragraphText(RenderParagraph paragraph) { final TextSpan textSpan = paragraph.text as TextSpan; return textSpan.text!; @@ -378,16 +395,23 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ), ); - expect(getInspectorState().selection.current, isNull); // ignore: avoid_dynamic_calls + expect(WidgetInspectorService.instance.selection.current, isNull); await tester.tap(find.text('TOP'), warnIfMissed: false); await tester.pump(); // Tap intercepted by the inspector expect(log, equals(<String>[])); // ignore: avoid_dynamic_calls - final InspectorSelection selection = getInspectorState().selection as InspectorSelection; - expect(paragraphText(selection.current! as RenderParagraph), equals('TOP')); + expect( + paragraphText( + WidgetInspectorService.instance.selection.current! as RenderParagraph, + ), + equals('TOP'), + ); final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject!; - expect(selection.candidates, contains(topButton)); + expect( + WidgetInspectorService.instance.selection.candidates, + contains(topButton), + ); await tester.tap(find.text('TOP')); expect(log, equals(<String>['top'])); @@ -398,7 +422,12 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { log.clear(); // Ensure the inspector selection has not changed to bottom. // ignore: avoid_dynamic_calls - expect(paragraphText(getInspectorState().selection.current as RenderParagraph), equals('TOP')); + expect( + paragraphText( + WidgetInspectorService.instance.selection.current! as RenderParagraph, + ), + equals('TOP'), + ); await tester.tap(find.byKey(selectButtonKey)); await tester.pump(); @@ -409,7 +438,12 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(log, equals(<String>[])); log.clear(); // ignore: avoid_dynamic_calls - expect(paragraphText(getInspectorState().selection.current as RenderParagraph), equals('BOTTOM')); + expect( + paragraphText( + WidgetInspectorService.instance.selection.current! as RenderParagraph, + ), + equals('BOTTOM'), + ); }); testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async { @@ -445,8 +479,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) { return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null)); } - // State type is private, hence using dynamic. - dynamic getInspectorState() => inspectorKey.currentState; await tester.pumpWidget( Directionality( @@ -482,7 +514,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { await tester.tap(find.byType(ListView), warnIfMissed: false); await tester.pump(); - expect(getInspectorState().selection.current, isNotNull); // ignore: avoid_dynamic_calls + expect(WidgetInspectorService.instance.selection.current, isNotNull); // Now out of inspect mode due to the click. await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0); @@ -566,17 +598,23 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ); await tester.longPress(find.byKey(clickTarget), warnIfMissed: false); - // State type is private, hence using dynamic. - final dynamic inspectorState = inspectorKey.currentState; // The object with width 95.0 wins over the object with width 94.0 because // the subtree with width 94.0 is offstage. // ignore: avoid_dynamic_calls - expect(inspectorState.selection.current.semanticBounds.width, equals(95.0)); + expect( + WidgetInspectorService.instance.selection.current?.semanticBounds.width, + equals(95.0), + ); // Exactly 2 out of the 3 text elements should be in the candidate list of // objects to select as only 2 are onstage. // ignore: avoid_dynamic_calls - expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2)); + expect( + WidgetInspectorService.instance.selection.candidates + .whereType<RenderParagraph>() + .length, + equals(2), + ); }); testWidgets('WidgetInspector with Transform above', (WidgetTester tester) async { @@ -645,9 +683,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { }; } - // State type is private, hence using dynamic. - // The inspector state is static, so it's enough with reading one of them. - dynamic getInspectorState() => inspector1Key.currentState; String paragraphText(RenderParagraph paragraph) { final TextSpan textSpan = paragraph.text as TextSpan; return textSpan.text!; @@ -683,18 +718,27 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ), ); - // ignore: avoid_dynamic_calls - final InspectorSelection selection = getInspectorState().selection as InspectorSelection; - // The selection is static, so it may be initialized from previous tests. - selection.clear(); - await tester.tap(find.text('Child 1'), warnIfMissed: false); await tester.pump(); - expect(paragraphText(selection.current! as RenderParagraph), equals('Child 1')); + expect( + paragraphText( + WidgetInspectorService.instance.selection.current! as RenderParagraph, + ), + equals('Child 1'), + ); + + // Re-enable select mode since it's state is shared between the + // WidgetInspectors + WidgetInspectorService.instance.isSelectMode.value = true; await tester.tap(find.text('Child 2'), warnIfMissed: false); await tester.pump(); - expect(paragraphText(selection.current! as RenderParagraph), equals('Child 2')); + expect( + paragraphText( + WidgetInspectorService.instance.selection.current! as RenderParagraph, + ), + equals('Child 2'), + ); }); test('WidgetInspectorService null id', () { @@ -1985,6 +2029,52 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. ); + group('InspectorSelection', () { + testWidgets('receives notifications when selection changes', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: <Widget>[ + Text('a'), + Text('b'), + ], + ), + ), + ); + final InspectorSelection selection = InspectorSelection(); + int count = 0; + selection.addListener(() { + count++; + }); + final RenderParagraph renderObjectA = + tester.renderObject<RenderParagraph>(find.text('a')); + final RenderParagraph renderObjectB = + tester.renderObject<RenderParagraph>(find.text('b')); + final Element elementA = find.text('a').evaluate().first; + + selection.candidates = <RenderObject>[renderObjectA, renderObjectB]; + await tester.pump(); + expect(count, equals(1)); + + selection.index = 1; + await tester.pump(); + expect(count, equals(2)); + + selection.clear(); + await tester.pump(); + expect(count, equals(3)); + + selection.current = renderObjectA; + await tester.pump(); + expect(count, equals(4)); + + selection.currentElement = elementA; + expect(count, equals(5)); + }); + }); + test('ext.flutter.inspector.disposeGroup', () async { final Object a = Object(); const String group1 = 'group-1'; @@ -3775,7 +3865,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { _CreationLocation location = knownLocations[id]!; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(55)); + expect(location.line, equals(56)); expect(location.column, equals(9)); expect(location.name, equals('ClockText')); expect(count, equals(1)); @@ -3785,7 +3875,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(93)); + expect(location.line, equals(94)); expect(location.column, equals(12)); expect(location.name, equals('Text')); expect(count, equals(1)); @@ -3812,7 +3902,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // ClockText widget. - expect(location.line, equals(55)); + expect(location.line, equals(56)); expect(location.column, equals(9)); expect(location.name, equals('ClockText')); expect(count, equals(3)); // 3 clock widget instances rebuilt. @@ -3822,7 +3912,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { location = knownLocations[id]!; expect(location.file, equals(file)); // Text widget in _ClockTextState build method. - expect(location.line, equals(93)); + expect(location.line, equals(94)); expect(location.column, equals(12)); expect(location.name, equals('Text')); expect(count, equals(3)); // 3 clock widget instances rebuilt. @@ -4493,7 +4583,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(result['parentData'], isNull); }); - testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async { await pumpWidgetForLayoutExplorer(tester); @@ -4514,7 +4603,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?; expect(renderObject, isNotNull); - expect(renderObject!['description'], startsWith('RenderView')); + expect(renderObject!['description'], contains('RenderView')); expect(result['parentRenderElement'], isNull); expect(result['constraints'], isNull); @@ -4682,6 +4771,53 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { } expect(error, isNull); }); + + testWidgets( + 'ext.flutter.inspector.getLayoutExplorerNode, on a ToolTip, does not throw StackOverflowError', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/devtools/issues/5946 + const Widget widget = MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Row( + children: <Widget>[ + Flexible( + child: ColoredBox( + color: Colors.green, + child: Tooltip( + message: 'a', + child: ElevatedButton( + onPressed: null, + child: Text('a'), + ), + ), + ), + ), + ], + ), + ), + ), + ); + await tester.pumpWidget(widget); + + final Element elevatedButton = + tester.element(find.byType(ElevatedButton).first); + service.setSelection(elevatedButton, group); + + final String id = service.toId(elevatedButton, group)!; + + Object? error; + try { + await service.testExtension( + WidgetInspectorServiceExtensions.getLayoutExplorerNode.name, + <String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'}, + ); + } catch (e) { + error = e; + } + expect(error, isNull); + }); }); test('ext.flutter.inspector.structuredErrors', () async { diff --git a/packages/flutter/test/widgets/widget_inspector_test_utils.dart b/packages/flutter/test/widgets/widget_inspector_test_utils.dart index f7b368663898f..19370bfa778b9 100644 --- a/packages/flutter/test/widgets/widget_inspector_test_utils.dart +++ b/packages/flutter/test/widgets/widget_inspector_test_utils.dart @@ -37,6 +37,13 @@ class DispatchedEventKey { } class TestWidgetInspectorService extends Object with WidgetInspectorService { + TestWidgetInspectorService() { + selection.addListener(() { + if (selectionChangedCallback != null) { + selectionChangedCallback!(); + } + }); + } final Map<String, ServiceExtensionCallback> extensions = <String, ServiceExtensionCallback>{}; final Map<DispatchedEventKey, List<Map<Object, Object?>>> eventsDispatched = @@ -47,6 +54,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { void registerServiceExtension({ required String name, required ServiceExtensionCallback callback, + required RegisterServiceExtensionCallback registerExtension, }) { assert(!extensions.containsKey(name)); extensions[name] = callback; @@ -107,7 +115,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { final WidgetsBinding binding = WidgetsBinding.instance; if (binding.rootElement != null) { - binding.buildOwner!.reassemble(binding.rootElement!, null); + binding.buildOwner!.reassemble(binding.rootElement!); } } diff --git a/packages/flutter/test/widgets/widget_span_test.dart b/packages/flutter/test/widgets/widget_span_test.dart new file mode 100644 index 0000000000000..d903ab567e972 --- /dev/null +++ b/packages/flutter/test/widgets/widget_span_test.dart @@ -0,0 +1,77 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('WidgetSpan codeUnitAt', () { + const InlineSpan span = WidgetSpan(child: SizedBox()); + expect(span.codeUnitAt(-1), isNull); + expect(span.codeUnitAt(0), PlaceholderSpan.placeholderCodeUnit); + expect(span.codeUnitAt(1), isNull); + expect(span.codeUnitAt(2), isNull); + + const InlineSpan nestedSpan = TextSpan( + text: 'AAA', + children: <InlineSpan>[span, span], + ); + expect(nestedSpan.codeUnitAt(-1), isNull); + expect(nestedSpan.codeUnitAt(0), 65); + expect(nestedSpan.codeUnitAt(1), 65); + expect(nestedSpan.codeUnitAt(2), 65); + expect(nestedSpan.codeUnitAt(3), PlaceholderSpan.placeholderCodeUnit); + expect(nestedSpan.codeUnitAt(4), PlaceholderSpan.placeholderCodeUnit); + expect(nestedSpan.codeUnitAt(5), isNull); + }); + + test('WidgetSpan.extractFromInlineSpan applies the correct scaling factor', () { + const WidgetSpan a = WidgetSpan(child: SizedBox(), style: TextStyle(fontSize: 0)); + const WidgetSpan b = WidgetSpan(child: SizedBox(), style: TextStyle(fontSize: 10)); + const WidgetSpan c = WidgetSpan(child: SizedBox()); + const WidgetSpan d = WidgetSpan(child: SizedBox(), style: TextStyle(letterSpacing: 999)); + + const TextSpan span = TextSpan( + children: <InlineSpan>[ + a, // fontSize = 0. + TextSpan( + children: <InlineSpan>[ + b, // fontSize = 10. + c, // fontSize = 20. + ], + style: TextStyle(fontSize: 20), + ), + d, // fontSize = 14. + ] + ); + + double effectiveTextScaleFactorFromWidget(Widget widget) { + final Semantics child = (widget as ProxyWidget).child as Semantics; + final dynamic grandChild = child.child; + final double textScaleFactor = grandChild.textScaleFactor as double; // ignore: avoid_dynamic_calls + return textScaleFactor; + } + + final List<double> textScaleFactors = WidgetSpan.extractFromInlineSpan(span, const _QuadraticScaler()) + .map(effectiveTextScaleFactorFromWidget).toList(); + + expect(textScaleFactors, <double>[ + 0, // a + 10, // b + 20, // c + 14, // d + ]); + }); +} + + +class _QuadraticScaler extends TextScaler { + const _QuadraticScaler(); + + @override + double scale(double fontSize) => fontSize * fontSize; + + @override + double get textScaleFactor => throw UnimplementedError(); +} diff --git a/packages/flutter/test/widgets/wrap_test.dart b/packages/flutter/test/widgets/wrap_test.dart index 70cd91dd139fd..41165288f9bad 100644 --- a/packages/flutter/test/widgets/wrap_test.dart +++ b/packages/flutter/test/widgets/wrap_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../rendering/mock_canvas.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void verify(WidgetTester tester, List<Offset> answerKey) { final List<Offset> testAnswers = tester.renderObjectList<RenderBox>(find.byType(SizedBox)).map<Offset>( @@ -16,7 +15,7 @@ void verify(WidgetTester tester, List<Offset> answerKey) { } void main() { - testWidgets('Basic Wrap test (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Basic Wrap test (LTR)', (WidgetTester tester) async { await tester.pumpWidget( const Wrap( textDirection: TextDirection.ltr, @@ -131,7 +130,7 @@ void main() { }); - testWidgets('Basic Wrap test (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Basic Wrap test (RTL)', (WidgetTester tester) async { await tester.pumpWidget( const Wrap( textDirection: TextDirection.rtl, @@ -249,12 +248,12 @@ void main() { }); - testWidgets('Empty wrap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Empty wrap', (WidgetTester tester) async { await tester.pumpWidget(const Center(child: Wrap(alignment: WrapAlignment.center))); expect(tester.renderObject<RenderBox>(find.byType(Wrap)).size, equals(Size.zero)); }); - testWidgets('Wrap alignment (LTR)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap alignment (LTR)', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( alignment: WrapAlignment.center, spacing: 5.0, @@ -324,7 +323,7 @@ void main() { ]); }); - testWidgets('Wrap alignment (RTL)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap alignment (RTL)', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( alignment: WrapAlignment.center, spacing: 5.0, @@ -394,7 +393,7 @@ void main() { ]); }); - testWidgets('Wrap runAlignment (DOWN)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap runAlignment (DOWN)', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( runAlignment: WrapAlignment.center, runSpacing: 5.0, @@ -481,7 +480,7 @@ void main() { }); - testWidgets('Wrap runAlignment (UP)', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap runAlignment (UP)', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( runAlignment: WrapAlignment.center, runSpacing: 5.0, @@ -572,7 +571,7 @@ void main() { }); - testWidgets('Shrink-wrapping Wrap test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Shrink-wrapping Wrap test', (WidgetTester tester) async { await tester.pumpWidget( const Align( alignment: Alignment.topLeft, @@ -622,7 +621,7 @@ void main() { ]); }); - testWidgets('Wrap spacing test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap spacing test', (WidgetTester tester) async { await tester.pumpWidget( const Align( alignment: Alignment.topLeft, @@ -647,7 +646,7 @@ void main() { ]); }); - testWidgets('Vertical Wrap test with spacing', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical Wrap test with spacing', (WidgetTester tester) async { await tester.pumpWidget( const Align( alignment: Alignment.topLeft, @@ -706,7 +705,7 @@ void main() { ]); }); - testWidgets('Visual overflow generates a clip', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Visual overflow generates a clip', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( textDirection: TextDirection.ltr, children: <Widget>[ @@ -728,7 +727,7 @@ void main() { expect(tester.renderObject<RenderBox>(find.byType(Wrap)), paints..clipRect()); }); - testWidgets('Hit test children in wrap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Hit test children in wrap', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Wrap( @@ -764,14 +763,14 @@ void main() { expect(log, equals(<String>['hit'])); }); - testWidgets('RenderWrap toStringShallow control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderWrap toStringShallow control test', (WidgetTester tester) async { await tester.pumpWidget(const Wrap(alignment: WrapAlignment.center)); final RenderBox wrap = tester.renderObject(find.byType(Wrap)); expect(wrap.toStringShallow(), hasOneLineDescription); }); - testWidgets('RenderWrap toString control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('RenderWrap toString control test', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( direction: Axis.vertical, runSpacing: 7.0, @@ -789,7 +788,7 @@ void main() { expect(width, equals(2021)); }); - testWidgets('Wrap baseline control test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap baseline control test', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: Baseline( @@ -817,7 +816,7 @@ void main() { ); }); - testWidgets('Spacing with slight overflow', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Spacing with slight overflow', (WidgetTester tester) async { await tester.pumpWidget(const Wrap( textDirection: TextDirection.ltr, spacing: 10.0, @@ -839,7 +838,7 @@ void main() { ]); }); - testWidgets('Object exactly matches container width', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Object exactly matches container width', (WidgetTester tester) async { await tester.pumpWidget( const Column( children: <Widget>[ @@ -881,7 +880,7 @@ void main() { ]); }); - testWidgets('Wrap can set and update clipBehavior', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Wrap can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget(const Wrap(textDirection: TextDirection.ltr)); final RenderWrap renderObject = tester.allRenderObjects.whereType<RenderWrap>().first; expect(renderObject.clipBehavior, equals(Clip.none)); @@ -890,7 +889,7 @@ void main() { expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); - testWidgets('Horizontal wrap - IntrinsicsHeight', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Horizontal wrap - IntrinsicsHeight', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/48679. await tester.pumpWidget( const Directionality( @@ -922,7 +921,7 @@ void main() { expect(tester.getSize(find.byType(IntrinsicHeight)).height, 2 * 16 + 40); }); - testWidgets('Vertical wrap - IntrinsicsWidth', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Vertical wrap - IntrinsicsWidth', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/48679. await tester.pumpWidget( const Directionality( diff --git a/packages/flutter/test_fixes/material/material.dart b/packages/flutter/test_fixes/material/material.dart index c00c6f91c8db7..7fa4f903e5d93 100644 --- a/packages/flutter/test_fixes/material/material.dart +++ b/packages/flutter/test_fixes/material/material.dart @@ -317,4 +317,10 @@ void main() { clipBehavior: Clip.none, ); final Clip clip = details.clipBehavior; + + // Changes made in https://github.com/flutter/flutter/pull/129942 + // TODO(guidezpl): enable fix after https://github.com/dart-lang/sdk/issues/52902 + // const Curve curve = standardEasing; expect Easing.legacy + // const Curve curve = accelerateEasing; expect Easing.legacyAccelerate + // const Curve curve = decelerateEasing; expect Easing.legacyDecelerate } diff --git a/packages/flutter/test_fixes/material/material.dart.expect b/packages/flutter/test_fixes/material/material.dart.expect index 48c65bd6175e9..f6515961459f9 100644 --- a/packages/flutter/test_fixes/material/material.dart.expect +++ b/packages/flutter/test_fixes/material/material.dart.expect @@ -313,4 +313,10 @@ void main() { decorationClipBehavior: Clip.none, ); final Clip clip = details.decorationClipBehavior; + + // Changes made in https://github.com/flutter/flutter/pull/129942 + // TODO(guidezpl): enable fix after https://github.com/dart-lang/sdk/issues/52902 + // const Curve curve = standardEasing; expect Easing.legacy + // const Curve curve = accelerateEasing; expect Easing.legacyAccelerate + // const Curve curve = decelerateEasing; expect Easing.legacyDecelerate } diff --git a/packages/flutter/test_fixes/material/theme_data.dart b/packages/flutter/test_fixes/material/theme_data.dart index 7c4d4e50f85e0..c9c5d929cb5bc 100644 --- a/packages/flutter/test_fixes/material/theme_data.dart +++ b/packages/flutter/test_fixes/material/theme_data.dart @@ -234,4 +234,7 @@ void main() { themeData = ThemeData(bottomAppBarColor: Colors.green); themeData = ThemeData.raw(bottomAppBarColor: Colors.green); themeData = ThemeData.copyWith(bottomAppBarColor: Colors.green); + + // Changes made in https://github.com/flutter/flutter/pull/131455 + ThemeData themeData = ThemeData.copyWith(useMaterial3: false); } diff --git a/packages/flutter/test_fixes/material/theme_data.dart.expect b/packages/flutter/test_fixes/material/theme_data.dart.expect index 0992a721fa38c..9430d934e38f6 100644 --- a/packages/flutter/test_fixes/material/theme_data.dart.expect +++ b/packages/flutter/test_fixes/material/theme_data.dart.expect @@ -440,4 +440,7 @@ void main() { themeData = ThemeData(bottomAppBarTheme: BottomAppBarTheme(color: Colors.green)); themeData = ThemeData.raw(bottomAppBarTheme: BottomAppBarTheme(color: Colors.green)); themeData = ThemeData.copyWith(bottomAppBarTheme: BottomAppBarTheme(color: Colors.green)); + + // Changes made in https://github.com/flutter/flutter/pull/131455 + ThemeData themeData = ThemeData.copyWith(); } diff --git a/packages/flutter/test_fixes/painting/painting.dart b/packages/flutter/test_fixes/painting/painting.dart index 625a0e701d179..2f2669f3557bf 100644 --- a/packages/flutter/test_fixes/painting/painting.dart +++ b/packages/flutter/test_fixes/painting/painting.dart @@ -7,4 +7,15 @@ import 'package:flutter/painting.dart'; void main() { // Change made in https://github.com/flutter/flutter/pull/121152 final EdgeInsets insets = EdgeInsets.fromWindowPadding(ViewPadding.zero, 3.0); + + // Change made in https://github.com/flutter/flutter/pull/128522 + const TextStyle textStyle = TextStyle() + ..getTextStyle(textScaleFactor: math.min(_kTextScaleFactor, 1.0)) + ..getTextStyle(); + + TextPainter(text: inlineSpan); + TextPainter(textScaleFactor: someValue); + + TextPainter.computeWidth(textScaleFactor: textScaleFactor); + TextPainter.computeMaxIntrinsicWidth(textScaleFactor: textScaleFactor); } diff --git a/packages/flutter/test_fixes/painting/painting.dart.expect b/packages/flutter/test_fixes/painting/painting.dart.expect index 245193c8538d2..249f3dac39330 100644 --- a/packages/flutter/test_fixes/painting/painting.dart.expect +++ b/packages/flutter/test_fixes/painting/painting.dart.expect @@ -7,4 +7,15 @@ import 'package:flutter/painting.dart'; void main() { // Change made in https://github.com/flutter/flutter/pull/121152 final EdgeInsets insets = EdgeInsets.fromViewPadding(ViewPadding.zero, 3.0); + + // Change made in https://github.com/flutter/flutter/pull/128522 + const TextStyle textStyle = TextStyle() + ..getTextStyle(textScaler: TextScaler.linear(math.min(_kTextScaleFactor, 1.0))) + ..getTextStyle(); + + TextPainter(text: inlineSpan); + TextPainter(textScaler: TextScaler.linear(someValue)); + + TextPainter.computeWidth(textScaler: TextScaler.linear(textScaleFactor)); + TextPainter.computeMaxIntrinsicWidth(textScaler: TextScaler.linear(textScaleFactor)); } diff --git a/packages/flutter/test_fixes/rendering/rendering.dart b/packages/flutter/test_fixes/rendering/rendering.dart index f8bfd0d1a5296..705085486fa68 100644 --- a/packages/flutter/test_fixes/rendering/rendering.dart +++ b/packages/flutter/test_fixes/rendering/rendering.dart @@ -18,4 +18,10 @@ void main() { renderListWheelViewport = RenderListWheelViewport(clipToSize: false); renderListWheelViewport = RenderListWheelViewport(error: ''); renderListWheelViewport.clipToSize; + + // Change made in https://github.com/flutter/flutter/pull/128522 + RenderParagraph(textScaleFactor: math.min(123, 456)); + RenderParagraph(); + RenderEditable(textScaleFactor: math.min(123, 456)); + RenderEditable(); } diff --git a/packages/flutter/test_fixes/rendering/rendering.dart.expect b/packages/flutter/test_fixes/rendering/rendering.dart.expect index 850b37fdf39e5..e660069f3fa5d 100644 --- a/packages/flutter/test_fixes/rendering/rendering.dart.expect +++ b/packages/flutter/test_fixes/rendering/rendering.dart.expect @@ -18,4 +18,10 @@ void main() { renderListWheelViewport = RenderListWheelViewport(clipBehavior: Clip.none); renderListWheelViewport = RenderListWheelViewport(error: ''); renderListWheelViewport.clipBehavior; + + // Change made in https://github.com/flutter/flutter/pull/128522 + RenderParagraph(textScaler: TextScaler.linear(math.min(123, 456))); + RenderParagraph(); + RenderEditable(textScaler: TextScaler.linear(math.min(123, 456))); + RenderEditable(); } diff --git a/packages/flutter/test_fixes/widgets/media_query.dart b/packages/flutter/test_fixes/widgets/media_query.dart new file mode 100644 index 0000000000000..35c8450a28251 --- /dev/null +++ b/packages/flutter/test_fixes/widgets/media_query.dart @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +void main() { + // Change made in https://github.com/flutter/flutter/pull/128522 + MediaQueryData(); + MediaQueryData(textScaleFactor: 2.0) + ..copyWith(textScaleFactor: 2.0) + ..copyWith(); +} diff --git a/packages/flutter/test_fixes/widgets/media_query.dart.expect b/packages/flutter/test_fixes/widgets/media_query.dart.expect new file mode 100644 index 0000000000000..6e88c7793caf4 --- /dev/null +++ b/packages/flutter/test_fixes/widgets/media_query.dart.expect @@ -0,0 +1,13 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +void main() { + // Change made in https://github.com/flutter/flutter/pull/128522 + MediaQueryData(); + MediaQueryData(textScaler: TextScaler.linear(2.0)) + ..copyWith(textScaler: TextScaler.linear(2.0)) + ..copyWith(); +} diff --git a/packages/flutter/test_fixes/widgets/rich_text.dart b/packages/flutter/test_fixes/widgets/rich_text.dart new file mode 100644 index 0000000000000..8e61177e1c406 --- /dev/null +++ b/packages/flutter/test_fixes/widgets/rich_text.dart @@ -0,0 +1,11 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +void main() { + // Change made in https://github.com/flutter/flutter/pull/128522 + RichText(); + RichText(textScaleFactor: 2.0); +} diff --git a/packages/flutter/test_fixes/widgets/rich_text.dart.expect b/packages/flutter/test_fixes/widgets/rich_text.dart.expect new file mode 100644 index 0000000000000..c517fb4a0f25e --- /dev/null +++ b/packages/flutter/test_fixes/widgets/rich_text.dart.expect @@ -0,0 +1,11 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +void main() { + // Change made in https://github.com/flutter/flutter/pull/128522 + RichText(); + RichText(textScaler: TextScaler.linear(2.0)); +} diff --git a/packages/flutter/test_private/pubspec.yaml b/packages/flutter/test_private/pubspec.yaml index 84443b3ce2526..96d98b1cda51d 100644 --- a/packages/flutter/test_private/pubspec.yaml +++ b/packages/flutter/test_private/pubspec.yaml @@ -2,19 +2,19 @@ name: flutter_test_private description: Tests private interfaces of the flutter environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". - meta: 1.9.1 + meta: 1.10.0 path: 1.8.3 process: 4.2.4 process_runner: 4.1.2 args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: bc90 +# PUBSPEC CHECKSUM: bdb8 diff --git a/packages/flutter/test_private/test/animated_icons_private_test.dart.tmpl b/packages/flutter/test_private/test/animated_icons_private_test.dart.tmpl index af1d2c43368a6..5e0205daafdf0 100644 --- a/packages/flutter/test_private/test/animated_icons_private_test.dart.tmpl +++ b/packages/flutter/test_private/test/animated_icons_private_test.dart.tmpl @@ -12,8 +12,7 @@ library material_animated_icons; import 'dart:math' as math show pi; -import 'dart:ui' show Offset, lerpDouble; -import 'dart:ui' as ui show Canvas, Paint, Path; +import 'dart:ui' as ui show Canvas, Paint, Path, lerpDouble; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/widgets.dart'; diff --git a/packages/flutter/test_private/test/pubspec.yaml b/packages/flutter/test_private/test/pubspec.yaml index c2e5ee2e909fa..48699793fd6eb 100644 --- a/packages/flutter/test_private/test/pubspec.yaml +++ b/packages/flutter/test_private/test/pubspec.yaml @@ -1,7 +1,7 @@ name: animated_icons_private_test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -12,8 +12,8 @@ dependencies: sky_engine: sdk: flutter characters: 1.3.0 - collection: 1.17.2 - meta: 1.9.1 + collection: 1.18.0 + meta: 1.10.0 vector_math: 2.1.4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -23,12 +23,12 @@ dependencies: material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_goldens: @@ -36,7 +36,7 @@ dev_dependencies: fake_async: 1.3.1 file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 6c3d +# PUBSPEC CHECKSUM: a79b diff --git a/packages/flutter/test_release/widgets/memory_allocations_test.dart b/packages/flutter/test_release/widgets/memory_allocations_test.dart index 166725ab6b1a6..4057379308d3f 100644 --- a/packages/flutter/test_release/widgets/memory_allocations_test.dart +++ b/packages/flutter/test_release/widgets/memory_allocations_test.dart @@ -55,7 +55,7 @@ class _TestRenderObject extends RenderObject { Rect get semanticBounds => throw UnimplementedError(); } -class _TestElement extends RenderObjectElement with RootElementMixin { +class _TestElement extends RenderTreeRootElement with RootElementMixin { _TestElement(): super(_TestLeafRenderObjectWidget()); void makeInactive() { diff --git a/packages/flutter_driver/lib/src/common/handler_factory.dart b/packages/flutter_driver/lib/src/common/handler_factory.dart index 05e70aee8d2fe..9960e8396dba4 100644 --- a/packages/flutter_driver/lib/src/common/handler_factory.dart +++ b/packages/flutter_driver/lib/src/common/handler_factory.dart @@ -187,11 +187,20 @@ mixin CommandHandlerFactory { Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok); Future<LayerTree> _getLayerTree(Command command) async { - return LayerTree(RendererBinding.instance.renderView.debugLayer?.toStringDeep()); + final String trees = <String>[ + for (final RenderView renderView in RendererBinding.instance.renderViews) + if (renderView.debugLayer != null) + renderView.debugLayer!.toStringDeep(), + ].join('\n\n'); + return LayerTree(trees.isNotEmpty ? trees : null); } Future<RenderTree> _getRenderTree(Command command) async { - return RenderTree(RendererBinding.instance.renderView.toStringDeep()); + final String trees = <String>[ + for (final RenderView renderView in RendererBinding.instance.renderViews) + renderView.toStringDeep(), + ].join('\n\n'); + return RenderTree(trees.isNotEmpty ? trees : null); } Future<Result> _enterText(Command command) async { diff --git a/packages/flutter_driver/lib/src/common/wait.dart b/packages/flutter_driver/lib/src/common/wait.dart index 5d9e86fefbef3..73a3424e13ea6 100644 --- a/packages/flutter_driver/lib/src/common/wait.dart +++ b/packages/flutter_driver/lib/src/common/wait.dart @@ -9,13 +9,9 @@ import 'message.dart'; /// A Flutter Driver command that waits until a given [condition] is satisfied. class WaitForCondition extends Command { /// Creates a command that waits for the given [condition] is met. - /// - /// The [condition] argument must not be null. const WaitForCondition(this.condition, {super.timeout}); /// Deserializes this command from the value generated by [serialize]. - /// - /// The [json] argument cannot be null. WaitForCondition.deserialize(super.json) : condition = _deserialize(json), super.deserialize(); @@ -89,8 +85,6 @@ class NoTransientCallbacks extends SerializableWaitCondition { /// Factory constructor to parse a [NoTransientCallbacks] instance from the /// given JSON map. - /// - /// The [json] argument must not be null. factory NoTransientCallbacks.deserialize(Map<String, String> json) { if (json['conditionName'] != 'NoTransientCallbacksCondition') { throw SerializationException('Error occurred during deserializing the NoTransientCallbacksCondition JSON string: $json'); @@ -109,8 +103,6 @@ class NoPendingFrame extends SerializableWaitCondition { /// Factory constructor to parse a [NoPendingFrame] instance from the given /// JSON map. - /// - /// The [json] argument must not be null. factory NoPendingFrame.deserialize(Map<String, String> json) { if (json['conditionName'] != 'NoPendingFrameCondition') { throw SerializationException('Error occurred during deserializing the NoPendingFrameCondition JSON string: $json'); @@ -129,8 +121,6 @@ class FirstFrameRasterized extends SerializableWaitCondition { /// Factory constructor to parse a [FirstFrameRasterized] instance from the /// given JSON map. - /// - /// The [json] argument must not be null. factory FirstFrameRasterized.deserialize(Map<String, String> json) { if (json['conditionName'] != 'FirstFrameRasterizedCondition') { throw SerializationException('Error occurred during deserializing the FirstFrameRasterizedCondition JSON string: $json'); @@ -152,8 +142,6 @@ class NoPendingPlatformMessages extends SerializableWaitCondition { /// Factory constructor to parse a [NoPendingPlatformMessages] instance from the /// given JSON map. - /// - /// The [json] argument must not be null. factory NoPendingPlatformMessages.deserialize(Map<String, String> json) { if (json['conditionName'] != 'NoPendingPlatformMessagesCondition') { throw SerializationException('Error occurred during deserializing the NoPendingPlatformMessagesCondition JSON string: $json'); @@ -168,14 +156,10 @@ class NoPendingPlatformMessages extends SerializableWaitCondition { /// A combined condition that waits until all the given [conditions] are met. class CombinedCondition extends SerializableWaitCondition { /// Creates a [CombinedCondition] condition. - /// - /// The [conditions] argument must not be null. const CombinedCondition(this.conditions); /// Factory constructor to parse a [CombinedCondition] instance from the /// given JSON map. - /// - /// The [jsonMap] argument must not be null. factory CombinedCondition.deserialize(Map<String, String> jsonMap) { if (jsonMap['conditionName'] != 'CombinedCondition') { throw SerializationException('Error occurred during deserializing the CombinedCondition JSON string: $jsonMap'); @@ -210,8 +194,6 @@ class CombinedCondition extends SerializableWaitCondition { } /// Parses a [SerializableWaitCondition] or its subclass from the given [json] map. -/// -/// The [json] argument must not be null. SerializableWaitCondition _deserialize(Map<String, String> json) { final String conditionName = json['conditionName']!; switch (conditionName) { diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index b29becca68ef4..bedb7f4ffaec0 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -83,6 +83,11 @@ const CommonFinders find = CommonFinders._(); /// See also [FlutterDriver.waitFor]. typedef EvaluatorFunction = dynamic Function(); +// Examples can assume: +// import 'package:flutter_driver/flutter_driver.dart'; +// import 'package:test/test.dart'; +// late FlutterDriver driver; + /// Drives a Flutter Application running in another process. abstract class FlutterDriver { /// Default constructor. @@ -478,7 +483,7 @@ abstract class FlutterDriver { /// /// ```dart /// test('enters text in a text field', () async { - /// var textField = find.byValueKey('enter-text-field'); + /// final SerializableFinder textField = find.byValueKey('enter-text-field'); /// await driver.tap(textField); // acquire focus /// await driver.enterText('Hello!'); // enter text /// await driver.waitFor(find.text('Hello!')); // verify text appears on UI @@ -520,7 +525,7 @@ abstract class FlutterDriver { /// /// ```dart /// test('submit text in a text field', () async { - /// var textField = find.byValueKey('enter-text-field'); + /// final SerializableFinder textField = find.byValueKey('enter-text-field'); /// await driver.tap(textField); // acquire focus /// await driver.enterText('Hello!'); // enter text /// await driver.waitFor(find.text('Hello!')); // verify text appears on UI diff --git a/packages/flutter_driver/lib/src/driver/frame_request_pending_latency_summarizer.dart b/packages/flutter_driver/lib/src/driver/frame_request_pending_latency_summarizer.dart new file mode 100644 index 0000000000000..2554611103d99 --- /dev/null +++ b/packages/flutter_driver/lib/src/driver/frame_request_pending_latency_summarizer.dart @@ -0,0 +1,64 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'percentile_utils.dart'; +import 'timeline.dart'; + +/// Event name for frame request pending timeline events. +const String kFrameRequestPendingEvent = 'Frame Request Pending'; + +/// Summarizes [TimelineEvents]s corresponding to [kFrameRequestPendingEvent] events. +/// +/// `FrameRequestPendingLatency` is the time between `Animator::RequestFrame` +/// and `Animator::BeginFrame` for each frame built by the Flutter engine. +class FrameRequestPendingLatencySummarizer { + /// Creates a FrameRequestPendingLatencySummarizer given the timeline events. + FrameRequestPendingLatencySummarizer(this.frameRequestPendingEvents); + + /// Timeline events with names in [kFrameRequestPendingTimelineEventNames]. + final List<TimelineEvent> frameRequestPendingEvents; + + /// Computes the average `FrameRequestPendingLatency` over the period of the timeline. + double computeAverageFrameRequestPendingLatency() { + final List<double> frameRequestPendingLatencies = + _computeFrameRequestPendingLatencies(); + if (frameRequestPendingLatencies.isEmpty) { + return 0; + } + + final double total = frameRequestPendingLatencies.reduce((double a, double b) => a + b); + return total / frameRequestPendingLatencies.length; + } + + /// Computes the [percentile]-th percentile `FrameRequestPendingLatency` over the + /// period of the timeline. + double computePercentileFrameRequestPendingLatency(double percentile) { + final List<double> frameRequestPendingLatencies = + _computeFrameRequestPendingLatencies(); + if (frameRequestPendingLatencies.isEmpty) { + return 0; + } + return findPercentile(frameRequestPendingLatencies, percentile); + } + + List<double> _computeFrameRequestPendingLatencies() { + final List<double> result = <double>[]; + final Map<String, int> starts = <String, int>{}; + for (int i = 0; i < frameRequestPendingEvents.length; i++) { + final TimelineEvent event = frameRequestPendingEvents[i]; + if (event.phase == 'b') { + final String? id = event.json['id'] as String?; + if (id != null) { + starts[id] = event.timestampMicros!; + } + } else if (event.phase == 'e') { + final int? start = starts[event.json['id']]; + if (start != null) { + result.add((event.timestampMicros! - start).toDouble()); + } + } + } + return result; + } +} diff --git a/packages/flutter_driver/lib/src/driver/gc_summarizer.dart b/packages/flutter_driver/lib/src/driver/gc_summarizer.dart index 2779e782fc79b..36abab586c991 100644 --- a/packages/flutter_driver/lib/src/driver/gc_summarizer.dart +++ b/packages/flutter_driver/lib/src/driver/gc_summarizer.dart @@ -17,7 +17,7 @@ const Set<String> kGCRootEvents = <String>{ /// Summarizes [TimelineEvents]s corresponding to [kGCRootEvents] category. /// /// A sample event (some fields have been omitted for brevity): -/// ``` +/// ```json /// { /// "name": "StartConcurrentMarking", /// "cat": "GC", diff --git a/packages/flutter_driver/lib/src/driver/profiling_summarizer.dart b/packages/flutter_driver/lib/src/driver/profiling_summarizer.dart index b3e708a6b886f..e70e86679756c 100644 --- a/packages/flutter_driver/lib/src/driver/profiling_summarizer.dart +++ b/packages/flutter_driver/lib/src/driver/profiling_summarizer.dart @@ -36,7 +36,7 @@ enum ProfileType { /// Summarizes [TimelineEvents]s corresponding to [kProfilingEvents] category. /// /// A sample event (some fields have been omitted for brevity): -/// ``` +/// ```json /// { /// "category": "embedder", /// "name": "CpuUsage", diff --git a/packages/flutter_driver/lib/src/driver/raster_cache_summarizer.dart b/packages/flutter_driver/lib/src/driver/raster_cache_summarizer.dart index 2b750116a8e42..3ae673d270a3d 100644 --- a/packages/flutter_driver/lib/src/driver/raster_cache_summarizer.dart +++ b/packages/flutter_driver/lib/src/driver/raster_cache_summarizer.dart @@ -16,7 +16,7 @@ const String _kPictureMemory = 'PictureMBytes'; /// Summarizes [TimelineEvents]s corresponding to [kRasterCacheEvent] events. /// /// A sample event (some fields have been omitted for brevity): -/// ``` +/// ```json /// { /// "name": "RasterCache", /// "ts": 75598996256, diff --git a/packages/flutter_driver/lib/src/driver/scene_display_lag_summarizer.dart b/packages/flutter_driver/lib/src/driver/scene_display_lag_summarizer.dart index d6eaec031b26f..6dd2023e84570 100644 --- a/packages/flutter_driver/lib/src/driver/scene_display_lag_summarizer.dart +++ b/packages/flutter_driver/lib/src/driver/scene_display_lag_summarizer.dart @@ -13,7 +13,7 @@ const String _kVsyncTransitionsMissed = 'vsync_transitions_missed'; /// Summarizes [TimelineEvents]s corresponding to [kSceneDisplayLagEvent] events. /// /// A sample event (some fields have been omitted for brevity): -/// ``` +/// ```json /// { /// "name": "SceneDisplayLag", /// "ts": 408920509340, diff --git a/packages/flutter_driver/lib/src/driver/timeline_summary.dart b/packages/flutter_driver/lib/src/driver/timeline_summary.dart index cd7451c2998cc..d79caeffd06a7 100644 --- a/packages/flutter_driver/lib/src/driver/timeline_summary.dart +++ b/packages/flutter_driver/lib/src/driver/timeline_summary.dart @@ -9,6 +9,7 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as path; import 'common.dart'; +import 'frame_request_pending_latency_summarizer.dart'; import 'gc_summarizer.dart'; import 'percentile_utils.dart'; import 'profiling_summarizer.dart'; @@ -31,7 +32,11 @@ generated by the interaction. '''; /// The maximum amount of time considered safe to spend for a frame's build -/// phase. Anything past that is in the danger of missing the frame as 60FPS. +/// phase. Anything past that is in the danger of missing the frame at 60FPS. +/// +/// This is a hard-coded number and does not take into account the real device +/// frame rate. Prefer using percentiles on the total build or raster time +/// than metrics based on this value. const Duration kBuildBudget = Duration(milliseconds: 16); /// The name of the framework frame build events we need to filter or extract. @@ -71,9 +76,14 @@ class TimelineSummary { /// The number of frames that missed the [kBuildBudget] and therefore are /// in the danger of missing frames. - int computeMissedFrameBuildBudgetCount([ Duration frameBuildBudget = kBuildBudget ]) => _extractFrameDurations() - .where((Duration duration) => duration > kBuildBudget) - .length; + /// + /// This does not take into account the real device frame rate. Prefer using + /// [computePercentileFrameBuildTimeMillis] for evaluating performance. + int computeMissedFrameBuildBudgetCount() { + return _extractFrameDurations() + .where((Duration duration) => duration > kBuildBudget) + .length; + } /// Average amount of time spent per frame in the engine rasterizer. /// @@ -82,6 +92,20 @@ class TimelineSummary { return _averageInMillis(_extractGpuRasterizerDrawDurations()); } + /// Standard deviation amount of time spent per frame in the engine rasterizer. + /// + /// Throws a [StateError] if this summary contains no timeline events. + double computeStandardDeviationFrameRasterizerTimeMillis() { + final List<Duration> durations = _extractGpuRasterizerDrawDurations(); + final double average = _averageInMillis(durations); + double tally = 0.0; + for (final Duration duration in durations) { + final double time = duration.inMicroseconds.toDouble() / 1000.0; + tally += (average - time).abs(); + } + return tally / durations.length; + } + /// The longest frame rasterization time in milliseconds. /// /// Throws a [StateError] if this summary contains no timeline events. @@ -98,9 +122,14 @@ class TimelineSummary { /// The number of frames that missed the [kBuildBudget] on the raster thread /// and therefore are in the danger of missing frames. - int computeMissedFrameRasterizerBudgetCount([ Duration frameBuildBudget = kBuildBudget ]) => _extractGpuRasterizerDrawDurations() - .where((Duration duration) => duration > kBuildBudget) - .length; + /// + /// This does not take into account the real device frame rate. Prefer using + /// [computePercentileFrameRasterizerTimeMillis] for evaluating performance. + int computeMissedFrameRasterizerBudgetCount() { + return _extractGpuRasterizerDrawDurations() + .where((Duration duration) => duration > kBuildBudget) + .length; + } /// The total number of frames recorded in the timeline. int countFrames() => _extractFrameDurations().length; @@ -142,10 +171,14 @@ class TimelineSummary { /// See [computeWorstFrameBuildTimeMillis]. /// * "missed_frame_build_budget_count': The number of frames that missed /// the [kBuildBudget] and therefore are in the danger of missing frames. - /// See [computeMissedFrameBuildBudgetCount]. + /// See [computeMissedFrameBuildBudgetCount]. Because [kBuildBudget] is a + /// constant, this does not represent a real missed frame count. /// * "average_frame_rasterizer_time_millis": Average amount of time spent /// per frame in the engine rasterizer. /// See [computeAverageFrameRasterizerTimeMillis]. + /// * "stddev_frame_rasterizer_time_millis": Standard deviation of the amount + /// of time spent per frame in the engine rasterizer. + /// See [computeStandardDeviationFrameRasterizerTimeMillis]. /// * "90th_percentile_frame_rasterizer_time_millis" and /// "99th_percentile_frame_rasterizer_time_millis": The 90/99-th percentile /// frame rasterization time in milliseconds. @@ -155,8 +188,9 @@ class TimelineSummary { /// See [computeWorstFrameRasterizerTimeMillis]. /// * "missed_frame_rasterizer_budget_count": The number of frames that missed /// the [kBuildBudget] on the raster thread and therefore are in the danger - /// of missing frames. - /// See [computeMissedFrameRasterizerBudgetCount]. + /// of missing frames. See [computeMissedFrameRasterizerBudgetCount]. + /// Because [kBuildBudget] is a constant, this does not represent a real + /// missed frame count. /// * "frame_count": The total number of frames recorded in the timeline. This /// is also the length of the "frame_build_times" and the "frame_begin_times" /// lists. @@ -225,6 +259,14 @@ class TimelineSummary { /// * "worst_picture_cache_memory": The worst (highest) value seen for the /// memory used for the engine picture cache entries. /// See [RasterCacheSummarizer.computeWorstPictureMemory]. + /// * "average_frame_request_pending_latency": Computes the average of the delay + /// between `Animator::RequestFrame` and `Animator::BeginFrame` in the engine. + /// See [FrameRequestPendingLatencySummarizer.computeAverageFrameRequestPendingLatency]. + /// * "90th_percentile_frame_request_pending_latency" and + /// "99th_percentile_frame_request_pending_latency": The 90/99-th percentile + /// delay between `Animator::RequestFrame` and `Animator::BeginFrame` in the + /// engine. + /// See [FrameRequestPendingLatencySummarizer.computePercentileFrameRequestPendingLatency]. Map<String, dynamic> get summaryJson { final SceneDisplayLagSummarizer sceneDisplayLagSummarizer = _sceneDisplayLagSummarizer(); final VsyncFrameLagSummarizer vsyncFrameLagSummarizer = _vsyncFrameLagSummarizer(); @@ -232,6 +274,7 @@ class TimelineSummary { final RasterCacheSummarizer rasterCacheSummarizer = _rasterCacheSummarizer(); final GCSummarizer gcSummarizer = _gcSummarizer(); final RefreshRateSummary refreshRateSummary = RefreshRateSummary(vsyncEvents: _extractNamedEvents(kUIThreadVsyncProcessEvent)); + final FrameRequestPendingLatencySummarizer frameRequestPendingLatencySummarizer = _frameRequestPendingLatencySummarizer(); final Map<String, dynamic> timelineSummary = <String, dynamic>{ 'average_frame_build_time_millis': computeAverageFrameBuildTimeMillis(), @@ -240,6 +283,7 @@ class TimelineSummary { 'worst_frame_build_time_millis': computeWorstFrameBuildTimeMillis(), 'missed_frame_build_budget_count': computeMissedFrameBuildBudgetCount(), 'average_frame_rasterizer_time_millis': computeAverageFrameRasterizerTimeMillis(), + 'stddev_frame_rasterizer_time_millis': computeStandardDeviationFrameRasterizerTimeMillis(), '90th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(90.0), '99th_percentile_frame_rasterizer_time_millis': computePercentileFrameRasterizerTimeMillis(99.0), 'worst_frame_rasterizer_time_millis': computeWorstFrameRasterizerTimeMillis(), @@ -269,6 +313,9 @@ class TimelineSummary { 'average_layer_cache_count': rasterCacheSummarizer.computeAverageLayerCount(), '90th_percentile_layer_cache_count': rasterCacheSummarizer.computePercentileLayerCount(90.0), '99th_percentile_layer_cache_count': rasterCacheSummarizer.computePercentileLayerCount(99.0), + 'average_frame_request_pending_latency': frameRequestPendingLatencySummarizer.computeAverageFrameRequestPendingLatency(), + '90th_percentile_frame_request_pending_latency': frameRequestPendingLatencySummarizer.computePercentileFrameRequestPendingLatency(90.0), + '99th_percentile_frame_request_pending_latency': frameRequestPendingLatencySummarizer.computePercentileFrameRequestPendingLatency(99.0), 'worst_layer_cache_count': rasterCacheSummarizer.computeWorstLayerCount(), 'average_layer_cache_memory': rasterCacheSummarizer.computeAverageLayerMemory(), '90th_percentile_layer_cache_memory': rasterCacheSummarizer.computePercentileLayerMemory(90.0), @@ -457,5 +504,7 @@ class TimelineSummary { RasterCacheSummarizer _rasterCacheSummarizer() => RasterCacheSummarizer(_extractNamedEvents(kRasterCacheEvent)); + FrameRequestPendingLatencySummarizer _frameRequestPendingLatencySummarizer() => FrameRequestPendingLatencySummarizer(_extractNamedEvents(kFrameRequestPendingEvent)); + GCSummarizer _gcSummarizer() => GCSummarizer.fromEvents(_extractEventsWithNames(kGCRootEvents)); } diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index 2bb6705dd25b4..6e29825d32523 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -58,6 +58,19 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, } } +// Examples can assume: +// import 'package:flutter_driver/flutter_driver.dart'; +// import 'package:flutter/widgets.dart'; +// import 'package:flutter_driver/driver_extension.dart'; +// import 'package:flutter_test/flutter_test.dart' hide find; +// import 'package:flutter_test/flutter_test.dart' as flutter_test; +// typedef MyHomeWidget = Placeholder; +// abstract class SomeWidget extends StatelessWidget { const SomeWidget({super.key, required this.title}); final String title; } +// late FlutterDriver driver; +// abstract class StubNestedCommand { int get times; SerializableFinder get finder; } +// class StubCommandResult extends Result { const StubCommandResult(this.arg); final String arg; @override Map<String, dynamic> toJson() => <String, dynamic>{}; } +// abstract class StubProberCommand { int get times; SerializableFinder get finder; } + /// Enables Flutter Driver VM service extension. /// /// This extension is required for tests that use `package:flutter_driver` to @@ -87,26 +100,40 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, /// The `finders` and `commands` parameters are optional and used to add custom /// finders or commands, as in the following example. /// -/// ```dart main +/// ```dart /// void main() { /// enableFlutterDriverExtension( /// finders: <FinderExtension>[ SomeFinderExtension() ], /// commands: <CommandExtension>[ SomeCommandExtension() ], /// ); /// -/// app.main(); +/// runApp(const MyHomeWidget()); /// } -/// ``` /// -/// ```dart -/// driver.sendCommand(SomeCommand(ByValueKey('Button'), 7)); -/// ``` +/// class SomeFinderExtension extends FinderExtension { +/// @override +/// String get finderType => 'SomeFinder'; /// -/// `SomeFinder` and `SomeFinderExtension` must be placed in different files -/// to avoid `dart:ui` import issue. Imports relative to `dart:ui` can't be -/// accessed from host runner, where flutter runtime is not accessible. +/// @override +/// SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory) { +/// return SomeFinder(params['title']!); +/// } /// -/// ```dart +/// @override +/// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) { +/// final SomeFinder someFinder = finder as SomeFinder; +/// +/// return flutter_test.find.byElementPredicate((Element element) { +/// final Widget widget = element.widget; +/// if (widget is SomeWidget) { +/// return widget.title == someFinder.title; +/// } +/// return false; +/// }); +/// } +/// } +/// +/// // Use this class in a test anywhere where a SerializableFinder is expected. /// class SomeFinder extends SerializableFinder { /// const SomeFinder(this.title); /// @@ -120,43 +147,51 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, /// 'title': title, /// }); /// } -/// ``` /// -/// ```dart -/// class SomeFinderExtension extends FinderExtension { +/// class SomeCommandExtension extends CommandExtension { +/// @override +/// String get commandKind => 'SomeCommand'; /// -/// String get finderType => 'SomeFinder'; +/// @override +/// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async { +/// final SomeCommand someCommand = command as SomeCommand; /// -/// SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory) { -/// return SomeFinder(json['title']); -/// } +/// // Deserialize [Finder]: +/// final Finder finder = finderFactory.createFinder(someCommand.finder); /// -/// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) { -/// Some someFinder = finder as SomeFinder; +/// // Wait for [Element]: +/// handlerFactory.waitForElement(finder); /// -/// return find.byElementPredicate((Element element) { -/// final Widget widget = element.widget; -/// if (element.widget is SomeWidget) { -/// return element.widget.title == someFinder.title; -/// } -/// return false; -/// }); -/// } -/// } -/// ``` +/// // Alternatively, wait for [Element] absence: +/// handlerFactory.waitForAbsentElement(finder); /// -/// `SomeCommand`, `SomeResult` and `SomeCommandExtension` must be placed in -/// different files to avoid `dart:ui` import issue. Imports relative to `dart:ui` -/// can't be accessed from host runner, where flutter runtime is not accessible. +/// // Submit known [Command]s: +/// for (int i = 0; i < someCommand.times; i++) { +/// await handlerFactory.handleCommand(Tap(someCommand.finder), prober, finderFactory); +/// } /// -/// ```dart +/// // Alternatively, use [WidgetController]: +/// for (int i = 0; i < someCommand.times; i++) { +/// await prober.tap(finder); +/// } +/// +/// return const SomeCommandResult('foo bar'); +/// } +/// +/// @override +/// Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory) { +/// return SomeCommand.deserialize(params, finderFactory); +/// } +/// } +/// +/// // Pass an instance of this class to `FlutterDriver.sendCommand` to invoke +/// // the custom command during a test. /// class SomeCommand extends CommandWithTarget { -/// SomeCommand(SerializableFinder finder, this.times, {Duration? timeout}) -/// : super(finder, timeout: timeout); +/// SomeCommand(super.finder, this.times, {super.timeout}); /// -/// SomeCommand.deserialize(Map<String, String> json, DeserializeFinderFactory finderFactory) +/// SomeCommand.deserialize(super.json, super.finderFactory) /// : times = int.parse(json['times']!), -/// super.deserialize(json, finderFactory); +/// super.deserialize(); /// /// @override /// Map<String, String> serialize() { @@ -168,9 +203,7 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, /// /// final int times; /// } -/// ``` /// -/// ```dart /// class SomeCommandResult extends Result { /// const SomeCommandResult(this.resultParam); /// @@ -184,45 +217,6 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, /// } /// } /// ``` -/// -/// ```dart -/// class SomeCommandExtension extends CommandExtension { -/// @override -/// String get commandKind => 'SomeCommand'; -/// -/// @override -/// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async { -/// final SomeCommand someCommand = command as SomeCommand; -/// -/// // Deserialize [Finder]: -/// final Finder finder = finderFactory.createFinder(stubCommand.finder); -/// -/// // Wait for [Element]: -/// handlerFactory.waitForElement(finder); -/// -/// // Alternatively, wait for [Element] absence: -/// handlerFactory.waitForAbsentElement(finder); -/// -/// // Submit known [Command]s: -/// for (int index = 0; i < someCommand.times; index++) { -/// await handlerFactory.handleCommand(Tap(someCommand.finder), prober, finderFactory); -/// } -/// -/// // Alternatively, use [WidgetController]: -/// for (int index = 0; i < stubCommand.times; index++) { -/// await prober.tap(finder); -/// } -/// -/// return const SomeCommandResult('foo bar'); -/// } -/// -/// @override -/// Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory) { -/// return SomeCommand.deserialize(params, finderFactory); -/// } -/// } -/// ``` -/// void enableFlutterDriverExtension({ DataHandler? handler, bool silenceErrors = false, bool enableTextEntryEmulation = true, List<FinderExtension>? finders, List<CommandExtension>? commands}) { _DriverBinding(handler, silenceErrors, enableTextEntryEmulation, finders ?? <FinderExtension>[], commands ?? <CommandExtension>[]); assert(WidgetsBinding.instance is _DriverBinding); @@ -287,7 +281,7 @@ abstract class CommandExtension { /// @override /// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async { /// final StubNestedCommand stubCommand = command as StubNestedCommand; - /// for (int index = 0; i < stubCommand.times; index++) { + /// for (int i = 0; i < stubCommand.times; i++) { /// await handlerFactory.handleCommand(Tap(stubCommand.finder), prober, finderFactory); /// } /// return const StubCommandResult('stub response'); @@ -300,7 +294,7 @@ abstract class CommandExtension { /// @override /// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async { /// final StubProberCommand stubCommand = command as StubProberCommand; - /// for (int index = 0; i < stubCommand.times; index++) { + /// for (int i = 0; i < stubCommand.times; i++) { /// await prober.tap(finderFactory.createFinder(stubCommand.finder)); /// } /// return const StubCommandResult('stub response'); diff --git a/packages/flutter_driver/lib/src/extension/wait_conditions.dart b/packages/flutter_driver/lib/src/extension/wait_conditions.dart index 1f61bcc1ca0ff..424a0adf9370a 100644 --- a/packages/flutter_driver/lib/src/extension/wait_conditions.dart +++ b/packages/flutter_driver/lib/src/extension/wait_conditions.dart @@ -39,8 +39,6 @@ class _InternalNoTransientCallbacksCondition implements WaitCondition { /// Factory constructor to parse an [InternalNoTransientCallbacksCondition] /// instance from the given [SerializableWaitCondition] instance. - /// - /// The [condition] argument must not be null. factory _InternalNoTransientCallbacksCondition.deserialize(SerializableWaitCondition condition) { if (condition.conditionName != 'NoTransientCallbacksCondition') { throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}'); @@ -67,8 +65,6 @@ class _InternalNoPendingFrameCondition implements WaitCondition { /// Factory constructor to parse an [InternalNoPendingFrameCondition] instance /// from the given [SerializableWaitCondition] instance. - /// - /// The [condition] argument must not be null. factory _InternalNoPendingFrameCondition.deserialize(SerializableWaitCondition condition) { if (condition.conditionName != 'NoPendingFrameCondition') { throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}'); @@ -95,8 +91,6 @@ class _InternalFirstFrameRasterizedCondition implements WaitCondition { /// Factory constructor to parse an [InternalNoPendingFrameCondition] instance /// from the given [SerializableWaitCondition] instance. - /// - /// The [condition] argument must not be null. factory _InternalFirstFrameRasterizedCondition.deserialize(SerializableWaitCondition condition) { if (condition.conditionName != 'FirstFrameRasterizedCondition') { throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}'); @@ -121,8 +115,6 @@ class _InternalNoPendingPlatformMessagesCondition implements WaitCondition { /// Factory constructor to parse an [_InternalNoPendingPlatformMessagesCondition] instance /// from the given [SerializableWaitCondition] instance. - /// - /// The [condition] argument must not be null. factory _InternalNoPendingPlatformMessagesCondition.deserialize(SerializableWaitCondition condition) { if (condition.conditionName != 'NoPendingPlatformMessagesCondition') { throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}'); @@ -150,14 +142,10 @@ class _InternalNoPendingPlatformMessagesCondition implements WaitCondition { class _InternalCombinedCondition implements WaitCondition { /// Creates an [_InternalCombinedCondition] instance with the given list of /// [conditions]. - /// - /// The [conditions] argument must not be null. const _InternalCombinedCondition(this.conditions); /// Factory constructor to parse an [_InternalCombinedCondition] instance from /// the given [SerializableWaitCondition] instance. - /// - /// The [condition] argument must not be null. factory _InternalCombinedCondition.deserialize(SerializableWaitCondition condition) { if (condition.conditionName != 'CombinedCondition') { throw SerializationException('Error occurred during deserializing from the given condition: ${condition.serialize()}'); @@ -187,8 +175,6 @@ class _InternalCombinedCondition implements WaitCondition { } /// Parses a [WaitCondition] or its subclass from the given serializable [waitCondition]. -/// -/// The [waitCondition] argument must not be null. WaitCondition deserializeCondition(SerializableWaitCondition waitCondition) { final String conditionName = waitCondition.conditionName; switch (conditionName) { diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index d817a82060eb7..005b23ad71f20 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -3,7 +3,7 @@ description: Integration and performance test API for Flutter applications homepage: https://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: file: 6.1.4 @@ -14,35 +14,35 @@ dependencies: fuchsia_remote_debug_protocol: sdk: flutter path: 1.8.3 - meta: 1.9.1 - vm_service: 11.7.1 + meta: 1.10.0 + vm_service: 11.10.0 webdriver: 3.0.2 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" process: 4.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: fake_async: 1.3.1 - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -65,11 +65,11 @@ dev_dependencies: shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a395 +# PUBSPEC CHECKSUM: 5ef5 diff --git a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart index b94249191e4e4..2e6c70a064f03 100644 --- a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart +++ b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart @@ -143,6 +143,20 @@ void main() { return result; } + Map<String, dynamic> frameRequestPendingStart(String id, int timeStamp) => <String, dynamic>{ + 'name': 'Frame Request Pending', + 'ph': 'b', + 'id': id, + 'ts': timeStamp, + }; + + Map<String, dynamic> frameRequestPendingEnd(String id, int timeStamp) => <String, dynamic>{ + 'name': 'Frame Request Pending', + 'ph': 'e', + 'id': id, + 'ts': timeStamp, + }; + group('frame_count', () { test('counts frames', () { expect( @@ -448,13 +462,19 @@ void main() { expect( summarize(<Map<String, dynamic>>[ begin(1000), end(19000), - begin(19000), end(29000), - begin(29000), end(49000), + begin(19001), end(29001), + begin(29002), end(49002), ...newGenGC(4, 10, 100), ...oldGenGC(5, 10000, 100), frameBegin(1000), frameEnd(18000), frameBegin(19000), frameEnd(28000), frameBegin(29000), frameEnd(48000), + frameRequestPendingStart('1', 1000), + frameRequestPendingEnd('1', 2000), + frameRequestPendingStart('2', 3000), + frameRequestPendingEnd('2', 5000), + frameRequestPendingStart('3', 6000), + frameRequestPendingEnd('3', 9000), ]).summaryJson, <String, dynamic>{ 'average_frame_build_time_millis': 15.0, @@ -463,6 +483,7 @@ void main() { 'worst_frame_build_time_millis': 19.0, 'missed_frame_build_budget_count': 2, 'average_frame_rasterizer_time_millis': 16.0, + 'stddev_frame_rasterizer_time_millis': 4.0, '90th_percentile_frame_rasterizer_time_millis': 20.0, '99th_percentile_frame_rasterizer_time_millis': 20.0, 'worst_frame_rasterizer_time_millis': 20.0, @@ -474,7 +495,7 @@ void main() { 'frame_build_times': <int>[17000, 9000, 19000], 'frame_rasterizer_times': <int>[18000, 10000, 20000], 'frame_begin_times': <int>[0, 18000, 28000], - 'frame_rasterizer_begin_times': <int>[0, 18000, 28000], + 'frame_rasterizer_begin_times': <int>[0, 18001, 28002], 'average_vsync_transitions_missed': 0.0, '90th_percentile_vsync_transitions_missed': 0.0, '99th_percentile_vsync_transitions_missed': 0.0, @@ -504,6 +525,9 @@ void main() { '90hz_frame_percentage': 0, '120hz_frame_percentage': 0, 'illegal_refresh_rate_frame_count': 0, + 'average_frame_request_pending_latency': 2000.0, + '90th_percentile_frame_request_pending_latency': 3000.0, + '99th_percentile_frame_request_pending_latency': 3000.0, }, ); }); @@ -555,8 +579,8 @@ void main() { test('writes summary to JSON file', () async { await summarize(<Map<String, dynamic>>[ begin(1000), end(19000), - begin(19000), end(29000), - begin(29000), end(49000), + begin(19001), end(29001), + begin(29002), end(49002), frameBegin(1000), frameEnd(18000), frameBegin(19000), frameEnd(28000), frameBegin(29000), frameEnd(48000), @@ -568,6 +592,12 @@ void main() { cpuUsage(5000, 20), cpuUsage(5010, 60), memoryUsage(6000, 20, 40), memoryUsage(6100, 30, 45), platformVsync(7000), vsyncCallback(7500), + frameRequestPendingStart('1', 1000), + frameRequestPendingEnd('1', 2000), + frameRequestPendingStart('2', 3000), + frameRequestPendingEnd('2', 5000), + frameRequestPendingStart('3', 6000), + frameRequestPendingEnd('3', 9000), ]).writeTimelineToFile('test', destinationDirectory: tempDir.path); final String written = await fs.file(path.join(tempDir.path, 'test.timeline_summary.json')).readAsString(); @@ -578,6 +608,7 @@ void main() { '99th_percentile_frame_build_time_millis': 19.0, 'missed_frame_build_budget_count': 2, 'average_frame_rasterizer_time_millis': 16.0, + 'stddev_frame_rasterizer_time_millis': 4.0, '90th_percentile_frame_rasterizer_time_millis': 20.0, '99th_percentile_frame_rasterizer_time_millis': 20.0, 'worst_frame_rasterizer_time_millis': 20.0, @@ -589,7 +620,7 @@ void main() { 'frame_build_times': <int>[17000, 9000, 19000], 'frame_rasterizer_times': <int>[18000, 10000, 20000], 'frame_begin_times': <int>[0, 18000, 28000], - 'frame_rasterizer_begin_times': <int>[0, 18000, 28000], + 'frame_rasterizer_begin_times': <int>[0, 18001, 28002], 'average_vsync_transitions_missed': 8.0, '90th_percentile_vsync_transitions_missed': 12.0, '99th_percentile_vsync_transitions_missed': 12.0, @@ -625,6 +656,9 @@ void main() { '90hz_frame_percentage': 0, '120hz_frame_percentage': 0, 'illegal_refresh_rate_frame_count': 0, + 'average_frame_request_pending_latency': 2000.0, + '90th_percentile_frame_request_pending_latency': 3000.0, + '99th_percentile_frame_request_pending_latency': 3000.0, }); }); }); diff --git a/packages/flutter_goldens/lib/flutter_goldens.dart b/packages/flutter_goldens/lib/flutter_goldens.dart index 2e53f34233c7b..d9b901e267027 100644 --- a/packages/flutter_goldens/lib/flutter_goldens.dart +++ b/packages/flutter_goldens/lib/flutter_goldens.dart @@ -97,11 +97,11 @@ abstract class FlutterGoldenFileComparator extends GoldenFileComparator { }); /// The directory to which golden file URIs will be resolved in [compare] and - /// [update], cannot be null. + /// [update]. final Uri basedir; /// A client for uploading image tests and making baseline requests to the - /// Flutter Gold Dashboard, cannot be null. + /// Flutter Gold Dashboard. final SkiaGoldClient skiaClient; /// The file system used to perform file access. @@ -378,8 +378,6 @@ class FlutterSkippingFileComparator extends FlutterGoldenFileComparator { }); /// Describes the reason for using the [FlutterSkippingFileComparator]. - /// - /// Cannot be null. final String reason; /// Creates a new [FlutterSkippingFileComparator] that mirrors the diff --git a/packages/flutter_goldens/pubspec.yaml b/packages/flutter_goldens/pubspec.yaml index 07e7fd2d56a37..ad0b4d260529f 100644 --- a/packages/flutter_goldens/pubspec.yaml +++ b/packages/flutter_goldens/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_goldens environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -12,28 +12,28 @@ dependencies: flutter_goldens_client: path: ../flutter_goldens_client file: 6.1.4 - meta: 1.9.1 - platform: 3.1.0 + meta: 1.10.0 + platform: 3.1.2 process: 4.2.4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 6a9e +# PUBSPEC CHECKSUM: 5bfc diff --git a/packages/flutter_goldens_client/pubspec.yaml b/packages/flutter_goldens_client/pubspec.yaml index 340c1756ac28c..e1254b0c6796f 100644 --- a/packages/flutter_goldens_client/pubspec.yaml +++ b/packages/flutter_goldens_client/pubspec.yaml @@ -1,17 +1,17 @@ name: flutter_goldens_client environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". crypto: 3.0.3 file: 6.1.4 - platform: 3.1.0 + platform: 3.1.2 process: 4.2.4 - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -19,4 +19,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: c70c +# PUBSPEC CHECKSUM: 1c34 diff --git a/packages/flutter_localizations/lib/src/cupertino_localizations.dart b/packages/flutter_localizations/lib/src/cupertino_localizations.dart index 4efbfb671c651..ec9b82120785a 100644 --- a/packages/flutter_localizations/lib/src/cupertino_localizations.dart +++ b/packages/flutter_localizations/lib/src/cupertino_localizations.dart @@ -10,6 +10,10 @@ import 'l10n/generated_cupertino_localizations.dart'; import 'utils/date_localizations.dart' as util; import 'widgets_localizations.dart'; +// Examples can assume: +// import 'package:flutter_localizations/flutter_localizations.dart'; +// import 'package:flutter/cupertino.dart'; + /// Implementation of localized strings for Cupertino widgets using the `intl` /// package for date and time formatting. /// @@ -32,11 +36,11 @@ import 'widgets_localizations.dart'; /// app supports with [CupertinoApp.supportedLocales]: /// /// ```dart -/// CupertinoApp( +/// const CupertinoApp( /// localizationsDelegates: GlobalCupertinoLocalizations.delegates, -/// supportedLocales: [ -/// const Locale('en', 'US'), // American English -/// const Locale('he', 'IL'), // Israeli Hebrew +/// supportedLocales: <Locale>[ +/// Locale('en', 'US'), // American English +/// Locale('he', 'IL'), // Israeli Hebrew /// // ... /// ], /// // ... @@ -96,6 +100,18 @@ abstract class GlobalCupertinoLocalizations implements CupertinoLocalizations { return _fullYearFormat.dateSymbols.MONTHS[monthIndex - 1]; } + @override + String datePickerStandaloneMonth(int monthIndex) { + // It doesn't actually have anything to do with _fullYearFormat. It's just + // taking advantage of the fact that _fullYearFormat loaded the needed + // locale's symbols. + // + // Because this will be used without specifying any day of month, + // in most cases it should be capitalized (according to rules in specific language). + return intl.toBeginningOfSentenceCase(_fullYearFormat.dateSymbols.STANDALONEMONTHS[monthIndex - 1]) ?? + _fullYearFormat.dateSymbols.STANDALONEMONTHS[monthIndex - 1]; + } + @override String datePickerDayOfMonth(int dayIndex, [int? weekDay]) { if (weekDay != null) { @@ -432,11 +448,11 @@ abstract class GlobalCupertinoLocalizations implements CupertinoLocalizations { /// app supports with [CupertinoApp.supportedLocales]: /// /// ```dart - /// CupertinoApp( + /// const CupertinoApp( /// localizationsDelegates: GlobalCupertinoLocalizations.delegates, - /// supportedLocales: [ - /// const Locale('en', 'US'), // English - /// const Locale('he', 'IL'), // Hebrew + /// supportedLocales: <Locale>[ + /// Locale('en', 'US'), // English + /// Locale('he', 'IL'), // Hebrew /// ], /// // ... /// ) diff --git a/packages/flutter_localizations/lib/src/l10n/README.md b/packages/flutter_localizations/lib/src/l10n/README.md index 857c687f18bad..4ff175ea1cd58 100644 --- a/packages/flutter_localizations/lib/src/l10n/README.md +++ b/packages/flutter_localizations/lib/src/l10n/README.md @@ -15,12 +15,10 @@ apps in general, see the ### Translations for one locale: .arb files The Material and Cupertino libraries use -[Application Resource Bundle](https://code.google.com/p/arb/wiki/ApplicationResourceBundleSpecification) +[Application Resource Bundle](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification) files, which have a `.arb` extension, to store localized translations of messages, format strings, and other values. This format is also -used by the Dart [intl](https://pub.dev/packages/intl) -package and it is supported by the -[Google Translators Toolkit](https://translate.google.com/toolkit). +used by the Dart [intl](https://pub.dev/packages/intl) package. The Material and Cupertino libraries only depend on a small subset of the ARB format. Each .arb file contains a single JSON table that diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_af.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_af.arb index 1f96f95a55a0c..8a689cc1deee5 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_af.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_af.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Oortjie $tabIndex van $tabCount", "modalBarrierDismissLabel": "Maak toe", "searchTextFieldPlaceholderLabel": "Soek", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Geen plaasvervangers gevind nie", + "menuDismissLabel": "Maak kieslys toe", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_am.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_am.arb index d3de14de4340c..e2075ff897e2b 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_am.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_am.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "ትር $tabIndex ከ$tabCount", "modalBarrierDismissLabel": "አሰናብት", "searchTextFieldPlaceholderLabel": "ፍለጋ", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ምንም ተተኪዎች አልተገኙም", + "menuDismissLabel": "ምናሌን አሰናብት", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ar.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ar.arb index 6f61d6b28544c..dbe42013b85bc 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ar.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ar.arb @@ -42,5 +42,9 @@ "tabSemanticsLabel": "علامة التبويب $tabIndex من $tabCount", "modalBarrierDismissLabel": "رفض", "searchTextFieldPlaceholderLabel": "بحث", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "لم يتم العثور على بدائل", + "menuDismissLabel": "إغلاق القائمة", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_as.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_as.arb index e40478d282f68..18770258b9084 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_as.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_as.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount টা টেবৰ $tabIndex নম্বৰটো", "modalBarrierDismissLabel": "অগ্ৰাহ্য কৰক", "searchTextFieldPlaceholderLabel": "সন্ধান কৰক", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "এইটোৰ সলনি ব্যৱহাৰ কৰিব পৰা শব্দ পোৱা নগ’ল", + "menuDismissLabel": "অগ্ৰাহ্য কৰাৰ মেনু", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_az.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_az.arb index 36b8ebb3f5aae..bef3e1de9a87c 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_az.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_az.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex/$tabCount", "modalBarrierDismissLabel": "İmtina edin", "searchTextFieldPlaceholderLabel": "Axtarın", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Əvəzləmə Tapılmadı", + "menuDismissLabel": "Menyunu qapadın", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_be.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_be.arb index 2ac08c1cf5289..da1534cf8dcfe 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_be.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_be.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Укладка $tabIndex з $tabCount", "modalBarrierDismissLabel": "Адхіліць", "searchTextFieldPlaceholderLabel": "Пошук", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Замен не знойдзена", + "menuDismissLabel": "Закрыць меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_bg.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_bg.arb index 70dbfb2554b22..8c727de43f2fb 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_bg.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_bg.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Раздел $tabIndex от $tabCount", "modalBarrierDismissLabel": "Отхвърляне", "searchTextFieldPlaceholderLabel": "Търсене", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Не бяха намерени замествания", + "menuDismissLabel": "Отхвърляне на менюто", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_bn.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_bn.arb index a69f8b2575a86..df1afd72b0462 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_bn.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_bn.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount-এর মধ্যে $tabIndex নম্বর ট্যাব", "modalBarrierDismissLabel": "খারিজ করুন", "searchTextFieldPlaceholderLabel": "সার্চ করুন", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "কোনও বিকল্প বানান দেখানো হয়নি", + "menuDismissLabel": "বাতিল করার মেনু", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_bs.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_bs.arb index 5a0039f51e95c..12f25c219eb56 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_bs.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_bs.arb @@ -27,5 +27,9 @@ "tabSemanticsLabel": "Kartica $tabIndex od $tabCount", "modalBarrierDismissLabel": "Odbaci", "searchTextFieldPlaceholderLabel": "Pretraživanje", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nije pronađena nijedna zamjena", + "menuDismissLabel": "Odbacivanje menija", + "lookUpButtonLabel": "Pogled prema gore", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ca.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ca.arb index 9f0fa56d2d6e1..e11eb138abc82 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ca.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ca.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Pestanya $tabIndex de $tabCount", "modalBarrierDismissLabel": "Ignora", "searchTextFieldPlaceholderLabel": "Cerca", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "No s'ha trobat cap substitució", + "menuDismissLabel": "Ignora el menú", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_cs.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_cs.arb index bf8c3c0efdf5b..de7c1b5d53456 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_cs.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_cs.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Karta $tabIndex z $tabCount", "modalBarrierDismissLabel": "Zavřít", "searchTextFieldPlaceholderLabel": "Hledat", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Žádná nahrazení nenalezena", + "menuDismissLabel": "Zavřít nabídku", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb index 4ca5372dbe5ee..90a118f4298dd 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb @@ -42,5 +42,9 @@ "selectAllButtonLabel": "Dewis y Cyfan", "searchTextFieldPlaceholderLabel": "Chwilio", "modalBarrierDismissLabel": "Diystyru", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Dim Ailosodiadau wedi'u Canfod", + "menuDismissLabel": "Diystyru'r ddewislen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_da.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_da.arb index c98e5d763c170..29b7d4a691cdb 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_da.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_da.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Fane $tabIndex af $tabCount", "modalBarrierDismissLabel": "Afvis", "searchTextFieldPlaceholderLabel": "Søg", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Der blev ikke fundet nogen erstatninger", + "menuDismissLabel": "Luk menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_de.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_de.arb index df870f03a041b..5eeefca1c7857 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_de.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_de.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex von $tabCount", "modalBarrierDismissLabel": "Schließen", "searchTextFieldPlaceholderLabel": "Suche", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Keine Ersetzungen gefunden", + "menuDismissLabel": "Menü schließen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_el.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_el.arb index 8020296f5b885..af1ee5a8bf999 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_el.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_el.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Καρτέλα $tabIndex από $tabCount", "modalBarrierDismissLabel": "Παράβλεψη", "searchTextFieldPlaceholderLabel": "Αναζήτηση", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Δεν βρέθηκαν αντικαταστάσεις", + "menuDismissLabel": "Παράβλεψη μενού", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en.arb index 2c003f7d5b415..6f52dc78ead20 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en.arb @@ -165,6 +165,21 @@ "description": "The label for select-all buttons and menu items. The reference abbreviation is what iOS shows on text selection toolbars." }, + "lookUpButtonLabel": "Look Up", + "@lookUpButtonLabel": { + "description": "The label for the Look Up button and menu items on iOS." + }, + + "searchWebButtonLabel": "Search Web", + "@searchWebButtonLabel": { + "description": "The label for the Search Web button and menu items on iOS." + }, + + "shareButtonLabel": "Share...", + "@shareButtonLabel": { + "description": "The label for the Share button and menu items on iOS." + }, + "noSpellCheckReplacementsLabel": "No Replacements Found", "@noSpellCheckReplacementsLabel": { "description": "The label shown in the text selection context menu on iOS when a misspelled word is tapped but the spell checker found no reasonable fixes for it." @@ -178,5 +193,10 @@ "modalBarrierDismissLabel": "Dismiss", "@modalBarrierDismissLabel": { "description": "Label read out by accessibility tools (VoiceOver) for a modal barrier to indicate that a tap dismisses the barrier. A modal barrier can, for example, be found behind an alert or popup to block user interaction with elements behind it." + }, + + "menuDismissLabel": "Dismiss menu", + "@menuDismissLabel": { + "description": "Label read out by accessibility tools (TalkBack or VoiceOver) for the area around a menu to indicate that a tap dismisses the menu." } } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_AU.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_AU.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_AU.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_AU.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_GB.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_GB.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_GB.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_GB.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_IE.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_IE.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_IE.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_IE.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_IN.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_IN.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_IN.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_IN.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_NZ.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_NZ.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_NZ.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_NZ.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_SG.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_SG.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_SG.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_SG.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_en_ZA.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_en_ZA.arb index a9fac71f82f0f..c0780fe62b876 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_en_ZA.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_en_ZA.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Look up", + "noSpellCheckReplacementsLabel": "No replacements found", + "menuDismissLabel": "Dismiss menu", "searchTextFieldPlaceholderLabel": "Search", "tabSemanticsLabel": "Tab $tabIndex of $tabCount", "datePickerHourSemanticsLabelOne": "$hour o'clock", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es.arb index fbbcdf4e484c0..aab7584291ddd 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "modalBarrierDismissLabel": "Cerrar", "searchTextFieldPlaceholderLabel": "Buscar", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "No se ha encontrado ninguna sustitución", + "menuDismissLabel": "Cerrar menú", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_419.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_419.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_419.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_419.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_AR.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_AR.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_AR.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_AR.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_BO.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_BO.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_BO.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_BO.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CL.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CL.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CL.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CL.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CO.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CO.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CO.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CO.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CR.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CR.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_CR.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_CR.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_DO.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_DO.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_DO.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_DO.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_EC.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_EC.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_EC.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_EC.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_GT.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_GT.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_GT.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_GT.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_HN.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_HN.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_HN.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_HN.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_MX.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_MX.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_MX.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_MX.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_NI.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_NI.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_NI.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_NI.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PA.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PA.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PA.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PA.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PE.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PE.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PE.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PE.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PR.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PR.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PR.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PR.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PY.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PY.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_PY.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_PY.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_SV.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_SV.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_SV.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_SV.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_US.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_US.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_US.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_US.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_UY.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_UY.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_UY.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_UY.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_es_VE.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_es_VE.arb index 272e2aea1a2c3..b716dc2b5bb91 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_es_VE.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_es_VE.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Mirar hacia arriba", + "noSpellCheckReplacementsLabel": "No se encontraron reemplazos", + "menuDismissLabel": "Descartar menú", "searchTextFieldPlaceholderLabel": "Buscar", "tabSemanticsLabel": "Pestaña $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour en punto", @@ -20,6 +23,6 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo", + "selectAllButtonLabel": "Seleccionar todos", "modalBarrierDismissLabel": "Descartar" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_et.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_et.arb index b584e67feae57..c4c6c6acdc0b7 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_et.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_et.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabIndex. vaheleht $tabCount-st", "modalBarrierDismissLabel": "Loobu", "searchTextFieldPlaceholderLabel": "Otsige", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Asendusi ei leitud", + "menuDismissLabel": "Sulge menüü", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_eu.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_eu.arb index d1882a4259d4e..6d72858ff3d96 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_eu.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_eu.arb @@ -18,9 +18,13 @@ "cutButtonLabel": "Ebaki", "copyButtonLabel": "Kopiatu", "pasteButtonLabel": "Itsatsi", - "selectAllButtonLabel": "Hautatu guztiak", + "selectAllButtonLabel": "Hautatu dena", "tabSemanticsLabel": "$tabIndex/$tabCount fitxa", "modalBarrierDismissLabel": "Baztertu", "searchTextFieldPlaceholderLabel": "Bilatu", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Ez da aurkitu ordezteko hitzik", + "menuDismissLabel": "Baztertu menua", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_fa.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_fa.arb index 63cd8734e083e..a36c89ba40177 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_fa.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_fa.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "برگه $tabIndex از $tabCount", "modalBarrierDismissLabel": "نپذیرفتن", "searchTextFieldPlaceholderLabel": "جستجو", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "جایگزینی پیدا نشد", + "menuDismissLabel": "بستن منو", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_fi.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_fi.arb index dbae7c6bc2777..0c56ce534f27b 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_fi.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_fi.arb @@ -19,8 +19,12 @@ "copyButtonLabel": "Kopioi", "pasteButtonLabel": "Liitä", "selectAllButtonLabel": "Valitse kaikki", - "tabSemanticsLabel": "Välilehti $tabIndex/$tabCount", + "tabSemanticsLabel": "Välilehti $tabIndex kautta $tabCount", "modalBarrierDismissLabel": "Ohita", "searchTextFieldPlaceholderLabel": "Hae", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Korvaavia sanoja ei löydy", + "menuDismissLabel": "Hylkää valikko", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_fil.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_fil.arb index 89396f80a54f4..e983d4d1a58c9 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_fil.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_fil.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex ng $tabCount", "modalBarrierDismissLabel": "I-dismiss", "searchTextFieldPlaceholderLabel": "Hanapin", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Walang Nahanap na Kapalit", + "menuDismissLabel": "I-dismiss ang menu", + "lookUpButtonLabel": "Tumingin sa Itaas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_fr.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_fr.arb index 069b9a19d3c66..57ae553ba1f46 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_fr.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_fr.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Onglet $tabIndex sur $tabCount", "modalBarrierDismissLabel": "Ignorer", "searchTextFieldPlaceholderLabel": "Rechercher", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Aucun remplacement trouvé", + "menuDismissLabel": "Fermer le menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_fr_CA.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_fr_CA.arb index e127661efbd4e..f86963fca218a 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_fr_CA.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_fr_CA.arb @@ -1,4 +1,6 @@ { + "noSpellCheckReplacementsLabel": "Aucun remplacement trouvé", + "menuDismissLabel": "Ignorer le menu", "searchTextFieldPlaceholderLabel": "Rechercher", "tabSemanticsLabel": "Onglet $tabIndex sur $tabCount", "datePickerHourSemanticsLabelOne": "$hour heure", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_gl.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_gl.arb index 4acd2457606cb..2949a2246b0ae 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_gl.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_gl.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Pestana $tabIndex de $tabCount", "modalBarrierDismissLabel": "Ignorar", "searchTextFieldPlaceholderLabel": "Fai unha busca", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Non se encontrou ningunha substitución", + "menuDismissLabel": "Pechar menú", + "lookUpButtonLabel": "Mirar cara arriba", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_gsw.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_gsw.arb index df870f03a041b..5eeefca1c7857 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_gsw.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_gsw.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex von $tabCount", "modalBarrierDismissLabel": "Schließen", "searchTextFieldPlaceholderLabel": "Suche", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Keine Ersetzungen gefunden", + "menuDismissLabel": "Menü schließen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_gu.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_gu.arb index c2b0e8e9a0f79..40f2602774898 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_gu.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_gu.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCountમાંથી $tabIndex ટૅબ", "modalBarrierDismissLabel": "છોડી દો", "searchTextFieldPlaceholderLabel": "શોધો", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "બદલવા માટે કોઈ શબ્દ મળ્યો નથી", + "menuDismissLabel": "મેનૂ છોડી દો", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_he.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_he.arb index 7671ac15dcf25..b2537b0af3836 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_he.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_he.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "כרטיסייה $tabIndex מתוך $tabCount", "modalBarrierDismissLabel": "סגירה", "searchTextFieldPlaceholderLabel": "חיפוש", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "לא נמצאו חלופות", + "menuDismissLabel": "סגירת התפריט", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_hi.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_hi.arb index 4dafff61120c6..d7262f017c029 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_hi.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_hi.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount का टैब $tabIndex", "modalBarrierDismissLabel": "खारिज करें", "searchTextFieldPlaceholderLabel": "खोजें", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "सही वर्तनी वाला कोई शब्द नहीं मिला", + "menuDismissLabel": "मेन्यू खारिज करें", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_hr.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_hr.arb index 3af6a49fa8ccc..9d68b3e925967 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_hr.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_hr.arb @@ -27,5 +27,9 @@ "tabSemanticsLabel": "Kartica $tabIndex od $tabCount", "modalBarrierDismissLabel": "Odbaci", "searchTextFieldPlaceholderLabel": "Pretraživanje", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nema pronađenih zamjena", + "menuDismissLabel": "Odbacivanje izbornika", + "lookUpButtonLabel": "Pogled prema gore", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_hu.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_hu.arb index 3f6c346e50d69..1d91e5b830bab 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_hu.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_hu.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount/$tabIndex. lap", "modalBarrierDismissLabel": "Elvetés", "searchTextFieldPlaceholderLabel": "Keresés", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nem található javítás", + "menuDismissLabel": "Menü bezárása", + "lookUpButtonLabel": "Felfelé nézés", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_hy.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_hy.arb index d381cd1dbaf28..011211fb5c853 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_hy.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_hy.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Ներդիր $tabIndex՝ $tabCount-ից", "modalBarrierDismissLabel": "Փակել", "searchTextFieldPlaceholderLabel": "Որոնում", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Փոխարինումներ չեն գտնվել", + "menuDismissLabel": "Փակել ընտրացանկը", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_id.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_id.arb index 59e1f52852c78..99c0fec3d64be 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_id.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_id.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex dari $tabCount", "modalBarrierDismissLabel": "Tutup", "searchTextFieldPlaceholderLabel": "Telusuri", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Penggantian Tidak Ditemukan", + "menuDismissLabel": "Tutup menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_is.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_is.arb index f9278ad1972a4..f71f0c3f59e92 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_is.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_is.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Flipi $tabIndex af $tabCount", "modalBarrierDismissLabel": "Hunsa", "searchTextFieldPlaceholderLabel": "Leit", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Engir staðgenglar fundust", + "menuDismissLabel": "Loka valmynd", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_it.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_it.arb index 0e81c55e059e1..15c1b1c810288 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_it.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_it.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Scheda $tabIndex di $tabCount", "modalBarrierDismissLabel": "Ignora", "searchTextFieldPlaceholderLabel": "Cerca", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nessuna sostituzione trovata", + "menuDismissLabel": "Ignora menu", + "lookUpButtonLabel": "Cerca", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ja.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ja.arb index 4920f0f4a3905..3fbfe7d9e057e 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ja.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ja.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "タブ: $tabIndex/$tabCount", "modalBarrierDismissLabel": "閉じる", "searchTextFieldPlaceholderLabel": "検索", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "置き換えるものがありません", + "menuDismissLabel": "メニューを閉じる", + "lookUpButtonLabel": "調べる", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ka.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ka.arb index 3859335ba1e24..825a5c44560e6 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ka.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ka.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "ჩანართი $tabIndex / $tabCount-დან", "modalBarrierDismissLabel": "დახურვა", "searchTextFieldPlaceholderLabel": "ძიება", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ჩანაცვლება არ მოიძებნა", + "menuDismissLabel": "მენიუს უარყოფა", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_kk.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_kk.arb index bc86ed2dd7ba0..8501fc704dd09 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_kk.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_kk.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Қойынды: $tabIndex/$tabCount", "modalBarrierDismissLabel": "Жабу", "searchTextFieldPlaceholderLabel": "Іздеу", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Ауыстыратын ешнәрсе табылмады.", + "menuDismissLabel": "Мәзірді жабу", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_km.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_km.arb index aa8dede381325..37da3ecc0dc66 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_km.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_km.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "ផ្ទាំងទី $tabIndex នៃ $tabCount", "modalBarrierDismissLabel": "ច្រាន​ចោល", "searchTextFieldPlaceholderLabel": "ស្វែងរក", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "រកមិនឃើញ​ការជំនួសទេ", + "menuDismissLabel": "ច្រានចោល​ម៉ឺនុយ", + "lookUpButtonLabel": "រកមើល", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_kn.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_kn.arb index 2c1ed6b2451df..ea83d91ff3aed 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_kn.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_kn.arb @@ -22,5 +22,9 @@ "modalBarrierDismissLabel": "\u0cb5\u0c9c\u0cbe\u0c97\u0cca\u0cb3\u0cbf\u0cb8\u0cbf", "tabSemanticsLabel": "\u0024\u0074\u0061\u0062\u0043\u006f\u0075\u006e\u0074\u0020\u0cb0\u0cb2\u0ccd\u0cb2\u0cbf\u0ca8\u0020\u0024\u0074\u0061\u0062\u0049\u006e\u0064\u0065\u0078\u0020\u0c9f\u0ccd\u0caf\u0cbe\u0cac\u0ccd", "searchTextFieldPlaceholderLabel": "\u0cb9\u0cc1\u0ca1\u0cc1\u0c95\u0cbf", - "noSpellCheckReplacementsLabel": "\u004e\u006f\u0020\u0052\u0065\u0070\u006c\u0061\u0063\u0065\u006d\u0065\u006e\u0074\u0073\u0020\u0046\u006f\u0075\u006e\u0064" + "noSpellCheckReplacementsLabel": "\u0caf\u0cbe\u0cb5\u0cc1\u0ca6\u0cc7\u0020\u0cac\u0ca6\u0cb2\u0cbe\u0cb5\u0ca3\u0cc6\u0c97\u0cb3\u0cc1\u0020\u0c95\u0c82\u0ca1\u0cc1\u0cac\u0c82\u0ca6\u0cbf\u0cb2\u0ccd\u0cb2", + "menuDismissLabel": "\u0cae\u0cc6\u0ca8\u0cc1\u0cb5\u0ca8\u0ccd\u0ca8\u0cc1\u0020\u0cb5\u0c9c\u0cbe\u0c97\u0cc6\u0cc2\u0cb3\u0cbf\u0cb8\u0cbf", + "lookUpButtonLabel": "\u004c\u006f\u006f\u006b\u0020\u0055\u0070", + "searchWebButtonLabel": "\u0053\u0065\u0061\u0072\u0063\u0068\u0020\u0057\u0065\u0062", + "shareButtonLabel": "\u0053\u0068\u0061\u0072\u0065\u002e\u002e\u002e" } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ko.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ko.arb index 626331524b117..44e5c3461877b 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ko.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ko.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "탭 $tabCount개 중 $tabIndex번째", "modalBarrierDismissLabel": "닫기", "searchTextFieldPlaceholderLabel": "검색", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "수정사항 없음", + "menuDismissLabel": "메뉴 닫기", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ky.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ky.arb index 7b04435d3ac86..ed73ad35ea05b 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ky.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ky.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount ичинен $tabIndex-өтмөк", "modalBarrierDismissLabel": "Жабуу", "searchTextFieldPlaceholderLabel": "Издөө", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Алмаштыруу үчүн сөз табылган жок", + "menuDismissLabel": "Менюну жабуу", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_lo.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_lo.arb index 811661befc724..8e4b6e0785358 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_lo.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_lo.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "ແຖບທີ $tabIndex ຈາກທັງໝົດ $tabCount", "modalBarrierDismissLabel": "ປິດໄວ້", "searchTextFieldPlaceholderLabel": "ຊອກຫາ", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ບໍ່ພົບການແທນທີ່", + "menuDismissLabel": "ປິດເມນູ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_lt.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_lt.arb index 99bc7283ef72a..b1939982d36e2 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_lt.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_lt.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "$tabIndex skirtukas iš $tabCount", "modalBarrierDismissLabel": "Atsisakyti", "searchTextFieldPlaceholderLabel": "Paieška", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nerasta jokių pakeitimų", + "menuDismissLabel": "Atsisakyti meniu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_lv.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_lv.arb index d9f56156de423..9a7943d06c150 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_lv.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_lv.arb @@ -27,5 +27,9 @@ "tabSemanticsLabel": "$tabIndex. cilne no $tabCount", "modalBarrierDismissLabel": "Nerādīt", "searchTextFieldPlaceholderLabel": "Meklēšana", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Netika atrasts neviens vārds aizstāšanai", + "menuDismissLabel": "Nerādīt izvēlni", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_mk.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_mk.arb index 33a7378629c92..ad88d5b9cec1c 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_mk.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_mk.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Картичка $tabIndex од $tabCount", "modalBarrierDismissLabel": "Отфрли", "searchTextFieldPlaceholderLabel": "Пребарувајте", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Не се најдени заменски зборови", + "menuDismissLabel": "Отфрлете го менито", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ml.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ml.arb index a261c9b2ebe75..da34cc0fe3922 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ml.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ml.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount ടാബിൽ $tabIndex-ാമത്തേത്", "modalBarrierDismissLabel": "നിരസിക്കുക", "searchTextFieldPlaceholderLabel": "തിരയുക", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "റീപ്ലേസ്‌മെന്റുകളൊന്നും കണ്ടെത്തിയില്ല", + "menuDismissLabel": "മെനു ഡിസ്മിസ് ചെയ്യുക", + "lookUpButtonLabel": "മുകളിലേക്ക് നോക്കുക", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_mn.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_mn.arb index a3baf66808a77..b8e4f2872babd 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_mn.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_mn.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount-н $tabIndex-р таб", "modalBarrierDismissLabel": "Үл хэрэгсэх", "searchTextFieldPlaceholderLabel": "Хайх", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Ямар ч орлуулалт олдсонгүй", + "menuDismissLabel": "Цэсийг хаах", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_mr.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_mr.arb index d013b9e4e24a4..fae07c75fbd43 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_mr.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_mr.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount पैकी $tabIndex टॅब", "modalBarrierDismissLabel": "डिसमिस करा", "searchTextFieldPlaceholderLabel": "शोधा", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "कोणतेही बदल आढळले नाहीत", + "menuDismissLabel": "मेनू डिसमिस करा", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ms.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ms.arb index 395f0fdb55285..d38bc94049ea5 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ms.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ms.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex daripada $tabCount", "modalBarrierDismissLabel": "Tolak", "searchTextFieldPlaceholderLabel": "Cari", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Tiada Penggantian Ditemukan", + "menuDismissLabel": "Ketepikan menu", + "lookUpButtonLabel": "Lihat ke Atas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_my.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_my.arb index 1c5dac39cce2e..2f58733fea43b 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_my.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_my.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "တဘ် $tabCount ခုအနက် $tabIndex ခု", "modalBarrierDismissLabel": "ပယ်ရန်", "searchTextFieldPlaceholderLabel": "ရှာရန်", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "အစားထိုးမှုများ မတွေ့ပါ", + "menuDismissLabel": "မီနူးကိုပယ်ပါ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_nb.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_nb.arb index 9e22158f08cf2..5492d2cbe0e28 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_nb.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_nb.arb @@ -22,5 +22,9 @@ "selectAllButtonLabel": "Velg alle", "modalBarrierDismissLabel": "Avvis", "searchTextFieldPlaceholderLabel": "Søk", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Fant ingen erstatninger", + "menuDismissLabel": "Lukk menyen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ne.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ne.arb index 1859d479bbaa2..5738d2472ddbd 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ne.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ne.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount मध्ये $tabIndex ट्याब", "modalBarrierDismissLabel": "खारेज गर्नुहोस्", "searchTextFieldPlaceholderLabel": "खोज्नुहोस्", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "बदल्नु पर्ने कुनै पनि कुरा भेटिएन", + "menuDismissLabel": "मेनु खारेज गर्नुहोस्", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_nl.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_nl.arb index 5aff5981ed8ce..d101c83979bc9 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_nl.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_nl.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tabblad $tabIndex van $tabCount", "modalBarrierDismissLabel": "Sluiten", "searchTextFieldPlaceholderLabel": "Zoeken", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Geen vervangingen gevonden", + "menuDismissLabel": "Menu sluiten", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_no.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_no.arb index 9e22158f08cf2..5492d2cbe0e28 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_no.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_no.arb @@ -22,5 +22,9 @@ "selectAllButtonLabel": "Velg alle", "modalBarrierDismissLabel": "Avvis", "searchTextFieldPlaceholderLabel": "Søk", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Fant ingen erstatninger", + "menuDismissLabel": "Lukk menyen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_or.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_or.arb index 347043ad8d89b..72de165a9ecf1 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_or.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_or.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCountର $tabIndex ଟାବ୍", "modalBarrierDismissLabel": "ଖାରଜ କରନ୍ତୁ", "searchTextFieldPlaceholderLabel": "ସନ୍ଧାନ କରନ୍ତୁ", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "କୌଣସି ରିପ୍ଲେସମେଣ୍ଟ ମିଳିଲା ନାହିଁ", + "menuDismissLabel": "ମେନୁ ଖାରଜ କରନ୍ତୁ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_pa.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_pa.arb index c742ff52fb92b..ad72fbfbf4427 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_pa.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_pa.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount ਵਿੱਚੋਂ $tabIndex ਟੈਬ", "modalBarrierDismissLabel": "ਖਾਰਜ ਕਰੋ", "searchTextFieldPlaceholderLabel": "ਖੋਜੋ", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ਕੋਈ ਸੁਝਾਅ ਨਹੀਂ ਮਿਲਿਆ", + "menuDismissLabel": "ਮੀਨੂ ਖਾਰਜ ਕਰੋ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_pl.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_pl.arb index 19aa5b8299845..36c88c9ebf498 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_pl.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_pl.arb @@ -28,9 +28,13 @@ "cutButtonLabel": "Wytnij", "copyButtonLabel": "Kopiuj", "pasteButtonLabel": "Wklej", - "selectAllButtonLabel": "Zaznacz wszystko", + "selectAllButtonLabel": "Wybierz wszystkie", "tabSemanticsLabel": "Karta $tabIndex z $tabCount", "modalBarrierDismissLabel": "Zamknij", "searchTextFieldPlaceholderLabel": "Szukaj", - "noSpellCheckReplacementsLabel": "Nie znaleziono zastąpień" + "noSpellCheckReplacementsLabel": "Brak wyników zamieniania", + "menuDismissLabel": "Zamknij menu", + "lookUpButtonLabel": "Sprawdź", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_pt.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_pt.arb index 23bf41fc61338..29c1cf87062df 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_pt.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_pt.arb @@ -18,9 +18,13 @@ "cutButtonLabel": "Cortar", "copyButtonLabel": "Copiar", "pasteButtonLabel": "Colar", - "selectAllButtonLabel": "Selecionar tudo", + "selectAllButtonLabel": "Selecionar Tudo", "tabSemanticsLabel": "Guia $tabIndex de $tabCount", "modalBarrierDismissLabel": "Dispensar", "searchTextFieldPlaceholderLabel": "Pesquisar", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nenhuma alternativa encontrada", + "menuDismissLabel": "Dispensar menu", + "lookUpButtonLabel": "Pesquisar", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_pt_PT.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_pt_PT.arb index 8b9610777212d..986e169d53cc4 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_pt_PT.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_pt_PT.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "Procurar", + "noSpellCheckReplacementsLabel": "Não foram encontradas substituições", + "menuDismissLabel": "Ignorar menu", "searchTextFieldPlaceholderLabel": "Pesquise", "tabSemanticsLabel": "Separador $tabIndex de $tabCount", "datePickerHourSemanticsLabelOne": "$hour hora", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ro.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ro.arb index 3086731d34fa3..e78637cf5ee65 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ro.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ro.arb @@ -27,5 +27,9 @@ "tabSemanticsLabel": "Fila $tabIndex din $tabCount", "modalBarrierDismissLabel": "Închideți", "searchTextFieldPlaceholderLabel": "Căutați", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nu s-au găsit înlocuiri", + "menuDismissLabel": "Respingeți meniul", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ru.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ru.arb index 31db898a91566..c0e93bea0954e 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ru.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ru.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Вкладка $tabIndex из $tabCount", "modalBarrierDismissLabel": "Закрыть", "searchTextFieldPlaceholderLabel": "Поиск", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Варианты замены не найдены", + "menuDismissLabel": "Закрыть меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_si.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_si.arb index be2d85f0ccd8d..35a98c19b7120 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_si.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_si.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "ටැබ $tabCount න් $tabIndex", "modalBarrierDismissLabel": "ඉවත ලන්න", "searchTextFieldPlaceholderLabel": "සෙවීම", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ප්‍රතිස්ථාපන හමු නොවිණි", + "menuDismissLabel": "මෙනුව අස් කරන්න", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sk.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sk.arb index a65dc24aa8766..6bce1b40316a1 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sk.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sk.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Karta $tabIndex z $tabCount", "modalBarrierDismissLabel": "Odmietnuť", "searchTextFieldPlaceholderLabel": "Hľadať", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nenašli sa žiadne náhrady", + "menuDismissLabel": "Zavrieť ponuku", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sl.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sl.arb index 48ddd51c66be4..466533c382c2c 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sl.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sl.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Zavihek $tabIndex od $tabCount", "modalBarrierDismissLabel": "Opusti", "searchTextFieldPlaceholderLabel": "Iskanje", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Ni zamenjav", + "menuDismissLabel": "Opusti meni", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sq.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sq.arb index c5f6f60d9acd4..f7f2ba5de95ce 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sq.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sq.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Skeda $tabIndex nga $tabCount", "modalBarrierDismissLabel": "Hiq", "searchTextFieldPlaceholderLabel": "Kërko", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Nuk u gjetën zëvendësime", + "menuDismissLabel": "Hiqe menynë", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sr.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sr.arb index 1883096dfce51..da22e63e74863 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sr.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sr.arb @@ -27,5 +27,9 @@ "tabSemanticsLabel": "$tabIndex. картица од $tabCount", "modalBarrierDismissLabel": "Одбаци", "searchTextFieldPlaceholderLabel": "Претражите", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Нису пронађене замене", + "menuDismissLabel": "Одбаците мени", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sr_Latn.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sr_Latn.arb index 3adac6d344c05..02c2db4f38591 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sr_Latn.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sr_Latn.arb @@ -1,4 +1,6 @@ { + "noSpellCheckReplacementsLabel": "Nisu pronađene zamene", + "menuDismissLabel": "Odbacite meni", "searchTextFieldPlaceholderLabel": "Pretražite", "tabSemanticsLabel": "$tabIndex. kartica od $tabCount", "datePickerHourSemanticsLabelFew": "$hour sata", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sv.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sv.arb index 8b6aa26ee1720..3fceee58ba699 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sv.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sv.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Flik $tabIndex av $tabCount", "modalBarrierDismissLabel": "Stäng", "searchTextFieldPlaceholderLabel": "Sök", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Inga ersättningar hittades", + "menuDismissLabel": "Stäng menyn", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_sw.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_sw.arb index 76e7eaee96863..3836a6c61d859 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_sw.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_sw.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Kichupo cha $tabIndex kati ya $tabCount", "modalBarrierDismissLabel": "Ondoa", "searchTextFieldPlaceholderLabel": "Tafuta", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Hakuna Neno Mbadala Lilopatikana", + "menuDismissLabel": "Ondoa menyu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ta.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ta.arb index a1d7a1d808af9..fb43d1bc64700 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ta.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ta.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "தாவல் $tabIndex / $tabCount", "modalBarrierDismissLabel": "நிராகரிக்கும்", "searchTextFieldPlaceholderLabel": "தேடுக", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "மாற்று வார்த்தைகள் கிடைக்கவில்லை", + "menuDismissLabel": "மெனுவை மூடும்", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_te.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_te.arb index 80ce7032be3d6..0e53a7e958255 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_te.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_te.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCountలో $tabIndexవ ట్యాబ్", "modalBarrierDismissLabel": "విస్మరించు", "searchTextFieldPlaceholderLabel": "సెర్చ్ చేయి", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "రీప్లేస్‌మెంట్‌లు ఏవీ కనుగొనబడలేదు", + "menuDismissLabel": "మెనూను తీసివేయండి", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_th.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_th.arb index f69e1a3483332..c6699874372f3 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_th.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_th.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "แท็บที่ $tabIndex จาก $tabCount", "modalBarrierDismissLabel": "ปิด", "searchTextFieldPlaceholderLabel": "ค้นหา", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "ไม่พบรายการแทนที่", + "menuDismissLabel": "ปิดเมนู", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_tl.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_tl.arb index 89396f80a54f4..e983d4d1a58c9 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_tl.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_tl.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Tab $tabIndex ng $tabCount", "modalBarrierDismissLabel": "I-dismiss", "searchTextFieldPlaceholderLabel": "Hanapin", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Walang Nahanap na Kapalit", + "menuDismissLabel": "I-dismiss ang menu", + "lookUpButtonLabel": "Tumingin sa Itaas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_tr.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_tr.arb index 0bad769433078..59bfa3eccf53e 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_tr.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_tr.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Sekme $tabIndex/$tabCount", "modalBarrierDismissLabel": "Kapat", "searchTextFieldPlaceholderLabel": "Ara", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Yerine Kelime Bulunamadı", + "menuDismissLabel": "Menüyü kapat", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_uk.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_uk.arb index c10dfff0a25c1..6f50ab997fb9d 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_uk.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_uk.arb @@ -32,5 +32,9 @@ "tabSemanticsLabel": "Вкладка $tabIndex з $tabCount", "modalBarrierDismissLabel": "Закрити", "searchTextFieldPlaceholderLabel": "Шукайте", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Замін не знайдено", + "menuDismissLabel": "Закрити меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_ur.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_ur.arb index 99561dbfe7968..ca0282cc6d285 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_ur.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_ur.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount میں سے $tabIndex ٹیب", "modalBarrierDismissLabel": "برخاست کریں", "searchTextFieldPlaceholderLabel": "تلاش کریں", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "کوئی تبدیلیاں نہیں ملیں", + "menuDismissLabel": "مینو برخاست کریں", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_uz.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_uz.arb index 438689b045a8f..86fd969dab4f8 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_uz.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_uz.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "$tabCount varaqdan $tabIndex", "modalBarrierDismissLabel": "Yopish", "searchTextFieldPlaceholderLabel": "Qidiruv", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Almashtirish uchun soʻz topilmadi", + "menuDismissLabel": "Menyuni yopish", + "lookUpButtonLabel": "Tepaga qarang", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_vi.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_vi.arb index 0f7714ae31c03..4b8277007bfd0 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_vi.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_vi.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Thẻ $tabIndex/$tabCount", "modalBarrierDismissLabel": "Bỏ qua", "searchTextFieldPlaceholderLabel": "Tìm kiếm", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Không tìm thấy phương án thay thế", + "menuDismissLabel": "Đóng trình đơn", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_zh.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_zh.arb index e90433429d058..98f18f4ab1237 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_zh.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_zh.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "第 $tabIndex 个标签,共 $tabCount 个", "modalBarrierDismissLabel": "关闭", "searchTextFieldPlaceholderLabel": "搜索", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "找不到替换文字", + "menuDismissLabel": "关闭菜单", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_zh_HK.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_zh_HK.arb index 4f518784300e8..5cac05b161314 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_zh_HK.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_zh_HK.arb @@ -1,4 +1,7 @@ { + "lookUpButtonLabel": "查詢", + "noSpellCheckReplacementsLabel": "找不到替換字詞", + "menuDismissLabel": "閂選單", "searchTextFieldPlaceholderLabel": "搜尋", "tabSemanticsLabel": "$tabCount 個分頁中嘅第 $tabIndex 個", "datePickerHourSemanticsLabelOne": "$hour 點", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_zh_TW.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_zh_TW.arb index a3dd35c7cee2e..2c1190da6c0fc 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_zh_TW.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_zh_TW.arb @@ -1,4 +1,6 @@ { + "noSpellCheckReplacementsLabel": "找不到替代文字", + "menuDismissLabel": "關閉選單", "searchTextFieldPlaceholderLabel": "搜尋", "tabSemanticsLabel": "第 $tabIndex 個分頁標籤,共 $tabCount 個", "datePickerHourSemanticsLabelOne": "$hour 點", diff --git a/packages/flutter_localizations/lib/src/l10n/cupertino_zu.arb b/packages/flutter_localizations/lib/src/l10n/cupertino_zu.arb index 57dbb148fdc01..6e37c0fa0e0a8 100644 --- a/packages/flutter_localizations/lib/src/l10n/cupertino_zu.arb +++ b/packages/flutter_localizations/lib/src/l10n/cupertino_zu.arb @@ -22,5 +22,9 @@ "tabSemanticsLabel": "Ithebhu $tabIndex kwangu-$tabCount", "modalBarrierDismissLabel": "Cashisa", "searchTextFieldPlaceholderLabel": "Sesha", - "noSpellCheckReplacementsLabel": "No Replacements Found" + "noSpellCheckReplacementsLabel": "Akukho Okuzofakwa Esikhundleni Okutholakele", + "menuDismissLabel": "Chitha imenyu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart b/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart index 90a899f0a3d57..4b584dcc61eb5 100644 --- a/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart +++ b/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart @@ -91,11 +91,17 @@ class CupertinoLocalizationAf extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Maak kieslys toe'; + @override String get modalBarrierDismissLabel => 'Maak toe'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Geen plaasvervangers gevind nie'; @override String get pasteButtonLabel => 'Plak'; @@ -106,9 +112,15 @@ class CupertinoLocalizationAf extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Soek'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Kies alles'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Oortjie $tabIndex van $tabCount'; @@ -241,11 +253,17 @@ class CupertinoLocalizationAm extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'ምናሌን አሰናብት'; + @override String get modalBarrierDismissLabel => 'አሰናብት'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ምንም ተተኪዎች አልተገኙም'; @override String get pasteButtonLabel => 'ለጥፍ'; @@ -256,9 +274,15 @@ class CupertinoLocalizationAm extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ፍለጋ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ሁሉንም ምረጥ'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'ትር $tabIndex ከ$tabCount'; @@ -391,11 +415,17 @@ class CupertinoLocalizationAr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => r'$minute دقيقة​'; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'إغلاق القائمة'; + @override String get modalBarrierDismissLabel => 'رفض'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'لم يتم العثور على بدائل'; @override String get pasteButtonLabel => 'لصق'; @@ -406,9 +436,15 @@ class CupertinoLocalizationAr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'بحث'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'اختيار الكل'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'علامة التبويب $tabIndex من $tabCount'; @@ -541,11 +577,17 @@ class CupertinoLocalizationAs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'অগ্ৰাহ্য কৰাৰ মেনু'; + @override String get modalBarrierDismissLabel => 'অগ্ৰাহ্য কৰক'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'এইটোৰ সলনি ব্যৱহাৰ কৰিব পৰা শব্দ পোৱা নগ’ল'; @override String get pasteButtonLabel => "পে'ষ্ট কৰক"; @@ -556,9 +598,15 @@ class CupertinoLocalizationAs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'সন্ধান কৰক'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'সকলো বাছনি কৰক'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount টা টেবৰ $tabIndex নম্বৰটো'; @@ -691,11 +739,17 @@ class CupertinoLocalizationAz extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Menyunu qapadın'; + @override String get modalBarrierDismissLabel => 'İmtina edin'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Əvəzləmə Tapılmadı'; @override String get pasteButtonLabel => 'Yerləşdirin'; @@ -706,9 +760,15 @@ class CupertinoLocalizationAz extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Axtarın'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Hamısını seçin'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex/$tabCount'; @@ -841,11 +901,17 @@ class CupertinoLocalizationBe extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Закрыць меню'; + @override String get modalBarrierDismissLabel => 'Адхіліць'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Замен не знойдзена'; @override String get pasteButtonLabel => 'Уставіць'; @@ -856,9 +922,15 @@ class CupertinoLocalizationBe extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Пошук'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Выбраць усе'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Укладка $tabIndex з $tabCount'; @@ -991,11 +1063,17 @@ class CupertinoLocalizationBg extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Отхвърляне на менюто'; + @override String get modalBarrierDismissLabel => 'Отхвърляне'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Не бяха намерени замествания'; @override String get pasteButtonLabel => 'Поставяне'; @@ -1006,9 +1084,15 @@ class CupertinoLocalizationBg extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Търсене'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Избиране на всички'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Раздел $tabIndex от $tabCount'; @@ -1141,11 +1225,17 @@ class CupertinoLocalizationBn extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'বাতিল করার মেনু'; + @override String get modalBarrierDismissLabel => 'খারিজ করুন'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'কোনও বিকল্প বানান দেখানো হয়নি'; @override String get pasteButtonLabel => 'পেস্ট করুন'; @@ -1156,9 +1246,15 @@ class CupertinoLocalizationBn extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'সার্চ করুন'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'সব বেছে নিন'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount-এর মধ্যে $tabIndex নম্বর ট্যাব'; @@ -1291,11 +1387,17 @@ class CupertinoLocalizationBs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Pogled prema gore'; + + @override + String get menuDismissLabel => 'Odbacivanje menija'; + @override String get modalBarrierDismissLabel => 'Odbaci'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nije pronađena nijedna zamjena'; @override String get pasteButtonLabel => 'Zalijepi'; @@ -1306,9 +1408,15 @@ class CupertinoLocalizationBs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Pretraživanje'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Odaberi sve'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Kartica $tabIndex od $tabCount'; @@ -1441,11 +1549,17 @@ class CupertinoLocalizationCa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Ignora el menú'; + @override String get modalBarrierDismissLabel => 'Ignora'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => "No s'ha trobat cap substitució"; @override String get pasteButtonLabel => 'Enganxa'; @@ -1456,9 +1570,15 @@ class CupertinoLocalizationCa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Cerca'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Selecciona-ho tot'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Pestanya $tabIndex de $tabCount'; @@ -1591,11 +1711,17 @@ class CupertinoLocalizationCs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Zavřít nabídku'; + @override String get modalBarrierDismissLabel => 'Zavřít'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Žádná nahrazení nenalezena'; @override String get pasteButtonLabel => 'Vložit'; @@ -1606,9 +1732,15 @@ class CupertinoLocalizationCs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Hledat'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vybrat vše'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Karta $tabIndex z $tabCount'; @@ -1741,11 +1873,17 @@ class CupertinoLocalizationCy extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => r'$minute munud'; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => "Diystyru'r ddewislen"; + @override String get modalBarrierDismissLabel => 'Diystyru'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => "Dim Ailosodiadau wedi'u Canfod"; @override String get pasteButtonLabel => 'Gludo'; @@ -1756,9 +1894,15 @@ class CupertinoLocalizationCy extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Chwilio'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Dewis y Cyfan'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex o $tabCount'; @@ -1891,11 +2035,17 @@ class CupertinoLocalizationDa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Luk menu'; + @override String get modalBarrierDismissLabel => 'Afvis'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Der blev ikke fundet nogen erstatninger'; @override String get pasteButtonLabel => 'Indsæt'; @@ -1906,9 +2056,15 @@ class CupertinoLocalizationDa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Søg'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vælg alle'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Fane $tabIndex af $tabCount'; @@ -2041,11 +2197,17 @@ class CupertinoLocalizationDe extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Menü schließen'; + @override String get modalBarrierDismissLabel => 'Schließen'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Keine Ersetzungen gefunden'; @override String get pasteButtonLabel => 'Einsetzen'; @@ -2056,9 +2218,15 @@ class CupertinoLocalizationDe extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Suche'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alles auswählen'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex von $tabCount'; @@ -2212,11 +2380,17 @@ class CupertinoLocalizationEl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Παράβλεψη μενού'; + @override String get modalBarrierDismissLabel => 'Παράβλεψη'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Δεν βρέθηκαν αντικαταστάσεις'; @override String get pasteButtonLabel => 'Επικόλληση'; @@ -2227,9 +2401,15 @@ class CupertinoLocalizationEl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Αναζήτηση'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Επιλογή όλων'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Καρτέλα $tabIndex από $tabCount'; @@ -2362,6 +2542,12 @@ class CupertinoLocalizationEn extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Dismiss menu'; + @override String get modalBarrierDismissLabel => 'Dismiss'; @@ -2377,9 +2563,15 @@ class CupertinoLocalizationEn extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Search'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Select All'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex of $tabCount'; @@ -2458,6 +2650,12 @@ class CupertinoLocalizationEnAu extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2506,6 +2704,12 @@ class CupertinoLocalizationEnGb extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2530,6 +2734,12 @@ class CupertinoLocalizationEnIe extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2554,6 +2764,12 @@ class CupertinoLocalizationEnIn extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2578,6 +2794,12 @@ class CupertinoLocalizationEnNz extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2602,6 +2824,12 @@ class CupertinoLocalizationEnSg extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2626,6 +2854,12 @@ class CupertinoLocalizationEnZa extends CupertinoLocalizationEn { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get noSpellCheckReplacementsLabel => 'No replacements found'; + @override String get datePickerDateOrderString => 'dmy'; @@ -2704,11 +2938,17 @@ class CupertinoLocalizationEs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Cerrar menú'; + @override String get modalBarrierDismissLabel => 'Cerrar'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'No se ha encontrado ninguna sustitución'; @override String get pasteButtonLabel => 'Pegar'; @@ -2719,9 +2959,15 @@ class CupertinoLocalizationEs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Buscar'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleccionar todo'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Pestaña $tabIndex de $tabCount'; @@ -2800,6 +3046,15 @@ class CupertinoLocalizationEs419 extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2812,6 +3067,9 @@ class CupertinoLocalizationEs419 extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2833,6 +3091,15 @@ class CupertinoLocalizationEsAr extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2845,6 +3112,9 @@ class CupertinoLocalizationEsAr extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2866,6 +3136,15 @@ class CupertinoLocalizationEsBo extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2878,6 +3157,9 @@ class CupertinoLocalizationEsBo extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2899,6 +3181,15 @@ class CupertinoLocalizationEsCl extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2911,6 +3202,9 @@ class CupertinoLocalizationEsCl extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2932,6 +3226,15 @@ class CupertinoLocalizationEsCo extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2944,6 +3247,9 @@ class CupertinoLocalizationEsCo extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2965,6 +3271,15 @@ class CupertinoLocalizationEsCr extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -2977,6 +3292,9 @@ class CupertinoLocalizationEsCr extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -2998,6 +3316,15 @@ class CupertinoLocalizationEsDo extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3010,6 +3337,9 @@ class CupertinoLocalizationEsDo extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3031,6 +3361,15 @@ class CupertinoLocalizationEsEc extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3043,6 +3382,9 @@ class CupertinoLocalizationEsEc extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3064,6 +3406,15 @@ class CupertinoLocalizationEsGt extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3076,6 +3427,9 @@ class CupertinoLocalizationEsGt extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3097,6 +3451,15 @@ class CupertinoLocalizationEsHn extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3109,6 +3472,9 @@ class CupertinoLocalizationEsHn extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3130,6 +3496,15 @@ class CupertinoLocalizationEsMx extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3142,6 +3517,9 @@ class CupertinoLocalizationEsMx extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3163,6 +3541,15 @@ class CupertinoLocalizationEsNi extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3175,6 +3562,9 @@ class CupertinoLocalizationEsNi extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3196,6 +3586,15 @@ class CupertinoLocalizationEsPa extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3208,6 +3607,9 @@ class CupertinoLocalizationEsPa extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3229,6 +3631,15 @@ class CupertinoLocalizationEsPe extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3241,6 +3652,9 @@ class CupertinoLocalizationEsPe extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3262,6 +3676,15 @@ class CupertinoLocalizationEsPr extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3274,6 +3697,9 @@ class CupertinoLocalizationEsPr extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3295,6 +3721,15 @@ class CupertinoLocalizationEsPy extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3307,6 +3742,9 @@ class CupertinoLocalizationEsPy extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3328,6 +3766,15 @@ class CupertinoLocalizationEsSv extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3340,6 +3787,9 @@ class CupertinoLocalizationEsSv extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3361,6 +3811,15 @@ class CupertinoLocalizationEsUs extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3373,6 +3832,9 @@ class CupertinoLocalizationEsUs extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3394,6 +3856,15 @@ class CupertinoLocalizationEsUy extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3406,6 +3877,9 @@ class CupertinoLocalizationEsUy extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3427,6 +3901,15 @@ class CupertinoLocalizationEsVe extends CupertinoLocalizationEs { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get noSpellCheckReplacementsLabel => 'No se encontraron reemplazos'; + + @override + String get menuDismissLabel => 'Descartar menú'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour en punto'; @@ -3439,6 +3922,9 @@ class CupertinoLocalizationEsVe extends CupertinoLocalizationEs { @override String get postMeridiemAbbreviation => 'p.m.'; + @override + String get selectAllButtonLabel => 'Seleccionar todos'; + @override String get modalBarrierDismissLabel => 'Descartar'; } @@ -3514,11 +4000,17 @@ class CupertinoLocalizationEt extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Sulge menüü'; + @override String get modalBarrierDismissLabel => 'Loobu'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Asendusi ei leitud'; @override String get pasteButtonLabel => 'Kleebi'; @@ -3529,9 +4021,15 @@ class CupertinoLocalizationEt extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Otsige'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vali kõik'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabIndex. vaheleht $tabCount-st'; @@ -3664,11 +4162,17 @@ class CupertinoLocalizationEu extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Baztertu menua'; + @override String get modalBarrierDismissLabel => 'Baztertu'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Ez da aurkitu ordezteko hitzik'; @override String get pasteButtonLabel => 'Itsatsi'; @@ -3680,7 +4184,13 @@ class CupertinoLocalizationEu extends GlobalCupertinoLocalizations { String get searchTextFieldPlaceholderLabel => 'Bilatu'; @override - String get selectAllButtonLabel => 'Hautatu guztiak'; + String get searchWebButtonLabel => 'Search Web'; + + @override + String get selectAllButtonLabel => 'Hautatu dena'; + + @override + String get shareButtonLabel => 'Share...'; @override String get tabSemanticsLabelRaw => r'$tabIndex/$tabCount fitxa'; @@ -3814,11 +4324,17 @@ class CupertinoLocalizationFa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'بستن منو'; + @override String get modalBarrierDismissLabel => 'نپذیرفتن'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'جایگزینی پیدا نشد'; @override String get pasteButtonLabel => 'جای‌گذاری'; @@ -3829,9 +4345,15 @@ class CupertinoLocalizationFa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'جستجو'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'انتخاب همه'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'برگه $tabIndex از $tabCount'; @@ -3964,11 +4486,17 @@ class CupertinoLocalizationFi extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Hylkää valikko'; + @override String get modalBarrierDismissLabel => 'Ohita'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Korvaavia sanoja ei löydy'; @override String get pasteButtonLabel => 'Liitä'; @@ -3979,11 +4507,17 @@ class CupertinoLocalizationFi extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Hae'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Valitse kaikki'; @override - String get tabSemanticsLabelRaw => r'Välilehti $tabIndex/$tabCount'; + String get shareButtonLabel => 'Share...'; + + @override + String get tabSemanticsLabelRaw => r'Välilehti $tabIndex kautta $tabCount'; @override String? get timerPickerHourLabelFew => null; @@ -4114,11 +4648,17 @@ class CupertinoLocalizationFil extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Tumingin sa Itaas'; + + @override + String get menuDismissLabel => 'I-dismiss ang menu'; + @override String get modalBarrierDismissLabel => 'I-dismiss'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Walang Nahanap na Kapalit'; @override String get pasteButtonLabel => 'I-paste'; @@ -4129,9 +4669,15 @@ class CupertinoLocalizationFil extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Hanapin'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Piliin Lahat'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex ng $tabCount'; @@ -4264,11 +4810,17 @@ class CupertinoLocalizationFr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Fermer le menu'; + @override String get modalBarrierDismissLabel => 'Ignorer'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Aucun remplacement trouvé'; @override String get pasteButtonLabel => 'Coller'; @@ -4279,9 +4831,15 @@ class CupertinoLocalizationFr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Rechercher'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Tout sélect.'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Onglet $tabIndex sur $tabCount'; @@ -4360,6 +4918,9 @@ class CupertinoLocalizationFrCa extends CupertinoLocalizationFr { required super.decimalFormat, }); + @override + String get menuDismissLabel => 'Ignorer le menu'; + @override String? get datePickerHourSemanticsLabelOne => r'$hour heure'; @@ -4456,11 +5017,17 @@ class CupertinoLocalizationGl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Mirar cara arriba'; + + @override + String get menuDismissLabel => 'Pechar menú'; + @override String get modalBarrierDismissLabel => 'Ignorar'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Non se encontrou ningunha substitución'; @override String get pasteButtonLabel => 'Pegar'; @@ -4471,9 +5038,15 @@ class CupertinoLocalizationGl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Fai unha busca'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleccionar todo'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Pestana $tabIndex de $tabCount'; @@ -4601,16 +5174,22 @@ class CupertinoLocalizationGsw extends GlobalCupertinoLocalizations { String get datePickerMinuteSemanticsLabelOther => r'$minute Minuten'; @override - String? get datePickerMinuteSemanticsLabelTwo => null; + String? get datePickerMinuteSemanticsLabelTwo => null; + + @override + String? get datePickerMinuteSemanticsLabelZero => null; + + @override + String get lookUpButtonLabel => 'Look Up'; @override - String? get datePickerMinuteSemanticsLabelZero => null; + String get menuDismissLabel => 'Menü schließen'; @override String get modalBarrierDismissLabel => 'Schließen'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Keine Ersetzungen gefunden'; @override String get pasteButtonLabel => 'Einsetzen'; @@ -4621,9 +5200,15 @@ class CupertinoLocalizationGsw extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Suche'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alles auswählen'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex von $tabCount'; @@ -4756,11 +5341,17 @@ class CupertinoLocalizationGu extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'મેનૂ છોડી દો'; + @override String get modalBarrierDismissLabel => 'છોડી દો'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'બદલવા માટે કોઈ શબ્દ મળ્યો નથી'; @override String get pasteButtonLabel => 'પેસ્ટ કરો'; @@ -4771,9 +5362,15 @@ class CupertinoLocalizationGu extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'શોધો'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'બધા પસંદ કરો'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCountમાંથી $tabIndex ટૅબ'; @@ -4906,11 +5503,17 @@ class CupertinoLocalizationHe extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'סגירת התפריט'; + @override String get modalBarrierDismissLabel => 'סגירה'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'לא נמצאו חלופות'; @override String get pasteButtonLabel => 'הדבקה'; @@ -4921,9 +5524,15 @@ class CupertinoLocalizationHe extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'חיפוש'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'בחירת הכול'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'כרטיסייה $tabIndex מתוך $tabCount'; @@ -5056,11 +5665,17 @@ class CupertinoLocalizationHi extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'मेन्यू खारिज करें'; + @override String get modalBarrierDismissLabel => 'खारिज करें'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'सही वर्तनी वाला कोई शब्द नहीं मिला'; @override String get pasteButtonLabel => 'चिपकाएं'; @@ -5071,9 +5686,15 @@ class CupertinoLocalizationHi extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'खोजें'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सभी चुनें'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount का टैब $tabIndex'; @@ -5206,11 +5827,17 @@ class CupertinoLocalizationHr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Pogled prema gore'; + + @override + String get menuDismissLabel => 'Odbacivanje izbornika'; + @override String get modalBarrierDismissLabel => 'Odbaci'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nema pronađenih zamjena'; @override String get pasteButtonLabel => 'Zalijepi'; @@ -5221,9 +5848,15 @@ class CupertinoLocalizationHr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Pretraživanje'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Odaberi sve'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Kartica $tabIndex od $tabCount'; @@ -5356,11 +5989,17 @@ class CupertinoLocalizationHu extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Felfelé nézés'; + + @override + String get menuDismissLabel => 'Menü bezárása'; + @override String get modalBarrierDismissLabel => 'Elvetés'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nem található javítás'; @override String get pasteButtonLabel => 'Beillesztés'; @@ -5371,9 +6010,15 @@ class CupertinoLocalizationHu extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Keresés'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Összes kijelölése'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount/$tabIndex. lap'; @@ -5506,11 +6151,17 @@ class CupertinoLocalizationHy extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Փակել ընտրացանկը'; + @override String get modalBarrierDismissLabel => 'Փակել'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Փոխարինումներ չեն գտնվել'; @override String get pasteButtonLabel => 'Տեղադրել'; @@ -5521,9 +6172,15 @@ class CupertinoLocalizationHy extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Որոնում'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Նշել բոլորը'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Ներդիր $tabIndex՝ $tabCount-ից'; @@ -5656,11 +6313,17 @@ class CupertinoLocalizationId extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Tutup menu'; + @override String get modalBarrierDismissLabel => 'Tutup'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Penggantian Tidak Ditemukan'; @override String get pasteButtonLabel => 'Tempel'; @@ -5671,9 +6334,15 @@ class CupertinoLocalizationId extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Telusuri'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pilih Semua'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex dari $tabCount'; @@ -5806,11 +6475,17 @@ class CupertinoLocalizationIs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Loka valmynd'; + @override String get modalBarrierDismissLabel => 'Hunsa'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Engir staðgenglar fundust'; @override String get pasteButtonLabel => 'Líma'; @@ -5821,9 +6496,15 @@ class CupertinoLocalizationIs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Leit'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velja allt'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Flipi $tabIndex af $tabCount'; @@ -5956,11 +6637,17 @@ class CupertinoLocalizationIt extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Cerca'; + + @override + String get menuDismissLabel => 'Ignora menu'; + @override String get modalBarrierDismissLabel => 'Ignora'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nessuna sostituzione trovata'; @override String get pasteButtonLabel => 'Incolla'; @@ -5971,9 +6658,15 @@ class CupertinoLocalizationIt extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Cerca'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleziona tutto'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Scheda $tabIndex di $tabCount'; @@ -6106,11 +6799,17 @@ class CupertinoLocalizationJa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => '調べる'; + + @override + String get menuDismissLabel => 'メニューを閉じる'; + @override String get modalBarrierDismissLabel => '閉じる'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => '置き換えるものがありません'; @override String get pasteButtonLabel => '貼り付け'; @@ -6121,9 +6820,15 @@ class CupertinoLocalizationJa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => '検索'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'すべて選択'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'タブ: $tabIndex/$tabCount'; @@ -6256,11 +6961,17 @@ class CupertinoLocalizationKa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'მენიუს უარყოფა'; + @override String get modalBarrierDismissLabel => 'დახურვა'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ჩანაცვლება არ მოიძებნა'; @override String get pasteButtonLabel => 'ჩასმა'; @@ -6271,9 +6982,15 @@ class CupertinoLocalizationKa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ძიება'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ყველას არჩევა'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'ჩანართი $tabIndex / $tabCount-დან'; @@ -6406,11 +7123,17 @@ class CupertinoLocalizationKk extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Мәзірді жабу'; + @override String get modalBarrierDismissLabel => 'Жабу'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Ауыстыратын ешнәрсе табылмады.'; @override String get pasteButtonLabel => 'Қою'; @@ -6421,9 +7144,15 @@ class CupertinoLocalizationKk extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Іздеу'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Барлығын таңдау'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Қойынды: $tabIndex/$tabCount'; @@ -6556,11 +7285,17 @@ class CupertinoLocalizationKm extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'រកមើល'; + + @override + String get menuDismissLabel => 'ច្រានចោល​ម៉ឺនុយ'; + @override String get modalBarrierDismissLabel => 'ច្រាន​ចោល'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'រកមិនឃើញ​ការជំនួសទេ'; @override String get pasteButtonLabel => 'ដាក់​ចូល'; @@ -6571,9 +7306,15 @@ class CupertinoLocalizationKm extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ស្វែងរក'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ជ្រើសរើស​ទាំងអស់'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'ផ្ទាំងទី $tabIndex នៃ $tabCount'; @@ -6706,11 +7447,17 @@ class CupertinoLocalizationKn extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => '\u{cae}\u{cc6}\u{ca8}\u{cc1}\u{cb5}\u{ca8}\u{ccd}\u{ca8}\u{cc1}\u{20}\u{cb5}\u{c9c}\u{cbe}\u{c97}\u{cc6}\u{cc2}\u{cb3}\u{cbf}\u{cb8}\u{cbf}'; + @override String get modalBarrierDismissLabel => '\u{cb5}\u{c9c}\u{cbe}\u{c97}\u{cca}\u{cb3}\u{cbf}\u{cb8}\u{cbf}'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => '\u{caf}\u{cbe}\u{cb5}\u{cc1}\u{ca6}\u{cc7}\u{20}\u{cac}\u{ca6}\u{cb2}\u{cbe}\u{cb5}\u{ca3}\u{cc6}\u{c97}\u{cb3}\u{cc1}\u{20}\u{c95}\u{c82}\u{ca1}\u{cc1}\u{cac}\u{c82}\u{ca6}\u{cbf}\u{cb2}\u{ccd}\u{cb2}'; @override String get pasteButtonLabel => '\u{c85}\u{c82}\u{c9f}\u{cbf}\u{cb8}\u{cbf}'; @@ -6721,9 +7468,15 @@ class CupertinoLocalizationKn extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => '\u{cb9}\u{cc1}\u{ca1}\u{cc1}\u{c95}\u{cbf}'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '\u{c8e}\u{cb2}\u{ccd}\u{cb2}\u{cb5}\u{ca8}\u{ccd}\u{ca8}\u{cc2}\u{20}\u{c86}\u{caf}\u{ccd}\u{c95}\u{cc6}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => '\u{24}\u{74}\u{61}\u{62}\u{43}\u{6f}\u{75}\u{6e}\u{74}\u{20}\u{cb0}\u{cb2}\u{ccd}\u{cb2}\u{cbf}\u{ca8}\u{20}\u{24}\u{74}\u{61}\u{62}\u{49}\u{6e}\u{64}\u{65}\u{78}\u{20}\u{c9f}\u{ccd}\u{caf}\u{cbe}\u{cac}\u{ccd}'; @@ -6856,11 +7609,17 @@ class CupertinoLocalizationKo extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => '메뉴 닫기'; + @override String get modalBarrierDismissLabel => '닫기'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => '수정사항 없음'; @override String get pasteButtonLabel => '붙여넣기'; @@ -6871,9 +7630,15 @@ class CupertinoLocalizationKo extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => '검색'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '전체 선택'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'탭 $tabCount개 중 $tabIndex번째'; @@ -7006,11 +7771,17 @@ class CupertinoLocalizationKy extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Менюну жабуу'; + @override String get modalBarrierDismissLabel => 'Жабуу'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Алмаштыруу үчүн сөз табылган жок'; @override String get pasteButtonLabel => 'Чаптоо'; @@ -7021,9 +7792,15 @@ class CupertinoLocalizationKy extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Издөө'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Баарын тандоо'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount ичинен $tabIndex-өтмөк'; @@ -7156,11 +7933,17 @@ class CupertinoLocalizationLo extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'ປິດເມນູ'; + @override String get modalBarrierDismissLabel => 'ປິດໄວ້'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ບໍ່ພົບການແທນທີ່'; @override String get pasteButtonLabel => 'ວາງ'; @@ -7171,9 +7954,15 @@ class CupertinoLocalizationLo extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ຊອກຫາ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ເລືອກທັງໝົດ'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'ແຖບທີ $tabIndex ຈາກທັງໝົດ $tabCount'; @@ -7306,11 +8095,17 @@ class CupertinoLocalizationLt extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Atsisakyti meniu'; + @override String get modalBarrierDismissLabel => 'Atsisakyti'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nerasta jokių pakeitimų'; @override String get pasteButtonLabel => 'Įklijuoti'; @@ -7321,9 +8116,15 @@ class CupertinoLocalizationLt extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Paieška'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pasirinkti viską'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabIndex skirtukas iš $tabCount'; @@ -7456,11 +8257,17 @@ class CupertinoLocalizationLv extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => r'$minute minūtes'; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Nerādīt izvēlni'; + @override String get modalBarrierDismissLabel => 'Nerādīt'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Netika atrasts neviens vārds aizstāšanai'; @override String get pasteButtonLabel => 'Ielīmēt'; @@ -7471,9 +8278,15 @@ class CupertinoLocalizationLv extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Meklēšana'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Atlasīt visu'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabIndex. cilne no $tabCount'; @@ -7606,11 +8419,17 @@ class CupertinoLocalizationMk extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Отфрлете го менито'; + @override String get modalBarrierDismissLabel => 'Отфрли'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Не се најдени заменски зборови'; @override String get pasteButtonLabel => 'Залепи'; @@ -7621,9 +8440,15 @@ class CupertinoLocalizationMk extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Пребарувајте'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Избери ги сите'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Картичка $tabIndex од $tabCount'; @@ -7756,11 +8581,17 @@ class CupertinoLocalizationMl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'മുകളിലേക്ക് നോക്കുക'; + + @override + String get menuDismissLabel => 'മെനു ഡിസ്മിസ് ചെയ്യുക'; + @override String get modalBarrierDismissLabel => 'നിരസിക്കുക'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'റീപ്ലേസ്‌മെന്റുകളൊന്നും കണ്ടെത്തിയില്ല'; @override String get pasteButtonLabel => 'ഒട്ടിക്കുക'; @@ -7771,9 +8602,15 @@ class CupertinoLocalizationMl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'തിരയുക'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'എല്ലാം തിരഞ്ഞെടുക്കുക'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount ടാബിൽ $tabIndex-ാമത്തേത്'; @@ -7906,11 +8743,17 @@ class CupertinoLocalizationMn extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Цэсийг хаах'; + @override String get modalBarrierDismissLabel => 'Үл хэрэгсэх'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Ямар ч орлуулалт олдсонгүй'; @override String get pasteButtonLabel => 'Буулгах'; @@ -7921,9 +8764,15 @@ class CupertinoLocalizationMn extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Хайх'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Бүгдийг сонгох'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount-н $tabIndex-р таб'; @@ -8056,11 +8905,17 @@ class CupertinoLocalizationMr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'मेनू डिसमिस करा'; + @override String get modalBarrierDismissLabel => 'डिसमिस करा'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'कोणतेही बदल आढळले नाहीत'; @override String get pasteButtonLabel => 'पेस्ट करा'; @@ -8071,9 +8926,15 @@ class CupertinoLocalizationMr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'शोधा'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सर्व निवडा'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount पैकी $tabIndex टॅब'; @@ -8206,11 +9067,17 @@ class CupertinoLocalizationMs extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Lihat ke Atas'; + + @override + String get menuDismissLabel => 'Ketepikan menu'; + @override String get modalBarrierDismissLabel => 'Tolak'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Tiada Penggantian Ditemukan'; @override String get pasteButtonLabel => 'Tampal'; @@ -8221,9 +9088,15 @@ class CupertinoLocalizationMs extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Cari'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pilih Semua'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex daripada $tabCount'; @@ -8356,11 +9229,17 @@ class CupertinoLocalizationMy extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'မီနူးကိုပယ်ပါ'; + @override String get modalBarrierDismissLabel => 'ပယ်ရန်'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'အစားထိုးမှုများ မတွေ့ပါ'; @override String get pasteButtonLabel => 'ကူးထည့်ရန်'; @@ -8371,9 +9250,15 @@ class CupertinoLocalizationMy extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ရှာရန်'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'အားလုံး ရွေးရန်'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'တဘ် $tabCount ခုအနက် $tabIndex ခု'; @@ -8506,11 +9391,17 @@ class CupertinoLocalizationNb extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Lukk menyen'; + @override String get modalBarrierDismissLabel => 'Avvis'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Fant ingen erstatninger'; @override String get pasteButtonLabel => 'Lim inn'; @@ -8521,9 +9412,15 @@ class CupertinoLocalizationNb extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Søk'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velg alle'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Fane $tabIndex av $tabCount'; @@ -8656,11 +9553,17 @@ class CupertinoLocalizationNe extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'मेनु खारेज गर्नुहोस्'; + @override String get modalBarrierDismissLabel => 'खारेज गर्नुहोस्'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'बदल्नु पर्ने कुनै पनि कुरा भेटिएन'; @override String get pasteButtonLabel => 'टाँस्नुहोस्'; @@ -8671,9 +9574,15 @@ class CupertinoLocalizationNe extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'खोज्नुहोस्'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सबै चयन गर्नुहोस्'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount मध्ये $tabIndex ट्याब'; @@ -8801,16 +9710,22 @@ class CupertinoLocalizationNl extends GlobalCupertinoLocalizations { String get datePickerMinuteSemanticsLabelOther => r'$minute minuten'; @override - String? get datePickerMinuteSemanticsLabelTwo => null; + String? get datePickerMinuteSemanticsLabelTwo => null; + + @override + String? get datePickerMinuteSemanticsLabelZero => null; + + @override + String get lookUpButtonLabel => 'Look Up'; @override - String? get datePickerMinuteSemanticsLabelZero => null; + String get menuDismissLabel => 'Menu sluiten'; @override String get modalBarrierDismissLabel => 'Sluiten'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Geen vervangingen gevonden'; @override String get pasteButtonLabel => 'Plakken'; @@ -8821,9 +9736,15 @@ class CupertinoLocalizationNl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Zoeken'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alles selecteren'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tabblad $tabIndex van $tabCount'; @@ -8956,11 +9877,17 @@ class CupertinoLocalizationNo extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Lukk menyen'; + @override String get modalBarrierDismissLabel => 'Avvis'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Fant ingen erstatninger'; @override String get pasteButtonLabel => 'Lim inn'; @@ -8971,9 +9898,15 @@ class CupertinoLocalizationNo extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Søk'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velg alle'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Fane $tabIndex av $tabCount'; @@ -9106,11 +10039,17 @@ class CupertinoLocalizationOr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'ମେନୁ ଖାରଜ କରନ୍ତୁ'; + @override String get modalBarrierDismissLabel => 'ଖାରଜ କରନ୍ତୁ'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'କୌଣସି ରିପ୍ଲେସମେଣ୍ଟ ମିଳିଲା ନାହିଁ'; @override String get pasteButtonLabel => 'ପେଷ୍ଟ କରନ୍ତୁ'; @@ -9121,9 +10060,15 @@ class CupertinoLocalizationOr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ସନ୍ଧାନ କରନ୍ତୁ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ସମସ୍ତ ଚୟନ କରନ୍ତୁ'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCountର $tabIndex ଟାବ୍'; @@ -9256,11 +10201,17 @@ class CupertinoLocalizationPa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'ਮੀਨੂ ਖਾਰਜ ਕਰੋ'; + @override String get modalBarrierDismissLabel => 'ਖਾਰਜ ਕਰੋ'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ਕੋਈ ਸੁਝਾਅ ਨਹੀਂ ਮਿਲਿਆ'; @override String get pasteButtonLabel => 'ਪੇਸਟ ਕਰੋ'; @@ -9271,9 +10222,15 @@ class CupertinoLocalizationPa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ਖੋਜੋ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ਸਭ ਚੁਣੋ'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount ਵਿੱਚੋਂ $tabIndex ਟੈਬ'; @@ -9406,11 +10363,17 @@ class CupertinoLocalizationPl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Sprawdź'; + + @override + String get menuDismissLabel => 'Zamknij menu'; + @override String get modalBarrierDismissLabel => 'Zamknij'; @override - String get noSpellCheckReplacementsLabel => 'Nie znaleziono zastąpień'; + String get noSpellCheckReplacementsLabel => 'Brak wyników zamieniania'; @override String get pasteButtonLabel => 'Wklej'; @@ -9422,7 +10385,13 @@ class CupertinoLocalizationPl extends GlobalCupertinoLocalizations { String get searchTextFieldPlaceholderLabel => 'Szukaj'; @override - String get selectAllButtonLabel => 'Zaznacz wszystko'; + String get searchWebButtonLabel => 'Search Web'; + + @override + String get selectAllButtonLabel => 'Wybierz wszystkie'; + + @override + String get shareButtonLabel => 'Share...'; @override String get tabSemanticsLabelRaw => r'Karta $tabIndex z $tabCount'; @@ -9556,11 +10525,17 @@ class CupertinoLocalizationPt extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Pesquisar'; + + @override + String get menuDismissLabel => 'Dispensar menu'; + @override String get modalBarrierDismissLabel => 'Dispensar'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nenhuma alternativa encontrada'; @override String get pasteButtonLabel => 'Colar'; @@ -9572,7 +10547,13 @@ class CupertinoLocalizationPt extends GlobalCupertinoLocalizations { String get searchTextFieldPlaceholderLabel => 'Pesquisar'; @override - String get selectAllButtonLabel => 'Selecionar tudo'; + String get searchWebButtonLabel => 'Search Web'; + + @override + String get selectAllButtonLabel => 'Selecionar Tudo'; + + @override + String get shareButtonLabel => 'Share...'; @override String get tabSemanticsLabelRaw => r'Guia $tabIndex de $tabCount'; @@ -9652,6 +10633,15 @@ class CupertinoLocalizationPtPt extends CupertinoLocalizationPt { required super.decimalFormat, }); + @override + String get lookUpButtonLabel => 'Procurar'; + + @override + String get noSpellCheckReplacementsLabel => 'Não foram encontradas substituições'; + + @override + String get menuDismissLabel => 'Ignorar menu'; + @override String get searchTextFieldPlaceholderLabel => 'Pesquise'; @@ -9667,6 +10657,9 @@ class CupertinoLocalizationPtPt extends CupertinoLocalizationPt { @override String get timerPickerSecondLabelOther => 'seg'; + @override + String get selectAllButtonLabel => 'Selecionar tudo'; + @override String get modalBarrierDismissLabel => 'Ignorar'; } @@ -9742,11 +10735,17 @@ class CupertinoLocalizationRo extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Respingeți meniul'; + @override String get modalBarrierDismissLabel => 'Închideți'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nu s-au găsit înlocuiri'; @override String get pasteButtonLabel => 'Inserați'; @@ -9757,9 +10756,15 @@ class CupertinoLocalizationRo extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Căutați'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Selectați-le pe toate'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Fila $tabIndex din $tabCount'; @@ -9892,11 +10897,17 @@ class CupertinoLocalizationRu extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Закрыть меню'; + @override String get modalBarrierDismissLabel => 'Закрыть'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Варианты замены не найдены'; @override String get pasteButtonLabel => 'Вставить'; @@ -9907,9 +10918,15 @@ class CupertinoLocalizationRu extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Поиск'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Выбрать все'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Вкладка $tabIndex из $tabCount'; @@ -10042,11 +11059,17 @@ class CupertinoLocalizationSi extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'මෙනුව අස් කරන්න'; + @override String get modalBarrierDismissLabel => 'ඉවත ලන්න'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ප්‍රතිස්ථාපන හමු නොවිණි'; @override String get pasteButtonLabel => 'අලවන්න'; @@ -10057,9 +11080,15 @@ class CupertinoLocalizationSi extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'සෙවීම'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'සියල්ල තෝරන්න'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'ටැබ $tabCount න් $tabIndex'; @@ -10192,11 +11221,17 @@ class CupertinoLocalizationSk extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Zavrieť ponuku'; + @override String get modalBarrierDismissLabel => 'Odmietnuť'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nenašli sa žiadne náhrady'; @override String get pasteButtonLabel => 'Prilepiť'; @@ -10207,9 +11242,15 @@ class CupertinoLocalizationSk extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Hľadať'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vybrať všetko'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Karta $tabIndex z $tabCount'; @@ -10342,11 +11383,17 @@ class CupertinoLocalizationSl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Opusti meni'; + @override String get modalBarrierDismissLabel => 'Opusti'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Ni zamenjav'; @override String get pasteButtonLabel => 'Prilepi'; @@ -10357,9 +11404,15 @@ class CupertinoLocalizationSl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Iskanje'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Izberi vse'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Zavihek $tabIndex od $tabCount'; @@ -10492,11 +11545,17 @@ class CupertinoLocalizationSq extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Hiqe menynë'; + @override String get modalBarrierDismissLabel => 'Hiq'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Nuk u gjetën zëvendësime'; @override String get pasteButtonLabel => 'Ngjit'; @@ -10507,9 +11566,15 @@ class CupertinoLocalizationSq extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Kërko'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Zgjidhi të gjitha'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Skeda $tabIndex nga $tabCount'; @@ -10642,11 +11707,17 @@ class CupertinoLocalizationSr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Одбаците мени'; + @override String get modalBarrierDismissLabel => 'Одбаци'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Нису пронађене замене'; @override String get pasteButtonLabel => 'Налепи'; @@ -10657,9 +11728,15 @@ class CupertinoLocalizationSr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Претражите'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Изабери све'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabIndex. картица од $tabCount'; @@ -10786,9 +11863,15 @@ class CupertinoLocalizationSrLatn extends CupertinoLocalizationSr { @override String get datePickerMinuteSemanticsLabelOther => r'$minute minuta'; + @override + String get menuDismissLabel => 'Odbacite meni'; + @override String get modalBarrierDismissLabel => 'Odbaci'; + @override + String get noSpellCheckReplacementsLabel => 'Nisu pronađene zamene'; + @override String get pasteButtonLabel => 'Nalepi'; @@ -10906,11 +11989,17 @@ class CupertinoLocalizationSv extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Stäng menyn'; + @override String get modalBarrierDismissLabel => 'Stäng'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Inga ersättningar hittades'; @override String get pasteButtonLabel => 'Klistra in'; @@ -10921,9 +12010,15 @@ class CupertinoLocalizationSv extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Sök'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Markera alla'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Flik $tabIndex av $tabCount'; @@ -11056,11 +12151,17 @@ class CupertinoLocalizationSw extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Ondoa menyu'; + @override String get modalBarrierDismissLabel => 'Ondoa'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Hakuna Neno Mbadala Lilopatikana'; @override String get pasteButtonLabel => 'Bandika'; @@ -11071,9 +12172,15 @@ class CupertinoLocalizationSw extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Tafuta'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Teua Zote'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Kichupo cha $tabIndex kati ya $tabCount'; @@ -11206,11 +12313,17 @@ class CupertinoLocalizationTa extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'மெனுவை மூடும்'; + @override String get modalBarrierDismissLabel => 'நிராகரிக்கும்'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'மாற்று வார்த்தைகள் கிடைக்கவில்லை'; @override String get pasteButtonLabel => 'ஒட்டு'; @@ -11221,9 +12334,15 @@ class CupertinoLocalizationTa extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'தேடுக'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'எல்லாம் தேர்ந்தெடு'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'தாவல் $tabIndex / $tabCount'; @@ -11356,11 +12475,17 @@ class CupertinoLocalizationTe extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'మెనూను తీసివేయండి'; + @override String get modalBarrierDismissLabel => 'విస్మరించు'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'రీప్లేస్‌మెంట్‌లు ఏవీ కనుగొనబడలేదు'; @override String get pasteButtonLabel => 'పేస్ట్ చేయండి'; @@ -11371,9 +12496,15 @@ class CupertinoLocalizationTe extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'సెర్చ్ చేయి'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'అన్నింటినీ ఎంచుకోండి'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCountలో $tabIndexవ ట్యాబ్'; @@ -11506,11 +12637,17 @@ class CupertinoLocalizationTh extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'ปิดเมนู'; + @override String get modalBarrierDismissLabel => 'ปิด'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'ไม่พบรายการแทนที่'; @override String get pasteButtonLabel => 'วาง'; @@ -11521,9 +12658,15 @@ class CupertinoLocalizationTh extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'ค้นหา'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'เลือกทั้งหมด'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'แท็บที่ $tabIndex จาก $tabCount'; @@ -11656,11 +12799,17 @@ class CupertinoLocalizationTl extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Tumingin sa Itaas'; + + @override + String get menuDismissLabel => 'I-dismiss ang menu'; + @override String get modalBarrierDismissLabel => 'I-dismiss'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Walang Nahanap na Kapalit'; @override String get pasteButtonLabel => 'I-paste'; @@ -11671,9 +12820,15 @@ class CupertinoLocalizationTl extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Hanapin'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Piliin Lahat'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Tab $tabIndex ng $tabCount'; @@ -11806,11 +12961,17 @@ class CupertinoLocalizationTr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Menüyü kapat'; + @override String get modalBarrierDismissLabel => 'Kapat'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Yerine Kelime Bulunamadı'; @override String get pasteButtonLabel => 'Yapıştır'; @@ -11821,9 +12982,15 @@ class CupertinoLocalizationTr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Ara'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Tümünü Seç'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Sekme $tabIndex/$tabCount'; @@ -11956,11 +13123,17 @@ class CupertinoLocalizationUk extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Закрити меню'; + @override String get modalBarrierDismissLabel => 'Закрити'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Замін не знайдено'; @override String get pasteButtonLabel => 'Вставити'; @@ -11971,9 +13144,15 @@ class CupertinoLocalizationUk extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Шукайте'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Вибрати все'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Вкладка $tabIndex з $tabCount'; @@ -12106,11 +13285,17 @@ class CupertinoLocalizationUr extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'مینو برخاست کریں'; + @override String get modalBarrierDismissLabel => 'برخاست کریں'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'کوئی تبدیلیاں نہیں ملیں'; @override String get pasteButtonLabel => 'پیسٹ کریں'; @@ -12121,9 +13306,15 @@ class CupertinoLocalizationUr extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'تلاش کریں'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'سبھی منتخب کریں'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount میں سے $tabIndex ٹیب'; @@ -12256,11 +13447,17 @@ class CupertinoLocalizationUz extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Tepaga qarang'; + + @override + String get menuDismissLabel => 'Menyuni yopish'; + @override String get modalBarrierDismissLabel => 'Yopish'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Almashtirish uchun soʻz topilmadi'; @override String get pasteButtonLabel => 'Joylash'; @@ -12271,9 +13468,15 @@ class CupertinoLocalizationUz extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Qidiruv'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Barchasini tanlash'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'$tabCount varaqdan $tabIndex'; @@ -12406,11 +13609,17 @@ class CupertinoLocalizationVi extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Đóng trình đơn'; + @override String get modalBarrierDismissLabel => 'Bỏ qua'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Không tìm thấy phương án thay thế'; @override String get pasteButtonLabel => 'Dán'; @@ -12421,9 +13630,15 @@ class CupertinoLocalizationVi extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Tìm kiếm'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Chọn tất cả'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Thẻ $tabIndex/$tabCount'; @@ -12556,11 +13771,17 @@ class CupertinoLocalizationZh extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => '关闭菜单'; + @override String get modalBarrierDismissLabel => '关闭'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => '找不到替换文字'; @override String get pasteButtonLabel => '粘贴'; @@ -12571,9 +13792,15 @@ class CupertinoLocalizationZh extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => '搜索'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '全选'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'第 $tabIndex 个标签,共 $tabCount 个'; @@ -12694,9 +13921,18 @@ class CupertinoLocalizationZhHant extends CupertinoLocalizationZh { @override String get datePickerMinuteSemanticsLabelOther => r'$minute 分鐘'; + @override + String get lookUpButtonLabel => '查詢'; + + @override + String get menuDismissLabel => '閂選單'; + @override String get modalBarrierDismissLabel => '拒絕'; + @override + String get noSpellCheckReplacementsLabel => '找不到替換字詞'; + @override String get pasteButtonLabel => '貼上'; @@ -12757,6 +13993,12 @@ class CupertinoLocalizationZhHantTw extends CupertinoLocalizationZhHant { required super.decimalFormat, }); + @override + String get noSpellCheckReplacementsLabel => '找不到替代文字'; + + @override + String get menuDismissLabel => '關閉選單'; + @override String get tabSemanticsLabelRaw => r'第 $tabIndex 個分頁標籤,共 $tabCount 個'; @@ -12853,11 +14095,17 @@ class CupertinoLocalizationZu extends GlobalCupertinoLocalizations { @override String? get datePickerMinuteSemanticsLabelZero => null; + @override + String get lookUpButtonLabel => 'Look Up'; + + @override + String get menuDismissLabel => 'Chitha imenyu'; + @override String get modalBarrierDismissLabel => 'Cashisa'; @override - String get noSpellCheckReplacementsLabel => 'No Replacements Found'; + String get noSpellCheckReplacementsLabel => 'Akukho Okuzofakwa Esikhundleni Okutholakele'; @override String get pasteButtonLabel => 'Namathisela'; @@ -12868,9 +14116,15 @@ class CupertinoLocalizationZu extends GlobalCupertinoLocalizations { @override String get searchTextFieldPlaceholderLabel => 'Sesha'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Khetha konke'; + @override + String get shareButtonLabel => 'Share...'; + @override String get tabSemanticsLabelRaw => r'Ithebhu $tabIndex kwangu-$tabCount'; diff --git a/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart b/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart index d936427c1653a..f454b13b92810 100644 --- a/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart +++ b/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart @@ -66,7 +66,7 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Maak toe'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Uitgevou'; @override String get collapsedIconTapHint => 'Vou uit'; @@ -126,22 +126,22 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigasiekieslys'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Ingevou'; @override String get expandedIconTapHint => 'Vou in'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dubbeltik om uit te vou'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Vou uit vir meer besonderhede'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dubbeltik om in te vou'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Vou in'; @override String get firstPageTooltip => 'Eerste bladsy'; @@ -329,9 +329,15 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisensies'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Kieslysbalkkieslys'; + @override + String get menuDismissLabel => 'Maak kieslys toe'; + @override String get modalBarrierDismissLabel => 'Maak toe'; @@ -417,7 +423,7 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Stoor'; @override - String get scanTextButtonLabel => 'Skena umbhalo'; + String get scanTextButtonLabel => 'Skandeer teks'; @override String get scrimLabel => 'Skerm'; @@ -431,6 +437,9 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Soek'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Kies alles'; @@ -455,6 +464,9 @@ class MaterialLocalizationAf extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Wys rekeninge'; @@ -544,7 +556,7 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ዝጋ'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ተዘርግቷል'; @override String get collapsedIconTapHint => 'ዘርጋ'; @@ -604,25 +616,25 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get drawerLabel => 'የዳሰሳ ምናሌ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ተሰብስቧል'; @override String get expandedIconTapHint => 'ሰብስብ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ለመዘርጋት ድርብ ሁለቴ መታ ያድርጉ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ለተጨማሪ ዝርዝሮች ይዘርጉ'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ለመሰብሰብ ሁለቴ መታ ያድርጉ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ሰብስብ'; @override - String get firstPageTooltip => 'የመጀመሪያው ገጽ'; + String get firstPageTooltip => 'የመጀመሪያው ገፅ'; @override String get hideAccountsLabel => 'መለያዎችን ደብቅ'; @@ -784,7 +796,7 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get keyboardKeySpace => 'ክፍተት'; @override - String get lastPageTooltip => 'የመጨረሻው ገጽ'; + String get lastPageTooltip => 'የመጨረሻው ገፅ'; @override String? get licensesPackageDetailTextFew => null; @@ -807,9 +819,15 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ፈቃዶች'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'የምናሌ አሞሌ ምናሌ'; + @override + String get menuDismissLabel => 'ምናሌን አሰናብት'; + @override String get modalBarrierDismissLabel => 'አሰናብት'; @@ -820,7 +838,7 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get nextMonthTooltip => 'ቀጣይ ወር'; @override - String get nextPageTooltip => 'ቀጣይ ገጽ'; + String get nextPageTooltip => 'ቀጣይ ገፅ'; @override String get okButtonLabel => 'እሺ'; @@ -847,7 +865,7 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get previousMonthTooltip => 'ቀዳሚ ወር'; @override - String get previousPageTooltip => 'ቀዳሚ ገጽ'; + String get previousPageTooltip => 'ቀዳሚ ገፅ'; @override String get refreshIndicatorSemanticLabel => 'አድስ'; @@ -895,7 +913,7 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { String get saveButtonLabel => 'አስቀምጥ'; @override - String get scanTextButtonLabel => 'ጽሑፍ ይቃኙ'; + String get scanTextButtonLabel => 'ጽሁፍን ቃኝ'; @override String get scrimLabel => 'ገዳቢ'; @@ -909,6 +927,9 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ይፈልጉ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ሁሉንም ምረጥ'; @@ -933,6 +954,9 @@ class MaterialLocalizationAm extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'መለያዎችን አሳይ'; @@ -1022,7 +1046,7 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'إغلاق'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'موسَّع'; @override String get collapsedIconTapHint => 'توسيع'; @@ -1082,22 +1106,22 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { String get drawerLabel => 'قائمة تنقل'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'مصغَّر'; @override String get expandedIconTapHint => 'تصغير'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'انقر مرّتين للتوسيع'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'وسِّع المربّع لعرض مزيد من التفاصيل.'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'يُرجى النقر مرّتين للتصغير.'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'تصغير'; @override String get firstPageTooltip => 'الصفحة الأولى'; @@ -1285,9 +1309,15 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'التراخيص'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'قائمة شريط القوائم'; + @override + String get menuDismissLabel => 'إغلاق القائمة'; + @override String get modalBarrierDismissLabel => 'رفض'; @@ -1373,7 +1403,7 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'الحفظ'; @override - String get scanTextButtonLabel => 'مسح النص'; + String get scanTextButtonLabel => 'مسح النص ضوئيًا'; @override String get scrimLabel => 'تمويه'; @@ -1387,6 +1417,9 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'بحث'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'اختيار الكل'; @@ -1411,6 +1444,9 @@ class MaterialLocalizationAr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'لم يتم اختيار أي عنصر'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'إظهار الحسابات'; @@ -1500,7 +1536,7 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'বন্ধ কৰক'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'বিস্তাৰ কৰা আছে'; @override String get collapsedIconTapHint => 'বিস্তাৰ কৰক'; @@ -1560,22 +1596,22 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { String get drawerLabel => 'নেভিগেশ্বন মেনু'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'সংকোচন কৰা আছে'; @override String get expandedIconTapHint => 'সংকোচন কৰক'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'বিস্তাৰ কৰিবলৈ দুবাৰ টিপক'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'অধিক সবিশেষ জানিবলৈ বিস্তাৰ কৰক'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'সংকোচন কৰিবলৈ দুবাৰ টিপক'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'সংকোচন কৰক'; @override String get firstPageTooltip => 'প্রথম পৃষ্ঠা'; @@ -1763,9 +1799,15 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'অনুজ্ঞাপত্ৰসমূহ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'মেনু বাৰ মেনু'; + @override + String get menuDismissLabel => 'অগ্ৰাহ্য কৰাৰ মেনু'; + @override String get modalBarrierDismissLabel => 'অগ্ৰাহ্য কৰক'; @@ -1851,7 +1893,7 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { String get saveButtonLabel => 'ছেভ কৰক'; @override - String get scanTextButtonLabel => 'স্কেন টেক্সট'; + String get scanTextButtonLabel => 'পাঠ স্কেন কৰক'; @override String get scrimLabel => 'স্ক্ৰিম'; @@ -1865,6 +1907,9 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'সন্ধান কৰক'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'সকলো বাছনি কৰক'; @@ -1889,6 +1934,9 @@ class MaterialLocalizationAs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'একাউণ্টসমূহ দেখুৱাওক'; @@ -1978,7 +2026,7 @@ class MaterialLocalizationAz extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Bağlayın'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Genişləndirildi'; @override String get collapsedIconTapHint => 'Genişləndirin'; @@ -2038,22 +2086,22 @@ class MaterialLocalizationAz extends GlobalMaterialLocalizations { String get drawerLabel => 'Naviqasiya menyusu'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Yığcamlaşdırıldı'; @override String get expandedIconTapHint => 'Yığcamlaşdırın'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'genişləndirmək üçün iki dəfə toxunun'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Daha çox detallar üçün genişləndirin'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'yığcamlaşdırmaq üçün iki dəfə toxunun'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Yığcamlaşdırın'; @override String get firstPageTooltip => 'Birinci səhifə'; @@ -2241,9 +2289,15 @@ class MaterialLocalizationAz extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisenziyalar'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menyu paneli menyusu'; + @override + String get menuDismissLabel => 'Menyunu qapadın'; + @override String get modalBarrierDismissLabel => 'İmtina edin'; @@ -2343,6 +2397,9 @@ class MaterialLocalizationAz extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Axtarın'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Hamısını seçin'; @@ -2367,6 +2424,9 @@ class MaterialLocalizationAz extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Hesabları göstərin'; @@ -2456,7 +2516,7 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Закрыць'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Разгорнута'; @override String get collapsedIconTapHint => 'Разгарнуць'; @@ -2516,22 +2576,22 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { String get drawerLabel => 'Меню навігацыі'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Згорнута'; @override String get expandedIconTapHint => 'Згарнуць'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'двойчы націснуць, каб разгарнуць'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Разгарніце, каб даведацца больш'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'двойчы націснуць, каб згарнуць'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Згарнуць'; @override String get firstPageTooltip => 'На першую старонку'; @@ -2719,9 +2779,15 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Ліцэнзіі'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Меню "Панэль меню"'; + @override + String get menuDismissLabel => 'Закрыць меню'; + @override String get modalBarrierDismissLabel => 'Адхіліць'; @@ -2807,7 +2873,7 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Захаваць'; @override - String get scanTextButtonLabel => 'Сканаваць тэкст'; + String get scanTextButtonLabel => 'Сканіраваць тэкст'; @override String get scrimLabel => 'Палатно'; @@ -2821,6 +2887,9 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Пошук'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Выбраць усе'; @@ -2845,6 +2914,9 @@ class MaterialLocalizationBe extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Паказаць уліковыя запісы'; @@ -2934,7 +3006,7 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Затваряне'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Разгънато'; @override String get collapsedIconTapHint => 'Разгъване'; @@ -2994,22 +3066,22 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { String get drawerLabel => 'Меню за навигация'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Свито'; @override String get expandedIconTapHint => 'Свиване'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'докоснете два пъти за разгъване'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Разгъване за още подробности'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'докоснете два пъти за свиване'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Свиване'; @override String get firstPageTooltip => 'Първа страница'; @@ -3197,9 +3269,15 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лицензи'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Меню на лентата с менюта'; + @override + String get menuDismissLabel => 'Отхвърляне на менюто'; + @override String get modalBarrierDismissLabel => 'Отхвърляне'; @@ -3285,7 +3363,7 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Запазване'; @override - String get scanTextButtonLabel => 'Сканиране на текст'; + String get scanTextButtonLabel => 'Сканирайте текст'; @override String get scrimLabel => 'Скрим'; @@ -3299,6 +3377,9 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Търсене'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Избиране на всички'; @@ -3323,6 +3404,9 @@ class MaterialLocalizationBg extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Показване на профилите'; @@ -3412,7 +3496,7 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'বন্ধ করুন'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'বড় করা হয়েছে'; @override String get collapsedIconTapHint => 'বড় করুন'; @@ -3472,22 +3556,22 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { String get drawerLabel => 'নেভিগেশান মেনু'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'আড়াল করা হয়েছে'; @override String get expandedIconTapHint => 'আড়াল করুন'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'বড় করে দেখতে ডবল ট্যাপ করুন'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'আরও বিবরণ পেতে বড় করে দেখুন'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'আড়াল করতে ডবল ট্যাপ করুন'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'আড়াল করুন'; @override String get firstPageTooltip => 'প্রথম পৃষ্ঠা'; @@ -3675,9 +3759,15 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'লাইসেন্স'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'মেনু বার মেনু'; + @override + String get menuDismissLabel => 'বাতিল করার মেনু'; + @override String get modalBarrierDismissLabel => 'খারিজ করুন'; @@ -3763,7 +3853,7 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { String get saveButtonLabel => 'সেভ করুন'; @override - String get scanTextButtonLabel => 'পাঠ্য স্ক্যান করুন'; + String get scanTextButtonLabel => 'টেক্সট স্ক্যান করুন'; @override String get scrimLabel => 'স্ক্রিম'; @@ -3777,6 +3867,9 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'খুঁজুন'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'সব বেছে নিন'; @@ -3801,6 +3894,9 @@ class MaterialLocalizationBn extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'অ্যাকাউন্টগুলি দেখান'; @@ -3890,7 +3986,7 @@ class MaterialLocalizationBs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zatvaranje'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Prošireno'; @override String get collapsedIconTapHint => 'Proširi'; @@ -3950,22 +4046,22 @@ class MaterialLocalizationBs extends GlobalMaterialLocalizations { String get drawerLabel => 'Meni za navigaciju'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Suženo'; @override String get expandedIconTapHint => 'Suzi'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'proširivanje dvostrukim dodirom'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Proširivanje za više detalja'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'sužavanje dvostrukim dodirom'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Sužavanje'; @override String get firstPageTooltip => 'Prva stranica'; @@ -4153,9 +4249,15 @@ class MaterialLocalizationBs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licence'; + @override + String get lookUpButtonLabel => 'Pogled prema gore'; + @override String get menuBarMenuLabel => 'Meni trake menija'; + @override + String get menuDismissLabel => 'Odbacivanje menija'; + @override String get modalBarrierDismissLabel => 'Odbaci'; @@ -4255,6 +4357,9 @@ class MaterialLocalizationBs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Pretražite'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Odaberi sve'; @@ -4279,6 +4384,9 @@ class MaterialLocalizationBs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Prikaži račune'; @@ -4368,7 +4476,7 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Tanca'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => "S'ha desplegat"; @override String get collapsedIconTapHint => 'Desplega'; @@ -4428,22 +4536,22 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { String get drawerLabel => 'Menú de navegació'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => "S'ha replegat"; @override String get expandedIconTapHint => 'Replega'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'fes doble toc per desplegar'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Desplega per obtenir més informació'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'fes doble toc per replegar'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Replega'; @override String get firstPageTooltip => 'Primera pàgina'; @@ -4631,9 +4739,15 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Llicències'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menú de la barra de menú'; + @override + String get menuDismissLabel => 'Ignora el menú'; + @override String get modalBarrierDismissLabel => 'Ignora'; @@ -4719,7 +4833,7 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Desa'; @override - String get scanTextButtonLabel => 'Escaneja el text'; + String get scanTextButtonLabel => 'Escaneja text'; @override String get scrimLabel => 'Fons atenuat'; @@ -4733,6 +4847,9 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Cerca'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Selecciona-ho tot'; @@ -4757,6 +4874,9 @@ class MaterialLocalizationCa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Mostra els comptes'; @@ -4846,7 +4966,7 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zavřít'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Rozbaleno'; @override String get collapsedIconTapHint => 'Rozbalit'; @@ -4906,22 +5026,22 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigační nabídka'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Sbaleno'; @override String get expandedIconTapHint => 'Sbalit'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dvojitým klepnutím rozbalíte'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Rozbalte pro další podrobnosti'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dvojitým klepnutím sbalíte'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Sbalit'; @override String get firstPageTooltip => 'První stránka'; @@ -5109,9 +5229,15 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licence'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Nabídka na liště s nabídkou'; + @override + String get menuDismissLabel => 'Zavřít nabídku'; + @override String get modalBarrierDismissLabel => 'Zavřít'; @@ -5197,7 +5323,7 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Uložit'; @override - String get scanTextButtonLabel => 'Naskenujte text'; + String get scanTextButtonLabel => 'Naskenovat text'; @override String get scrimLabel => 'Scrim'; @@ -5211,6 +5337,9 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Hledat'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vybrat vše'; @@ -5235,6 +5364,9 @@ class MaterialLocalizationCs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Zobrazit účty'; @@ -5324,7 +5456,7 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Cau'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => "Wedi'i ehangu"; @override String get collapsedIconTapHint => 'Ehangu'; @@ -5384,22 +5516,22 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { String get drawerLabel => 'Dewislen llywio'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => "Wedi'i grebachu"; @override String get expandedIconTapHint => 'Crebachu'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'tapiwch ddwywaith i ehangu'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Ehangwch am ragor o fanylion'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tapiwch ddwywaith i grebachu'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Crebachu'; @override String get firstPageTooltip => 'Tudalen gyntaf'; @@ -5587,9 +5719,15 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Trwyddedau'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Dewislen bar dewislen'; + @override + String get menuDismissLabel => "Diystyru'r ddewislen"; + @override String get modalBarrierDismissLabel => 'Diystyru'; @@ -5675,7 +5813,7 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Cadw'; @override - String get scanTextButtonLabel => 'Scan text'; + String get scanTextButtonLabel => 'Sganio testun'; @override String get scrimLabel => 'Scrim'; @@ -5689,6 +5827,9 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Chwilio'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Dewis y Cyfan'; @@ -5713,6 +5854,9 @@ class MaterialLocalizationCy extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => "Nid oes unrhyw eitemau wedi'u dewis"; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Dangos cyfrifon'; @@ -5802,7 +5946,7 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Luk'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Udvidet'; @override String get collapsedIconTapHint => 'Udvid'; @@ -5862,22 +6006,22 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigationsmenu'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Skjult'; @override String get expandedIconTapHint => 'Skjul'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'tryk to gange for at udvide'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Udvid for at få flere oplysninger'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tryk to gange for at skjule'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Skjul'; @override String get firstPageTooltip => 'Første side'; @@ -6065,9 +6209,15 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenser'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menuen for menulinjen'; + @override + String get menuDismissLabel => 'Luk menu'; + @override String get modalBarrierDismissLabel => 'Afvis'; @@ -6167,6 +6317,9 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Søg'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Markér alt'; @@ -6191,6 +6344,9 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Vis konti'; @@ -6204,7 +6360,7 @@ class MaterialLocalizationDa extends GlobalMaterialLocalizations { String get tabLabelRaw => r'Fane $tabIndex af $tabCount'; @override - TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.HH_colon_mm; + TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.HH_dot_mm; @override String get timePickerDialHelpText => 'Vælg tidspunkt'; @@ -6280,7 +6436,7 @@ class MaterialLocalizationDe extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Schließen'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Maximiert'; @override String get collapsedIconTapHint => 'Maximieren'; @@ -6340,22 +6496,22 @@ class MaterialLocalizationDe extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigationsmenü'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Minimiert'; @override String get expandedIconTapHint => 'Minimieren'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'Zum Maximieren doppeltippen'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Für weitere Details maximieren'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'Zum Minimieren doppeltippen'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Minimieren'; @override String get firstPageTooltip => 'Erste Seite'; @@ -6543,9 +6699,15 @@ class MaterialLocalizationDe extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lizenzen'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menü in der Menüleiste'; + @override + String get menuDismissLabel => 'Menü schließen'; + @override String get modalBarrierDismissLabel => 'Schließen'; @@ -6645,6 +6807,9 @@ class MaterialLocalizationDe extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Suchen'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alle auswählen'; @@ -6669,6 +6834,9 @@ class MaterialLocalizationDe extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Keine Objekte ausgewählt'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Konten anzeigen'; @@ -6822,7 +6990,7 @@ class MaterialLocalizationEl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Κλείσιμο'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Αναπτύχθηκε'; @override String get collapsedIconTapHint => 'Ανάπτυξη'; @@ -6882,22 +7050,22 @@ class MaterialLocalizationEl extends GlobalMaterialLocalizations { String get drawerLabel => 'Μενού πλοήγησης'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Συμπτύχθηκε'; @override String get expandedIconTapHint => 'Σύμπτυξη'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'πατήστε δύο φορές για ανάπτυξη'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Ανάπτυξη για περισσότερες λεπτομέρειες'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'πατήστε δύο φορές για σύμπτυξη'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Σύμπτυξη'; @override String get firstPageTooltip => 'Πρώτη σελίδα'; @@ -7085,9 +7253,15 @@ class MaterialLocalizationEl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Άδειες'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Μενού γραμμής μενού'; + @override + String get menuDismissLabel => 'Παράβλεψη μενού'; + @override String get modalBarrierDismissLabel => 'Παράβλεψη'; @@ -7187,6 +7361,9 @@ class MaterialLocalizationEl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Αναζήτηση'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Επιλογή όλων'; @@ -7211,6 +7388,9 @@ class MaterialLocalizationEl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Εμφάνιση λογαριασμών'; @@ -7563,9 +7743,15 @@ class MaterialLocalizationEn extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenses'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu bar menu'; + @override + String get menuDismissLabel => 'Dismiss menu'; + @override String get modalBarrierDismissLabel => 'Dismiss'; @@ -7665,6 +7851,9 @@ class MaterialLocalizationEn extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Search'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Select all'; @@ -7689,6 +7878,9 @@ class MaterialLocalizationEn extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'No items selected'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Show accounts'; @@ -7750,6 +7942,15 @@ class MaterialLocalizationEnAu extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -7947,6 +8148,15 @@ class MaterialLocalizationEnGb extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8038,6 +8248,15 @@ class MaterialLocalizationEnIe extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8129,6 +8348,15 @@ class MaterialLocalizationEnIn extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8217,6 +8445,15 @@ class MaterialLocalizationEnNz extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8305,6 +8542,15 @@ class MaterialLocalizationEnSg extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8393,6 +8639,15 @@ class MaterialLocalizationEnZa extends MaterialLocalizationEn { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Look up'; + + @override + String get expansionTileExpandedHint => 'double-tap to collapse'; + + @override + String get expansionTileCollapsedHint => 'double-tap to expand'; + @override String get bottomSheetLabel => 'Bottom sheet'; @@ -8512,7 +8767,7 @@ class MaterialLocalizationEs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Cerrar'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Desplegado'; @override String get collapsedIconTapHint => 'Mostrar'; @@ -8572,22 +8827,22 @@ class MaterialLocalizationEs extends GlobalMaterialLocalizations { String get drawerLabel => 'Menú de navegación'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Contraído'; @override String get expandedIconTapHint => 'Ocultar'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'toca dos veces para desplegar'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Desplegar para ver más detalles'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'toca dos veces para contraer'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Contraer'; @override String get firstPageTooltip => 'Primera página'; @@ -8775,9 +9030,15 @@ class MaterialLocalizationEs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencias'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menú de la barra de menú'; + @override + String get menuDismissLabel => 'Cerrar menú'; + @override String get modalBarrierDismissLabel => 'Cerrar'; @@ -8877,6 +9138,9 @@ class MaterialLocalizationEs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Buscar'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleccionar todo'; @@ -8901,6 +9165,9 @@ class MaterialLocalizationEs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'No se han seleccionado elementos'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Mostrar cuentas'; @@ -8962,6 +9229,27 @@ class MaterialLocalizationEs419 extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9131,6 +9419,27 @@ class MaterialLocalizationEsAr extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9300,6 +9609,27 @@ class MaterialLocalizationEsBo extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9469,6 +9799,27 @@ class MaterialLocalizationEsCl extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9638,6 +9989,27 @@ class MaterialLocalizationEsCo extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9807,6 +10179,27 @@ class MaterialLocalizationEsCr extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -9976,6 +10369,27 @@ class MaterialLocalizationEsDo extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10145,6 +10559,27 @@ class MaterialLocalizationEsEc extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10314,6 +10749,27 @@ class MaterialLocalizationEsGt extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10483,6 +10939,27 @@ class MaterialLocalizationEsHn extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10652,6 +11129,27 @@ class MaterialLocalizationEsMx extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10821,6 +11319,27 @@ class MaterialLocalizationEsNi extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -10990,6 +11509,27 @@ class MaterialLocalizationEsPa extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -11159,6 +11699,27 @@ class MaterialLocalizationEsPe extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -11328,6 +11889,27 @@ class MaterialLocalizationEsPr extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -11497,6 +12079,27 @@ class MaterialLocalizationEsPy extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -11666,6 +12269,27 @@ class MaterialLocalizationEsSv extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -11835,6 +12459,27 @@ class MaterialLocalizationEsUs extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -12007,6 +12652,27 @@ class MaterialLocalizationEsUy extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -12176,6 +12842,27 @@ class MaterialLocalizationEsVe extends MaterialLocalizationEs { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Analizar texto'; + + @override + String get lookUpButtonLabel => 'Mirar hacia arriba'; + + @override + String get menuDismissLabel => 'Descartar menú'; + + @override + String get expansionTileExpandedHint => 'presiona dos veces para contraer'; + + @override + String get expansionTileCollapsedHint => 'presiona dos veces para expandir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para ver más detalles'; + + @override + String get collapsedHint => 'Expandido'; + @override String get scrimLabel => 'Lámina'; @@ -12373,7 +13060,7 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Sule'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Laiendatud'; @override String get collapsedIconTapHint => 'Laienda'; @@ -12433,22 +13120,22 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigeerimismenüü'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Ahendatud'; @override String get expandedIconTapHint => 'Ahenda'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'topeltpuudutage laiendamiseks'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Laiendage lisateabe nägemiseks'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'topeltpuudutage ahendamiseks'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Ahenda'; @override String get firstPageTooltip => 'Esimene leht'; @@ -12636,9 +13323,15 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Litsentsid'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menüüriba menüü'; + @override + String get menuDismissLabel => 'Sulge menüü'; + @override String get modalBarrierDismissLabel => 'Loobu'; @@ -12724,7 +13417,7 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Salvesta'; @override - String get scanTextButtonLabel => 'Skanni teksti'; + String get scanTextButtonLabel => 'Skanni tekst'; @override String get scrimLabel => 'Sirm'; @@ -12738,6 +13431,9 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Otsing'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vali kõik'; @@ -12762,6 +13458,9 @@ class MaterialLocalizationEt extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Kuva kontod'; @@ -12851,7 +13550,7 @@ class MaterialLocalizationEu extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Itxi'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Zabalduta'; @override String get collapsedIconTapHint => 'Zabaldu'; @@ -12911,22 +13610,22 @@ class MaterialLocalizationEu extends GlobalMaterialLocalizations { String get drawerLabel => 'Nabigazio-menua'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Tolestuta'; @override String get expandedIconTapHint => 'Tolestu'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'zabaltzeko, sakatu birritan'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Zabaldu hau xehetasun gehiago lortzeko'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tolesteko, sakatu birritan'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Tolestu'; @override String get firstPageTooltip => 'Lehenengo orria'; @@ -13114,9 +13813,15 @@ class MaterialLocalizationEu extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lizentziak'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu-barraren menua'; + @override + String get menuDismissLabel => 'Baztertu menua'; + @override String get modalBarrierDismissLabel => 'Baztertu'; @@ -13216,6 +13921,9 @@ class MaterialLocalizationEu extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Bilatu'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Hautatu guztiak'; @@ -13240,6 +13948,9 @@ class MaterialLocalizationEu extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Erakutsi kontuak'; @@ -13329,7 +14040,7 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'بستن'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ازهم بازشده'; @override String get collapsedIconTapHint => 'بزرگ کردن'; @@ -13389,22 +14100,22 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { String get drawerLabel => 'منوی پیمایش'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'جمع‌شده'; @override String get expandedIconTapHint => 'کوچک کردن'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'برای ازهم بازکردن، دوضربه بزنید'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ازهم بازکردن برای جزئیات بیشتر'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'برای جمع کردن، دوضربه بزنید'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'جمع کردن'; @override String get firstPageTooltip => 'صفحه اول'; @@ -13437,7 +14148,7 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { String get keyboardKeyBackspace => 'پس‌بَر'; @override - String get keyboardKeyCapsLock => 'حالت حروف بزرگ'; + String get keyboardKeyCapsLock => 'Caps Lock'; @override String get keyboardKeyChannelDown => 'کانال پایین'; @@ -13590,11 +14301,17 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { String? get licensesPackageDetailTextZero => 'No licenses'; @override - String get licensesPageTitle => 'مجوزها'; + String get licensesPageTitle => 'پروانه‌ها'; + + @override + String get lookUpButtonLabel => 'Look Up'; @override String get menuBarMenuLabel => 'منوی نوار منو'; + @override + String get menuDismissLabel => 'بستن منو'; + @override String get modalBarrierDismissLabel => 'نپذیرفتن'; @@ -13680,7 +14397,7 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { String get saveButtonLabel => 'ذخیره'; @override - String get scanTextButtonLabel => 'اسکن متن'; + String get scanTextButtonLabel => 'اسکن کردن نوشتار'; @override String get scrimLabel => 'رویه'; @@ -13694,6 +14411,9 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'جستجو'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'انتخاب همه'; @@ -13718,6 +14438,9 @@ class MaterialLocalizationFa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'نشان دادن حساب‌ها'; @@ -13807,7 +14530,7 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Sulje'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Laajennettu'; @override String get collapsedIconTapHint => 'Laajenna'; @@ -13867,22 +14590,22 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigointivalikko'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Tiivistetty'; @override String get expandedIconTapHint => 'Tiivistä'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'laajenna kaksoisnapauttamalla'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Katso lisätietoja laajentamalla'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tiivistä kaksoisnapauttamalla'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Tiivistä'; @override String get firstPageTooltip => 'Ensimmäinen sivu'; @@ -14070,9 +14793,15 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisenssit'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Valikkopalkki'; + @override + String get menuDismissLabel => 'Hylkää valikko'; + @override String get modalBarrierDismissLabel => 'Ohita'; @@ -14172,6 +14901,9 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Haku'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Valitse kaikki'; @@ -14196,6 +14928,9 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Näytä tilit'; @@ -14206,7 +14941,7 @@ class MaterialLocalizationFi extends GlobalMaterialLocalizations { String get signedInLabel => 'Kirjautunut sisään'; @override - String get tabLabelRaw => r'Välilehti $tabIndex/$tabCount'; + String get tabLabelRaw => r'Välilehti $tabIndex kautta $tabCount'; @override TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.HH_dot_mm; @@ -14285,7 +15020,7 @@ class MaterialLocalizationFil extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Isara'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Naka-expand'; @override String get collapsedIconTapHint => 'I-expand'; @@ -14345,22 +15080,22 @@ class MaterialLocalizationFil extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu ng navigation'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Naka-collapse'; @override String get expandedIconTapHint => 'I-collapse'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'i-double tap para i-expand'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'I-expand para sa higit pang detalye'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'i-double tap para i-collapse'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'I-collapse'; @override String get firstPageTooltip => 'Unang page'; @@ -14548,9 +15283,15 @@ class MaterialLocalizationFil extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Mga Lisensya'; + @override + String get lookUpButtonLabel => 'Tumingin sa Itaas'; + @override String get menuBarMenuLabel => 'Menu sa menu bar'; + @override + String get menuDismissLabel => 'I-dismiss ang menu'; + @override String get modalBarrierDismissLabel => 'I-dismiss'; @@ -14650,6 +15391,9 @@ class MaterialLocalizationFil extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Maghanap'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Piliin lahat'; @@ -14674,6 +15418,9 @@ class MaterialLocalizationFil extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Ipakita ang mga account'; @@ -14763,7 +15510,7 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Fermer'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Développé'; @override String get collapsedIconTapHint => 'Développer'; @@ -14823,22 +15570,22 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu de navigation'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Réduit'; @override String get expandedIconTapHint => 'Réduire'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'appuyez deux fois pour développer'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Développer pour en savoir plus'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'appuyez deux fois pour réduire'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Réduire'; @override String get firstPageTooltip => 'Première page'; @@ -15026,9 +15773,15 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licences'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu de la barre de menu'; + @override + String get menuDismissLabel => 'Fermer le menu'; + @override String get modalBarrierDismissLabel => 'Ignorer'; @@ -15114,7 +15867,7 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Enregistrer'; @override - String get scanTextButtonLabel => 'Numériser du texte'; + String get scanTextButtonLabel => 'Scanner du texte'; @override String get scrimLabel => 'Fond'; @@ -15128,6 +15881,9 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Rechercher'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Tout sélectionner'; @@ -15152,6 +15908,9 @@ class MaterialLocalizationFr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Aucun élément sélectionné'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Afficher les comptes'; @@ -15213,6 +15972,21 @@ class MaterialLocalizationFrCa extends MaterialLocalizationFr { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => 'Balayer un texte'; + + @override + String get menuDismissLabel => 'Ignorer le menu'; + + @override + String get expansionTileExpandedHint => 'toucher deux fois pour réduire'; + + @override + String get expansionTileCollapsedHint => 'toucher deux fois pour développer'; + + @override + String get expansionTileCollapsedTapHint => 'Développer le panneau pour plus de détails'; + @override String get scrimLabel => 'Grille'; @@ -15383,7 +16157,7 @@ class MaterialLocalizationGl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Pechar'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Despregado'; @override String get collapsedIconTapHint => 'Despregar'; @@ -15443,22 +16217,22 @@ class MaterialLocalizationGl extends GlobalMaterialLocalizations { String get drawerLabel => 'Menú de navegación'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Contraído'; @override String get expandedIconTapHint => 'Contraer'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'tocar dúas veces para despregar'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Despregar para obter máis detalles'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'toca dúas veces para contraer'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Contraer'; @override String get firstPageTooltip => 'Primeira páxina'; @@ -15646,9 +16420,15 @@ class MaterialLocalizationGl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenzas'; + @override + String get lookUpButtonLabel => 'Mirar cara arriba'; + @override String get menuBarMenuLabel => 'Menú da barra de menú'; + @override + String get menuDismissLabel => 'Pechar menú'; + @override String get modalBarrierDismissLabel => 'Ignorar'; @@ -15748,6 +16528,9 @@ class MaterialLocalizationGl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Buscar'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleccionar todo'; @@ -15772,6 +16555,9 @@ class MaterialLocalizationGl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Non se seleccionaron elementos'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Mostrar contas'; @@ -15861,7 +16647,7 @@ class MaterialLocalizationGsw extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Schließen'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Maximiert'; @override String get collapsedIconTapHint => 'Maximieren'; @@ -15921,22 +16707,22 @@ class MaterialLocalizationGsw extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigationsmenü'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Minimiert'; @override String get expandedIconTapHint => 'Minimieren'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'Zum Maximieren doppeltippen'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Für weitere Details maximieren'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'Zum Minimieren doppeltippen'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Minimieren'; @override String get firstPageTooltip => 'Erste Seite'; @@ -16124,9 +16910,15 @@ class MaterialLocalizationGsw extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lizenzen'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menü in der Menüleiste'; + @override + String get menuDismissLabel => 'Menü schließen'; + @override String get modalBarrierDismissLabel => 'Schließen'; @@ -16226,6 +17018,9 @@ class MaterialLocalizationGsw extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Suchen'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alle auswählen'; @@ -16250,6 +17045,9 @@ class MaterialLocalizationGsw extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Konten anzeigen'; @@ -16339,7 +17137,7 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'બંધ કરો'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'મોટી કરી'; @override String get collapsedIconTapHint => 'વિસ્તૃત કરો'; @@ -16399,22 +17197,22 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { String get drawerLabel => 'નૅવિગેશન મેનૂ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'નાની કરી'; @override String get expandedIconTapHint => 'સંકુચિત કરો'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'મોટી કરવા માટે બે વાર ટૅપ કરો'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'વધુ વિગતો માટે મોટી કરો'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'નાની કરવા માટે બે વાર ટૅપ કરો'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'નાની કરો'; @override String get firstPageTooltip => 'પહેલું પેજ'; @@ -16602,9 +17400,15 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'લાઇસન્સ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'મેનૂ બાર મેનૂ'; + @override + String get menuDismissLabel => 'મેનૂ છોડી દો'; + @override String get modalBarrierDismissLabel => 'છોડી દો'; @@ -16690,7 +17494,7 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { String get saveButtonLabel => 'સાચવો'; @override - String get scanTextButtonLabel => 'ટેક્સ્ટ સ્કેન કરો'; + String get scanTextButtonLabel => 'ટેક્સ્ટ સ્કૅન કરો'; @override String get scrimLabel => 'સ્ક્રિમ'; @@ -16704,6 +17508,9 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'શોધો'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'બધા પસંદ કરો'; @@ -16728,6 +17535,9 @@ class MaterialLocalizationGu extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'એકાઉન્ટ બતાવો'; @@ -16817,7 +17627,7 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'סגירה'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'מורחב'; @override String get collapsedIconTapHint => 'הרחבה'; @@ -16877,22 +17687,22 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { String get drawerLabel => 'תפריט ניווט'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'מכווץ'; @override String get expandedIconTapHint => 'כיווץ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'כדי להרחיב, יש להקיש הקשה כפולה'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ניתן להרחיב להצגת פרטים נוספים'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'כדי לכווץ, יש להקיש הקשה כפולה'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'כיווץ'; @override String get firstPageTooltip => 'לדף הראשון'; @@ -17080,9 +17890,15 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'רישיונות'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'תפריט בסרגל התפריטים'; + @override + String get menuDismissLabel => 'סגירת התפריט'; + @override String get modalBarrierDismissLabel => 'סגירה'; @@ -17168,7 +17984,7 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { String get saveButtonLabel => 'שמירה'; @override - String get scanTextButtonLabel => 'סרוק טקסט'; + String get scanTextButtonLabel => 'סריקת טקסט'; @override String get scrimLabel => 'מיסוך'; @@ -17182,6 +17998,9 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'חיפוש'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'בחירת הכול'; @@ -17206,6 +18025,9 @@ class MaterialLocalizationHe extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'הצגת החשבונות'; @@ -17295,7 +18117,7 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'बंद करें'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'बड़ा किया गया'; @override String get collapsedIconTapHint => 'बड़ा करें'; @@ -17355,22 +18177,22 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { String get drawerLabel => 'नेविगेशन मेन्यू'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'छोटा किया गया'; @override String get expandedIconTapHint => 'छोटा करें'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'बड़ा करने के लिए दो बार टैप करें'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ज़्यादा जानकारी के लिए बड़ा करें'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'छोटा करने के लिए दो बार टैप करें'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'छोटा करें'; @override String get firstPageTooltip => 'पहला पेज'; @@ -17558,9 +18380,15 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'लाइसेंस'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'मेन्यू बार का मेन्यू'; + @override + String get menuDismissLabel => 'मेन्यू खारिज करें'; + @override String get modalBarrierDismissLabel => 'खारिज करें'; @@ -17646,7 +18474,7 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { String get saveButtonLabel => 'सेव करें'; @override - String get scanTextButtonLabel => 'पाठ स्कैन करें'; + String get scanTextButtonLabel => 'टेक्स्ट स्कैन करें'; @override String get scrimLabel => 'स्क्रिम'; @@ -17660,6 +18488,9 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'खोजें'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सभी को चुनें'; @@ -17684,6 +18515,9 @@ class MaterialLocalizationHi extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'खाते दिखाएं'; @@ -17773,7 +18607,7 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zatvaranje'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Prošireno'; @override String get collapsedIconTapHint => 'Proširi'; @@ -17833,22 +18667,22 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigacijski izbornik'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Sažeto'; @override String get expandedIconTapHint => 'Sažmi'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dvaput dodirnite za proširivanje'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Proširite da biste saznali više'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dvaput dodirnite za sažimanje'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Sažmi'; @override String get firstPageTooltip => 'Prva stranica'; @@ -18036,9 +18870,15 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licence'; + @override + String get lookUpButtonLabel => 'Pogled prema gore'; + @override String get menuBarMenuLabel => 'Izbornik trake izbornika'; + @override + String get menuDismissLabel => 'Odbacivanje izbornika'; + @override String get modalBarrierDismissLabel => 'Odbaci'; @@ -18124,7 +18964,7 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Spremi'; @override - String get scanTextButtonLabel => 'Skeniraj tekst'; + String get scanTextButtonLabel => 'Skeniranje teksta'; @override String get scrimLabel => 'Rubno'; @@ -18138,6 +18978,9 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Pretražite'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Odaberi sve'; @@ -18162,6 +19005,9 @@ class MaterialLocalizationHr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Prikažite račune'; @@ -18251,7 +19097,7 @@ class MaterialLocalizationHu extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Bezárás'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Kibontva'; @override String get collapsedIconTapHint => 'Kibontás'; @@ -18311,22 +19157,22 @@ class MaterialLocalizationHu extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigációs menü'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Összecsukva'; @override String get expandedIconTapHint => 'Összecsukás'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'duplán koppintva kibonthatja'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Bontsa ki a további részletek megtekintéséhez'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'duplán koppintva összecsukhatja'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Összecsukás'; @override String get firstPageTooltip => 'Első oldal'; @@ -18514,9 +19360,15 @@ class MaterialLocalizationHu extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencek'; + @override + String get lookUpButtonLabel => 'Felfelé nézés'; + @override String get menuBarMenuLabel => 'Menüsor menüje'; + @override + String get menuDismissLabel => 'Menü bezárása'; + @override String get modalBarrierDismissLabel => 'Elvetés'; @@ -18616,6 +19468,9 @@ class MaterialLocalizationHu extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Keresés'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Összes kijelölése'; @@ -18640,6 +19495,9 @@ class MaterialLocalizationHu extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Fiókok megjelenítése'; @@ -18729,7 +19587,7 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Փակել'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Ծավալված է'; @override String get collapsedIconTapHint => 'Ծավալել'; @@ -18789,22 +19647,22 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { String get drawerLabel => 'Նավիգացիայի ընտրացանկ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Ծալված է'; @override String get expandedIconTapHint => 'Ծալել'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'կրկնակի հպեք ծավալելու համար'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ծավալեք՝ մանրամասները տեսնելու համար'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'կրկնակի հպեք ծալելու համար'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Ծալել'; @override String get firstPageTooltip => 'Առաջին էջ'; @@ -18992,9 +19850,15 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Արտոնագրեր'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Ընտրացանկի գոտու ընտրացանկ'; + @override + String get menuDismissLabel => 'Փակել ընտրացանկը'; + @override String get modalBarrierDismissLabel => 'Փակել'; @@ -19080,7 +19944,7 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Պահել'; @override - String get scanTextButtonLabel => 'Սկանավորեք տեքստը'; + String get scanTextButtonLabel => 'Սկանավորել տեքստ'; @override String get scrimLabel => 'Դիմակ'; @@ -19094,6 +19958,9 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Որոնել'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Նշել բոլորը'; @@ -19118,6 +19985,9 @@ class MaterialLocalizationHy extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Տողերը ընտրված չեն'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Ցույց տալ հաշիվները'; @@ -19207,7 +20077,7 @@ class MaterialLocalizationId extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Tutup'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Diluaskan'; @override String get collapsedIconTapHint => 'Luaskan'; @@ -19267,22 +20137,22 @@ class MaterialLocalizationId extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu navigasi'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Diciutkan'; @override String get expandedIconTapHint => 'Ciutkan'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ketuk dua kali untuk meluaskan'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Luaskan untuk mengetahui detail selengkapnya'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ketuk dua kali untuk menciutkan'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Ciutkan'; @override String get firstPageTooltip => 'Halaman pertama'; @@ -19470,9 +20340,15 @@ class MaterialLocalizationId extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisensi'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu panel menu'; + @override + String get menuDismissLabel => 'Tutup menu'; + @override String get modalBarrierDismissLabel => 'Tutup'; @@ -19572,6 +20448,9 @@ class MaterialLocalizationId extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Telusuri'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pilih semua'; @@ -19596,6 +20475,9 @@ class MaterialLocalizationId extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Tampilkan akun'; @@ -19685,7 +20567,7 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Loka'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Stækkað'; @override String get collapsedIconTapHint => 'Stækka'; @@ -19745,22 +20627,22 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { String get drawerLabel => 'Yfirlitsvalmynd'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Minnkað'; @override String get expandedIconTapHint => 'Draga saman'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ýttu tvisvar til að stækka'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Stækka til að sjá frekari upplýsingar'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ýttu tvisvar til að minnka'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Minnka'; @override String get firstPageTooltip => 'Fyrsta síða'; @@ -19948,9 +20830,15 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Leyfi'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Valmyndarstika'; + @override + String get menuDismissLabel => 'Loka valmynd'; + @override String get modalBarrierDismissLabel => 'Hunsa'; @@ -20036,7 +20924,7 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Vista'; @override - String get scanTextButtonLabel => 'Skannaðu texta'; + String get scanTextButtonLabel => 'Skanna texta'; @override String get scrimLabel => 'Möskvi'; @@ -20050,6 +20938,9 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Leit'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velja allt'; @@ -20074,6 +20965,9 @@ class MaterialLocalizationIs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Sýna reikninga'; @@ -20163,7 +21057,7 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Chiudi'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Espanso'; @override String get collapsedIconTapHint => 'Espandi'; @@ -20223,22 +21117,22 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu di navigazione'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Compresso'; @override String get expandedIconTapHint => 'Comprimi'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'Tocca due volte per espandere'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'espandere e visualizzare altri dettagli'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tocca due volte per comprimere'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'comprimere'; @override String get firstPageTooltip => 'Prima pagina'; @@ -20426,9 +21320,15 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenze'; + @override + String get lookUpButtonLabel => 'Cerca'; + @override String get menuBarMenuLabel => 'Menu barra dei menu'; + @override + String get menuDismissLabel => 'Ignora menu'; + @override String get modalBarrierDismissLabel => 'Ignora'; @@ -20514,7 +21414,7 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Salva'; @override - String get scanTextButtonLabel => 'Scansiona il testo'; + String get scanTextButtonLabel => 'Scansiona testo'; @override String get scrimLabel => 'Rete'; @@ -20528,6 +21428,9 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Cerca'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Seleziona tutto'; @@ -20552,6 +21455,9 @@ class MaterialLocalizationIt extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Mostra account'; @@ -20641,7 +21547,7 @@ class MaterialLocalizationJa extends GlobalMaterialLocalizations { String get closeButtonTooltip => '閉じる'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => '開きました'; @override String get collapsedIconTapHint => '展開'; @@ -20701,22 +21607,22 @@ class MaterialLocalizationJa extends GlobalMaterialLocalizations { String get drawerLabel => 'ナビゲーション メニュー'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => '閉じました'; @override String get expandedIconTapHint => '折りたたむ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => '開くにはダブルタップします'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => '開いて詳細を表示'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ダブルタップすると閉じます'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => '閉じる'; @override String get firstPageTooltip => '最初のページ'; @@ -20904,9 +21810,15 @@ class MaterialLocalizationJa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ライセンス'; + @override + String get lookUpButtonLabel => '調べる'; + @override String get menuBarMenuLabel => 'メニューバーのメニュー'; + @override + String get menuDismissLabel => 'メニューを閉じる'; + @override String get modalBarrierDismissLabel => '閉じる'; @@ -21006,6 +21918,9 @@ class MaterialLocalizationJa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => '検索'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'すべて選択'; @@ -21030,6 +21945,9 @@ class MaterialLocalizationJa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'アカウントを表示'; @@ -21119,7 +22037,7 @@ class MaterialLocalizationKa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'დახურვა'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'გაფართოებულია'; @override String get collapsedIconTapHint => 'გაშლა'; @@ -21179,22 +22097,22 @@ class MaterialLocalizationKa extends GlobalMaterialLocalizations { String get drawerLabel => 'ნავიგაციის მენიუ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ჩაკეცილია'; @override String get expandedIconTapHint => 'ჩაკეცვა'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'გასაფართოებლად ორჯერ შეეხეთ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'მეტი დეტალებისთვის გააფართოეთ'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ორმაგად შეეხეთ ჩასაკეცად'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ჩაკეცვა'; @override String get firstPageTooltip => 'პირველი გვერდი'; @@ -21382,9 +22300,15 @@ class MaterialLocalizationKa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ლიცენზიები'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'მენიუს ზოლის მენიუ'; + @override + String get menuDismissLabel => 'მენიუს უარყოფა'; + @override String get modalBarrierDismissLabel => 'დახურვა'; @@ -21484,6 +22408,9 @@ class MaterialLocalizationKa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ძიება'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ყველას არჩევა'; @@ -21508,6 +22435,9 @@ class MaterialLocalizationKa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ანგარიშების ჩვენება'; @@ -21597,7 +22527,7 @@ class MaterialLocalizationKk extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Жабу'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Жайылды'; @override String get collapsedIconTapHint => 'Жаю'; @@ -21657,22 +22587,22 @@ class MaterialLocalizationKk extends GlobalMaterialLocalizations { String get drawerLabel => 'Навигация мәзірі'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Жиылды'; @override String get expandedIconTapHint => 'Жию'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'жаю үшін екі рет түртіңіз'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Толық мәлімет алу үшін жайыңыз.'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'жию үшін екі рет түртіңіз'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Жию'; @override String get firstPageTooltip => 'Бірінші бет'; @@ -21860,9 +22790,15 @@ class MaterialLocalizationKk extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лицензиялар'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Мәзір жолағының мәзірі'; + @override + String get menuDismissLabel => 'Мәзірді жабу'; + @override String get modalBarrierDismissLabel => 'Жабу'; @@ -21962,6 +22898,9 @@ class MaterialLocalizationKk extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Іздеу'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Барлығын таңдау'; @@ -21986,6 +22925,9 @@ class MaterialLocalizationKk extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Тармақ таңдалмаған'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Аккаунттарды көрсету'; @@ -22075,7 +23017,7 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'បិទ'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'បាន​ពង្រីក'; @override String get collapsedIconTapHint => 'ពង្រីក'; @@ -22135,22 +23077,22 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { String get drawerLabel => 'ម៉ឺនុយរុករក'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'បាន​បង្រួម'; @override String get expandedIconTapHint => 'បង្រួម'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ចុចពីរដង ដើម្បីពង្រីក'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ពង្រីក​ដើម្បីទទួលបាន​ព័ត៌មានលម្អិត​បន្ថែម'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ចុចពីរដង ដើម្បីបង្រួម'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'បង្រួម'; @override String get firstPageTooltip => 'ទំព័រ​ដំបូង'; @@ -22338,9 +23280,15 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'អាជ្ញាបណ្ណ'; + @override + String get lookUpButtonLabel => 'រកមើល'; + @override String get menuBarMenuLabel => 'ម៉ឺនុយរបារម៉ឺនុយ'; + @override + String get menuDismissLabel => 'ច្រានចោល​ម៉ឺនុយ'; + @override String get modalBarrierDismissLabel => 'ច្រាន​ចោល'; @@ -22426,7 +23374,7 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { String get saveButtonLabel => 'រក្សាទុក'; @override - String get scanTextButtonLabel => 'ស្កេនអត្ថបទ'; + String get scanTextButtonLabel => 'ស្កេន​អក្សរ'; @override String get scrimLabel => 'ផ្ទាំងស្រអាប់'; @@ -22440,6 +23388,9 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ស្វែងរក'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ជ្រើសរើស​ទាំងអស់'; @@ -22464,6 +23415,9 @@ class MaterialLocalizationKm extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'បង្ហាញគណនី'; @@ -22553,7 +23507,7 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { String get closeButtonTooltip => '\u{cae}\u{cc1}\u{c9a}\u{ccd}\u{c9a}\u{cbf}\u{cb0}\u{cbf}'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => '\u{cb5}\u{cbf}\u{cb8}\u{ccd}\u{ca4}\u{cb0}\u{cbf}\u{cb8}\u{cb2}\u{cbe}\u{c97}\u{cbf}\u{ca6}\u{cc6}'; @override String get collapsedIconTapHint => '\u{cb5}\u{cbf}\u{cb8}\u{ccd}\u{ca4}\u{cb0}\u{cbf}\u{cb8}\u{cbf}'; @@ -22613,22 +23567,22 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { String get drawerLabel => '\u{ca8}\u{ccd}\u{caf}\u{cbe}\u{cb5}\u{cbf}\u{c97}\u{cc7}\u{cb6}\u{ca8}\u{ccd}\u{200c}\u{20}\u{cae}\u{cc6}\u{ca8}\u{cc1}'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => '\u{c95}\u{cc1}\u{c97}\u{ccd}\u{c97}\u{cbf}\u{cb8}\u{cb2}\u{cbe}\u{c97}\u{cbf}\u{ca6}\u{cc6}'; @override String get expandedIconTapHint => '\u{c95}\u{cc1}\u{c97}\u{ccd}\u{c97}\u{cbf}\u{cb8}\u{cbf}'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => '\u{cb5}\u{cbf}\u{cb8}\u{ccd}\u{ca4}\u{cb0}\u{cbf}\u{cb8}\u{cb2}\u{cc1}\u{20}\u{ca1}\u{cac}\u{cb2}\u{ccd}\u{20}\u{c9f}\u{ccd}\u{caf}\u{cbe}\u{caa}\u{ccd}\u{20}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => '\u{c87}\u{ca8}\u{ccd}\u{ca8}\u{cb7}\u{ccd}\u{c9f}\u{cc1}\u{20}\u{cb5}\u{cbf}\u{cb5}\u{cb0}\u{c97}\u{cb3}\u{cbf}\u{c97}\u{cbe}\u{c97}\u{cbf}\u{20}\u{cb5}\u{cbf}\u{cb8}\u{ccd}\u{ca4}\u{cb0}\u{cbf}\u{cb8}\u{cbf}'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => '\u{c95}\u{cc1}\u{c97}\u{ccd}\u{c97}\u{cbf}\u{cb8}\u{cb2}\u{cc1}\u{20}\u{ca1}\u{cac}\u{cb2}\u{ccd}\u{20}\u{c9f}\u{ccd}\u{caf}\u{cbe}\u{caa}\u{ccd}\u{20}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => '\u{c95}\u{cc1}\u{c97}\u{ccd}\u{c97}\u{cbf}\u{cb8}\u{cbf}'; @override String get firstPageTooltip => '\u{cae}\u{cca}\u{ca6}\u{cb2}\u{20}\u{caa}\u{cc1}\u{c9f}'; @@ -22816,9 +23770,15 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { @override String get licensesPageTitle => '\u{caa}\u{cb0}\u{cb5}\u{cbe}\u{ca8}\u{c97}\u{cbf}\u{c97}\u{cb3}\u{cc1}'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => '\u{cae}\u{cc6}\u{ca8}\u{cc1}\u{20}\u{cac}\u{cbe}\u{cb0}\u{ccd}\u{200c}\u{20}\u{cae}\u{cc6}\u{ca8}\u{cc1}'; + @override + String get menuDismissLabel => '\u{cae}\u{cc6}\u{ca8}\u{cc1}\u{cb5}\u{ca8}\u{ccd}\u{ca8}\u{cc1}\u{20}\u{cb5}\u{c9c}\u{cbe}\u{c97}\u{cc6}\u{cc2}\u{cb3}\u{cbf}\u{cb8}\u{cbf}'; + @override String get modalBarrierDismissLabel => '\u{cb5}\u{c9c}\u{cbe}\u{c97}\u{cca}\u{cb3}\u{cbf}\u{cb8}\u{cbf}'; @@ -22901,7 +23861,7 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { String get rowsPerPageTitle => '\u{caa}\u{ccd}\u{cb0}\u{ca4}\u{cbf}\u{20}\u{caa}\u{cc1}\u{c9f}\u{c95}\u{ccd}\u{c95}\u{cc6}\u{20}\u{cb8}\u{cbe}\u{cb2}\u{cc1}\u{c97}\u{cb3}\u{cc1}\u{3a}'; @override - String get saveButtonLabel => '\u{c89}\u{cb3}\u{cbf}\u{cb8}\u{cbf}'; + String get saveButtonLabel => '\u{cb8}\u{cc7}\u{cb5}\u{ccd}\u{20}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; @override String get scanTextButtonLabel => '\u{caa}\u{ca0}\u{ccd}\u{caf}\u{cb5}\u{ca8}\u{ccd}\u{ca8}\u{cc1}\u{20}\u{cb8}\u{ccd}\u{c95}\u{ccd}\u{caf}\u{cbe}\u{ca8}\u{ccd}\u{20}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; @@ -22918,6 +23878,9 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { @override String get searchFieldLabel => '\u{cb9}\u{cc1}\u{ca1}\u{cc1}\u{c95}\u{cbf}'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '\u{c8e}\u{cb2}\u{ccd}\u{cb2}\u{cb5}\u{ca8}\u{ccd}\u{ca8}\u{cc2}\u{20}\u{c86}\u{caf}\u{ccd}\u{c95}\u{cc6}\u{20}\u{cae}\u{cbe}\u{ca1}\u{cbf}'; @@ -22942,6 +23905,9 @@ class MaterialLocalizationKn extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => '\u{c96}\u{cbe}\u{ca4}\u{cc6}\u{c97}\u{cb3}\u{ca8}\u{ccd}\u{ca8}\u{cc1}\u{20}\u{ca4}\u{ccb}\u{cb0}\u{cbf}\u{cb8}\u{cbf}'; @@ -23031,7 +23997,7 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { String get closeButtonTooltip => '닫기'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => '펼침'; @override String get collapsedIconTapHint => '펼치기'; @@ -23091,22 +24057,22 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { String get drawerLabel => '탐색 메뉴'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => '접힘'; @override String get expandedIconTapHint => '접기'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => '두 번 탭하여 펼치기'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => '자세히 알아보려면 펼치기'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => '두 번 탭하여 접기'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => '접기'; @override String get firstPageTooltip => '첫 페이지'; @@ -23294,9 +24260,15 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { @override String get licensesPageTitle => '라이선스'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => '메뉴 바 메뉴'; + @override + String get menuDismissLabel => '메뉴 닫기'; + @override String get modalBarrierDismissLabel => '닫기'; @@ -23382,7 +24354,7 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { String get saveButtonLabel => '저장'; @override - String get scanTextButtonLabel => '스캔 텍스트'; + String get scanTextButtonLabel => '텍스트 스캔'; @override String get scrimLabel => '스크림'; @@ -23396,6 +24368,9 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { @override String get searchFieldLabel => '검색'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '전체 선택'; @@ -23420,6 +24395,9 @@ class MaterialLocalizationKo extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => '계정 표시'; @@ -23509,7 +24487,7 @@ class MaterialLocalizationKy extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Жабуу'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Жайылып көрсөтүлдү'; @override String get collapsedIconTapHint => 'Жайып көрсөтүү'; @@ -23569,22 +24547,22 @@ class MaterialLocalizationKy extends GlobalMaterialLocalizations { String get drawerLabel => 'Чабыттоо менюсу'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Жыйыштырылды'; @override String get expandedIconTapHint => 'Жыйыштыруу'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'жайып көрсөтүү үчүн эки жолу таптаңыз'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Толук маалымат алуу үчүн жайып көрүңүз'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'жыйыштыруу үчүн эки жолу таптаңыз'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Жыйыштыруу'; @override String get firstPageTooltip => 'Биринчи бет'; @@ -23772,9 +24750,15 @@ class MaterialLocalizationKy extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Уруксаттамалар'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Меню тилкеси менюсу'; + @override + String get menuDismissLabel => 'Менюну жабуу'; + @override String get modalBarrierDismissLabel => 'Жабуу'; @@ -23874,6 +24858,9 @@ class MaterialLocalizationKy extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Издөө'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Баарын тандоо'; @@ -23898,6 +24885,9 @@ class MaterialLocalizationKy extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Аккаунттарды көрсөтүү'; @@ -23987,7 +24977,7 @@ class MaterialLocalizationLo extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ປິດ'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ຂະຫຍາຍແລ້ວ'; @override String get collapsedIconTapHint => 'ຂະຫຍາຍ'; @@ -24047,22 +25037,22 @@ class MaterialLocalizationLo extends GlobalMaterialLocalizations { String get drawerLabel => 'ເມນູນຳທາງ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ຫຍໍ້ລົງແລ້ວ'; @override String get expandedIconTapHint => 'ຫຍໍ້ເຂົ້າ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ແຕະສອງເທື່ອເພື່ອຂະຫຍາຍ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ຂະຫຍາຍສຳລັບຂໍ້ມູນເພີ່ມເຕີມ'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ແຕະສອງເທື່ອເພື່ອຫຍໍ້ລົງ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ຫຍໍ້ລົງ'; @override String get firstPageTooltip => 'ໜ້າທຳອິດ'; @@ -24250,9 +25240,15 @@ class MaterialLocalizationLo extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ໃບອະນຸຍາດ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'ເມນູແຖບເມນູ'; + @override + String get menuDismissLabel => 'ປິດເມນູ'; + @override String get modalBarrierDismissLabel => 'ປິດໄວ້'; @@ -24352,6 +25348,9 @@ class MaterialLocalizationLo extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ຊອກຫາ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ເລືອກທັງໝົດ'; @@ -24376,6 +25375,9 @@ class MaterialLocalizationLo extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ສະແດງບັນຊີ'; @@ -24465,7 +25467,7 @@ class MaterialLocalizationLt extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Uždaryti'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Išskleista'; @override String get collapsedIconTapHint => 'Išskleisti'; @@ -24525,22 +25527,22 @@ class MaterialLocalizationLt extends GlobalMaterialLocalizations { String get drawerLabel => 'Naršymo meniu'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Sutraukta'; @override String get expandedIconTapHint => 'Sutraukti'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dukart palieskite, kad išskleistumėte'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Išskleiskite, jei reikia daugiau išsamios informacijos'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dukart palieskite, kad sutrauktumėte'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Sutraukti'; @override String get firstPageTooltip => 'Pirmas puslapis'; @@ -24728,9 +25730,15 @@ class MaterialLocalizationLt extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencijos'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Meniu juostos meniu'; + @override + String get menuDismissLabel => 'Atsisakyti meniu'; + @override String get modalBarrierDismissLabel => 'Atsisakyti'; @@ -24830,6 +25838,9 @@ class MaterialLocalizationLt extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Paieška'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pasirinkti viską'; @@ -24854,6 +25865,9 @@ class MaterialLocalizationLt extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Rodyti paskyras'; @@ -24943,7 +25957,7 @@ class MaterialLocalizationLv extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Aizvērt'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Izvērsts'; @override String get collapsedIconTapHint => 'Izvērst'; @@ -25003,22 +26017,22 @@ class MaterialLocalizationLv extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigācijas izvēlne'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Sakļauts'; @override String get expandedIconTapHint => 'Sakļaut'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dubultskāriens, lai izvērstu'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Izvērst, lai iegūtu plašāku informāciju'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dubultskāriens, lai sakļautu'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Sakļaut'; @override String get firstPageTooltip => 'Pirmā lapa'; @@ -25206,9 +26220,15 @@ class MaterialLocalizationLv extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licences'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Izvēļņu joslas izvēlne'; + @override + String get menuDismissLabel => 'Nerādīt izvēlni'; + @override String get modalBarrierDismissLabel => 'Nerādīt'; @@ -25308,6 +26328,9 @@ class MaterialLocalizationLv extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Meklēt'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Atlasīt visu'; @@ -25332,6 +26355,9 @@ class MaterialLocalizationLv extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Nav atlasītu vienumu'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Rādīt kontus'; @@ -25421,7 +26447,7 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Затвори'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Проширено'; @override String get collapsedIconTapHint => 'Прошири'; @@ -25481,28 +26507,28 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { String get drawerLabel => 'Мени за навигација'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Собрано'; @override String get expandedIconTapHint => 'Собери'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'допри двапати за проширување'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Прошири за повеќе детали'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'допрете двапати за собирање'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Собери'; @override String get firstPageTooltip => 'Прва страница'; @override - String get hideAccountsLabel => 'Сокриј сметки'; + String get hideAccountsLabel => 'Скриј сметки'; @override String get inputDateModeButtonLabel => 'Префрли на внесување'; @@ -25684,9 +26710,15 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лиценци'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Мени на лентата со мени'; + @override + String get menuDismissLabel => 'Отфрлете го менито'; + @override String get modalBarrierDismissLabel => 'Отфрли'; @@ -25772,7 +26804,7 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Зачувај'; @override - String get scanTextButtonLabel => 'Скенирајте текст'; + String get scanTextButtonLabel => 'Скенирајте го текстот'; @override String get scrimLabel => 'Скрим'; @@ -25786,6 +26818,9 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Пребарувајте'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Избери ги сите'; @@ -25810,6 +26845,9 @@ class MaterialLocalizationMk extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Прикажи сметки'; @@ -25899,7 +26937,7 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'അടയ്‌ക്കുക'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'വികസിപ്പിച്ചു'; @override String get collapsedIconTapHint => 'വികസിപ്പിക്കുക'; @@ -25959,22 +26997,22 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { String get drawerLabel => 'നാവിഗേഷൻ മെനു'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ചുരുക്കി'; @override String get expandedIconTapHint => 'ചുരുക്കുക'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'വികസിപ്പിക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യുക'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'കൂടുതൽ വിശദാംശങ്ങൾക്ക് വികസിപ്പിക്കുക'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ചുരുക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യുക'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ചുരുക്കുക'; @override String get firstPageTooltip => 'ആദ്യ പേജ്'; @@ -26162,9 +27200,15 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ലൈസൻസുകൾ'; + @override + String get lookUpButtonLabel => 'മുകളിലേക്ക് നോക്കുക'; + @override String get menuBarMenuLabel => 'മെനു ബാർ മെനു'; + @override + String get menuDismissLabel => 'മെനു ഡിസ്മിസ് ചെയ്യുക'; + @override String get modalBarrierDismissLabel => 'നിരസിക്കുക'; @@ -26250,7 +27294,7 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { String get saveButtonLabel => 'സംരക്ഷിക്കുക'; @override - String get scanTextButtonLabel => 'ടെക്സ്റ്റ് സ്കാൻ ചെയ്യുക'; + String get scanTextButtonLabel => 'ടെക്സ്റ്റ് സ്‌കാൻ ചെയ്യുക'; @override String get scrimLabel => 'സ്ക്രിം'; @@ -26264,6 +27308,9 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'തിരയുക'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'എല്ലാം തിരഞ്ഞെടുക്കുക'; @@ -26288,6 +27335,9 @@ class MaterialLocalizationMl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'അക്കൗണ്ടുകൾ കാണിക്കുക'; @@ -26377,7 +27427,7 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Хаах'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Дэлгэсэн'; @override String get collapsedIconTapHint => 'Дэлгэх'; @@ -26437,22 +27487,22 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { String get drawerLabel => 'Навигацын цэс'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Хураасан'; @override String get expandedIconTapHint => 'Буулгах'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'дэлгэхийн тулд хоёр товшино уу'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Илүү дэлгэрэнгүй авах бол дэлгэнэ үү'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'хураахын тулд хоёр товшино уу'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Хураах'; @override String get firstPageTooltip => 'Эхний хуудас'; @@ -26640,9 +27690,15 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лиценз'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Цэсний талбарын цэс'; + @override + String get menuDismissLabel => 'Цэсийг хаах'; + @override String get modalBarrierDismissLabel => 'Үл хэрэгсэх'; @@ -26728,7 +27784,7 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Хадгалах'; @override - String get scanTextButtonLabel => 'Текст сканнердах'; + String get scanTextButtonLabel => 'Текстийг скан хийх'; @override String get scrimLabel => 'Скрим'; @@ -26742,6 +27798,9 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Хайх'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Бүгдийг сонгох'; @@ -26766,6 +27825,9 @@ class MaterialLocalizationMn extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Бичлэг сонгоогүй байна'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Бүртгэлүүдийг харуулах'; @@ -26855,7 +27917,7 @@ class MaterialLocalizationMr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'बंद करा'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'विस्तार केले'; @override String get collapsedIconTapHint => 'विस्तार करा'; @@ -26915,22 +27977,22 @@ class MaterialLocalizationMr extends GlobalMaterialLocalizations { String get drawerLabel => 'नेव्हिगेशन मेनू'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'कोलॅप्स केले'; @override String get expandedIconTapHint => 'कोलॅप्स करा'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'विस्तार करण्‍यासाठी दोनदा टॅप करा'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'आणखी तपशिलांसाठी विस्तार करा'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'कोलॅप्स करण्यासाठी दोनदा टॅप करा'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'कोलॅप्स करा'; @override String get firstPageTooltip => 'पहिले पेज'; @@ -27118,9 +28180,15 @@ class MaterialLocalizationMr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'परवाने'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'मेनू बार मेनू'; + @override + String get menuDismissLabel => 'मेनू डिसमिस करा'; + @override String get modalBarrierDismissLabel => 'डिसमिस करा'; @@ -27220,6 +28288,9 @@ class MaterialLocalizationMr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'शोध'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सर्व निवडा'; @@ -27244,6 +28315,9 @@ class MaterialLocalizationMr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'कोणतेही आयटम निवडलेले नाहीत'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'खाती दर्शवा'; @@ -27333,7 +28407,7 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Tutup'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Dikembangkan'; @override String get collapsedIconTapHint => 'Kembangkan'; @@ -27393,22 +28467,22 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu navigasi'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Dikuncupkan'; @override String get expandedIconTapHint => 'Runtuhkan'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ketik dua kali untuk kembangkan'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Kembangkan untuk mendapatkan butiran lanjut'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ketik dua kali untuk kuncupkan'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Kuncupkan'; @override String get firstPageTooltip => 'Halaman pertama'; @@ -27596,9 +28670,15 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lesen'; + @override + String get lookUpButtonLabel => 'Lihat ke Atas'; + @override String get menuBarMenuLabel => 'Menu bar menu'; + @override + String get menuDismissLabel => 'Ketepikan menu'; + @override String get modalBarrierDismissLabel => 'Tolak'; @@ -27684,7 +28764,7 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Simpan'; @override - String get scanTextButtonLabel => 'Pindai teks'; + String get scanTextButtonLabel => 'Imbas teks'; @override String get scrimLabel => 'Scrim'; @@ -27698,6 +28778,9 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Cari'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Pilih semua'; @@ -27722,6 +28805,9 @@ class MaterialLocalizationMs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Tiada item dipilih'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Tunjukkan akaun'; @@ -27811,7 +28897,7 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ပိတ်ရန်'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ဖြန့်ထားသည်'; @override String get collapsedIconTapHint => 'ချဲ့ရန်'; @@ -27871,22 +28957,22 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { String get drawerLabel => 'လမ်းညွှန် မီနူး'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ခေါက်ထားသည်'; @override String get expandedIconTapHint => 'လျှော့ပြရန်'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ဖြန့်ရန် နှစ်ချက်တို့ပါ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'အသေးစိတ်အတွက် ဖြန့်ရန်'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ခေါက်ရန် နှစ်ချက်တို့ပါ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ခေါက်ရန်'; @override String get firstPageTooltip => 'ပထမ စာမျက်နှာ'; @@ -28074,9 +29160,15 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'လိုင်စင်များ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'မီနူးဘား မီနူး'; + @override + String get menuDismissLabel => 'မီနူးကိုပယ်ပါ'; + @override String get modalBarrierDismissLabel => 'ပယ်ရန်'; @@ -28162,7 +29254,7 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { String get saveButtonLabel => 'သိမ်းရန်'; @override - String get scanTextButtonLabel => 'စာသားကို စကင်ဖတ်ပါ။'; + String get scanTextButtonLabel => 'စာသား စကင်ဖတ်ရန်'; @override String get scrimLabel => 'Scrim'; @@ -28176,6 +29268,9 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ရှာဖွေရန်'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'အားလုံး ရွေးရန်'; @@ -28200,6 +29295,9 @@ class MaterialLocalizationMy extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'အကောင့်များကို ပြရန်'; @@ -28289,7 +29387,7 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Lukk'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Vises'; @override String get collapsedIconTapHint => 'Vis'; @@ -28349,22 +29447,22 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigasjonsmeny'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Skjules'; @override String get expandedIconTapHint => 'Skjul'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dobbelttrykk for å vise'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Vis for å se mer informasjon'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dobbelttrykk for å skjule'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Skjul'; @override String get firstPageTooltip => 'Første side'; @@ -28552,9 +29650,15 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisenser'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Meny med menylinje'; + @override + String get menuDismissLabel => 'Lukk menyen'; + @override String get modalBarrierDismissLabel => 'Avvis'; @@ -28640,7 +29744,7 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Lagre'; @override - String get scanTextButtonLabel => 'Scan tekst'; + String get scanTextButtonLabel => 'Skann tekst'; @override String get scrimLabel => 'Vev'; @@ -28654,6 +29758,9 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Søk'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velg alle'; @@ -28678,6 +29785,9 @@ class MaterialLocalizationNb extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Vis kontoer'; @@ -28767,7 +29877,7 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'बन्द गर्नुहोस्'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'एक्स्पान्ड गरियो'; @override String get collapsedIconTapHint => 'विस्तार गर्नुहोस्'; @@ -28827,22 +29937,22 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { String get drawerLabel => 'नेभिगेसन मेनु'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'कोल्याप्स गरियो'; @override String get expandedIconTapHint => 'संक्षिप्त गर्नुहोस्'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'एक्स्पान्ड गर्न डबल ट्याप गर्नुहोस्'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'थप विवरण हेर्न एक्स्पान्ड गर्नुहोस्'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'कोल्याप्स गर्न डबल ट्याप गर्नुहोस्'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'कोल्याप्स गर्नुहोस्'; @override String get firstPageTooltip => 'प्रथम पेज'; @@ -29030,9 +30140,15 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'इजाजतपत्रहरू'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => '"मेनु बार" मेनु'; + @override + String get menuDismissLabel => 'मेनु खारेज गर्नुहोस्'; + @override String get modalBarrierDismissLabel => 'खारेज गर्नुहोस्'; @@ -29043,7 +30159,7 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { String get nextMonthTooltip => 'अर्को महिना'; @override - String get nextPageTooltip => 'अर्को पृष्ठ'; + String get nextPageTooltip => 'अर्को पेज'; @override String get okButtonLabel => 'ठिक छ'; @@ -29118,7 +30234,7 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { String get saveButtonLabel => 'सेभ गर्नुहोस्'; @override - String get scanTextButtonLabel => 'पाठ स्क्यान गर्नुहोस्'; + String get scanTextButtonLabel => 'टेक्स्ट स्क्यान गर्नुहोस्'; @override String get scrimLabel => 'स्क्रिम'; @@ -29132,6 +30248,9 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'खोज्नुहोस्'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'सबै बटनहरू चयन गर्नुहोस्'; @@ -29156,6 +30275,9 @@ class MaterialLocalizationNe extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'खाताहरू देखाउनुहोस्'; @@ -29245,7 +30367,7 @@ class MaterialLocalizationNl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Sluiten'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Uitgevouwen'; @override String get collapsedIconTapHint => 'Uitvouwen'; @@ -29305,22 +30427,22 @@ class MaterialLocalizationNl extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigatiemenu'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Samengevouwen'; @override String get expandedIconTapHint => 'Samenvouwen'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dubbeltik om uit te vouwen'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Uitvouwen voor meer informatie'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dubbeltik om samen te vouwen'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Samenvouwen'; @override String get firstPageTooltip => 'Eerste pagina'; @@ -29508,9 +30630,15 @@ class MaterialLocalizationNl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenties'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu van menubalk'; + @override + String get menuDismissLabel => 'Menu sluiten'; + @override String get modalBarrierDismissLabel => 'Sluiten'; @@ -29610,6 +30738,9 @@ class MaterialLocalizationNl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Zoeken'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Alles selecteren'; @@ -29634,6 +30765,9 @@ class MaterialLocalizationNl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Accounts tonen'; @@ -29723,7 +30857,7 @@ class MaterialLocalizationNo extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Lukk'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Vises'; @override String get collapsedIconTapHint => 'Vis'; @@ -29783,22 +30917,22 @@ class MaterialLocalizationNo extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigasjonsmeny'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Skjules'; @override String get expandedIconTapHint => 'Skjul'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'dobbelttrykk for å vise'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Vis for å se mer informasjon'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'dobbelttrykk for å skjule'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Skjul'; @override String get firstPageTooltip => 'Første side'; @@ -29986,9 +31120,15 @@ class MaterialLocalizationNo extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisenser'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Meny med menylinje'; + @override + String get menuDismissLabel => 'Lukk menyen'; + @override String get modalBarrierDismissLabel => 'Avvis'; @@ -30088,6 +31228,9 @@ class MaterialLocalizationNo extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Søk'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Velg alle'; @@ -30112,6 +31255,9 @@ class MaterialLocalizationNo extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Vis kontoer'; @@ -30201,7 +31347,7 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ବନ୍ଦ କରନ୍ତୁ'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ବିସ୍ତାର କରାଯାଇଛି'; @override String get collapsedIconTapHint => 'ପ୍ରସାରିତ କରନ୍ତୁ'; @@ -30261,22 +31407,22 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { String get drawerLabel => 'ନେଭିଗେସନ୍ ମେନୁ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ସଙ୍କୁଚିତ କରାଯାଇଛି'; @override String get expandedIconTapHint => 'ସଙ୍କୁଚିତ କରନ୍ତୁ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ବିସ୍ତାର କରିବା ପାଇଁ ଦୁଇଥର ଟାପ କରନ୍ତୁ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ଅଧିକ ବିବରଣୀ ପାଇଁ ବିସ୍ତାର କରନ୍ତୁ'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ସଙ୍କୁଚିତ କରିବା ପାଇଁ ଦୁଇଥର ଟାପ କରନ୍ତୁ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ସଙ୍କୁଚିତ କରନ୍ତୁ'; @override String get firstPageTooltip => 'ପ୍ରଥମ ପୃଷ୍ଠା'; @@ -30464,9 +31610,15 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ଲାଇସେନ୍ସଗୁଡ଼କ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'ମେନୁ ବାର ମେନୁ'; + @override + String get menuDismissLabel => 'ମେନୁ ଖାରଜ କରନ୍ତୁ'; + @override String get modalBarrierDismissLabel => 'ଖାରଜ କରନ୍ତୁ'; @@ -30552,7 +31704,7 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'ସେଭ କରନ୍ତୁ'; @override - String get scanTextButtonLabel => 'ପାଠ୍ୟ ସ୍କାନ୍ କରନ୍ତୁ'; + String get scanTextButtonLabel => 'ଟେକ୍ସଟ୍ ସ୍କାନ୍ କରନ୍ତୁ'; @override String get scrimLabel => 'ସ୍କ୍ରିମ'; @@ -30566,6 +31718,9 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ସନ୍ଧାନ କରନ୍ତୁ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ସବୁ ଚୟନ କରନ୍ତୁ'; @@ -30590,6 +31745,9 @@ class MaterialLocalizationOr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ଆକାଉଣ୍ଟ ଦେଖାନ୍ତୁ'; @@ -30679,7 +31837,7 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ਬੰਦ ਕਰੋ'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ਵਿਸਤਾਰ ਕੀਤਾ ਗਿਆ'; @override String get collapsedIconTapHint => 'ਵਿਸਤਾਰ ਕਰੋ'; @@ -30739,22 +31897,22 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { String get drawerLabel => 'ਨੈਵੀਗੇਸ਼ਨ ਮੀਨੂ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ਸਮੇਟਿਆ ਗਿਆ'; @override String get expandedIconTapHint => 'ਸਮੇਟੋ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'ਵਿਸਤਾਰ ਕਰਨ ਲਈ ਡਬਲ ਟੈਪ ਕਰੋ'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ਹੋਰ ਵੇਰਵਿਆਂ ਲਈ ਵਿਸਤਾਰ ਕਰੋ'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'ਸਮੇਟਣ ਲਈ ਡਬਲ ਟੈਪ ਕਰੋ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ਸਮੇਟੋ'; @override String get firstPageTooltip => 'ਪਹਿਲਾ ਪੰਨਾ'; @@ -30942,9 +32100,15 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ਲਾਇਸੰਸ'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'ਮੀਨੂ ਬਾਰ ਮੀਨੂ'; + @override + String get menuDismissLabel => 'ਮੀਨੂ ਖਾਰਜ ਕਰੋ'; + @override String get modalBarrierDismissLabel => 'ਖਾਰਜ ਕਰੋ'; @@ -31030,7 +32194,7 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { String get saveButtonLabel => 'ਰੱਖਿਅਤ ਕਰੋ'; @override - String get scanTextButtonLabel => 'ਟੈਕਸਟ ਸਕੈਨ ਕਰੋ'; + String get scanTextButtonLabel => 'ਲਿਖਤ ਨੂੰ ਸਕੈਨ ਕਰੋ'; @override String get scrimLabel => 'ਸਕ੍ਰਿਮ'; @@ -31044,6 +32208,9 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ਖੋਜੋ'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'ਸਭ ਚੁਣੋ'; @@ -31068,6 +32235,9 @@ class MaterialLocalizationPa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ਖਾਤੇ ਦਿਖਾਓ'; @@ -31157,7 +32327,7 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zamknij'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Rozwinięto'; @override String get collapsedIconTapHint => 'Rozwiń'; @@ -31217,22 +32387,22 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu nawigacyjne'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Zwinięto'; @override String get expandedIconTapHint => 'Zwiń'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'kliknij dwukrotnie, aby rozwinąć'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Rozwiń, aby wyświetlić więcej informacji'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'kliknij dwukrotnie, aby zwinąć'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Zwiń'; @override String get firstPageTooltip => 'Pierwsza strona'; @@ -31420,9 +32590,15 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencje'; + @override + String get lookUpButtonLabel => 'Sprawdź'; + @override String get menuBarMenuLabel => 'Pasek menu'; + @override + String get menuDismissLabel => 'Zamknij menu'; + @override String get modalBarrierDismissLabel => 'Zamknij'; @@ -31508,7 +32684,7 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Zapisz'; @override - String get scanTextButtonLabel => 'Zeskanuj tekst'; + String get scanTextButtonLabel => 'Skanuj tekst'; @override String get scrimLabel => 'Siatka'; @@ -31522,6 +32698,9 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Szukaj'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Zaznacz wszystko'; @@ -31546,6 +32725,9 @@ class MaterialLocalizationPl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Pokaż konta'; @@ -31898,9 +33080,15 @@ class MaterialLocalizationPs extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'جوازونه'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menu bar menu'; + @override + String get menuDismissLabel => 'Dismiss menu'; + @override String get modalBarrierDismissLabel => 'رد کړه'; @@ -32000,6 +33188,9 @@ class MaterialLocalizationPs extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'لټون'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'غوره کړئ'; @@ -32024,6 +33215,9 @@ class MaterialLocalizationPs extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'حسابونه ښکاره کړئ'; @@ -32113,7 +33307,7 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Fechar'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Aberto.'; @override String get collapsedIconTapHint => 'Abrir'; @@ -32164,7 +33358,7 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { String get deleteButtonTooltip => 'Excluir'; @override - String get dialModeButtonLabel => 'Alternar para o modo de seleção de discagem'; + String get dialModeButtonLabel => 'Mudar para o modo de seleção de discagem'; @override String get dialogLabel => 'Caixa de diálogo'; @@ -32173,22 +33367,22 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu de navegação'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Fechado.'; @override String get expandedIconTapHint => 'Recolher'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'Toque duas vezes para abrir'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Abra para mostrar mais detalhes'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'toque duas vezes para fechar'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Feche'; @override String get firstPageTooltip => 'Primeira página'; @@ -32200,7 +33394,7 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { String get inputDateModeButtonLabel => 'Mudar para modo de entrada'; @override - String get inputTimeModeButtonLabel => 'Alternar para o modo de entrada de texto'; + String get inputTimeModeButtonLabel => 'Mudar para o modo de entrada de texto'; @override String get invalidDateFormatLabel => 'Formato inválido.'; @@ -32376,9 +33570,15 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenças'; + @override + String get lookUpButtonLabel => 'Pesquisar'; + @override String get menuBarMenuLabel => 'Menu da barra de menus'; + @override + String get menuDismissLabel => 'Dispensar menu'; + @override String get modalBarrierDismissLabel => 'Dispensar'; @@ -32478,6 +33678,9 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Pesquisa'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Selecionar tudo'; @@ -32502,6 +33705,9 @@ class MaterialLocalizationPt extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Mostrar contas'; @@ -32563,6 +33769,30 @@ class MaterialLocalizationPtPt extends MaterialLocalizationPt { required super.twoDigitZeroPaddedFormat, }); + @override + String get lookUpButtonLabel => 'Procurar'; + + @override + String get menuDismissLabel => 'Ignorar menu'; + + @override + String get expansionTileExpandedHint => 'toque duas vezes para reduzir'; + + @override + String get expansionTileCollapsedHint => 'toque duas vezes para expandir'; + + @override + String get expansionTileExpandedTapHint => 'Reduzir'; + + @override + String get expansionTileCollapsedTapHint => 'Expandir para obter mais detalhes'; + + @override + String get expandedHint => 'Reduzido'; + + @override + String get collapsedHint => 'Expandido'; + @override String get bottomSheetLabel => 'Secção inferior'; @@ -32742,7 +33972,7 @@ class MaterialLocalizationRo extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Închideți'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Extins'; @override String get collapsedIconTapHint => 'Extindeți'; @@ -32802,22 +34032,22 @@ class MaterialLocalizationRo extends GlobalMaterialLocalizations { String get drawerLabel => 'Meniu de navigare'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Restrâns'; @override String get expandedIconTapHint => 'Restrângeți'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'atingeți de două ori pentru a extinde'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Extindeți pentru mai multe detalii'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'atingeți de două ori pentru a restrânge'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Restrângeți'; @override String get firstPageTooltip => 'Prima pagină'; @@ -33005,9 +34235,15 @@ class MaterialLocalizationRo extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licențe'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Bară de meniu'; + @override + String get menuDismissLabel => 'Respingeți meniul'; + @override String get modalBarrierDismissLabel => 'Închideți'; @@ -33107,6 +34343,9 @@ class MaterialLocalizationRo extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Căutați'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Selectați tot'; @@ -33131,6 +34370,9 @@ class MaterialLocalizationRo extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Nu există elemente selectate'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Afișați conturile'; @@ -33220,7 +34462,7 @@ class MaterialLocalizationRu extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Закрыть'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Развернуто'; @override String get collapsedIconTapHint => 'Развернуть'; @@ -33280,22 +34522,22 @@ class MaterialLocalizationRu extends GlobalMaterialLocalizations { String get drawerLabel => 'Меню навигации'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Свернуто'; @override String get expandedIconTapHint => 'Свернуть'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'нажмите дважды, чтобы развернуть'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Развернуть дополнительные сведения'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'нажмите дважды, чтобы свернуть'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Свернуть'; @override String get firstPageTooltip => 'Первая страница'; @@ -33483,9 +34725,15 @@ class MaterialLocalizationRu extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лицензии'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Строка меню'; + @override + String get menuDismissLabel => 'Закрыть меню'; + @override String get modalBarrierDismissLabel => 'Закрыть'; @@ -33585,6 +34833,9 @@ class MaterialLocalizationRu extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Поиск'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Выбрать все'; @@ -33609,6 +34860,9 @@ class MaterialLocalizationRu extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Строки не выбраны'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Показать аккаунты'; @@ -33698,7 +34952,7 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'වසන්න'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'දිග හරින ලදි'; @override String get collapsedIconTapHint => 'දිග හරින්න'; @@ -33758,22 +35012,22 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { String get drawerLabel => 'සංචාලන මෙනුව'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'හකුළන ලදි'; @override String get expandedIconTapHint => 'හකුළන්න'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'විහිදුවීමට දෙවරක් තට්ටු කරන්න'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'වැඩි විස්තර සඳහා පුළුල් කරන්න'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'හැකිළවීමට දෙවරක් තට්ටු කරන්න'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'හකුළන්න'; @override String get firstPageTooltip => 'පළමු පිටුව'; @@ -33961,9 +35215,15 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'බලපත්‍ර'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'මෙනු තීරු මෙනුව'; + @override + String get menuDismissLabel => 'මෙනුව අස් කරන්න'; + @override String get modalBarrierDismissLabel => 'ඉවත ලන්න'; @@ -34049,7 +35309,7 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { String get saveButtonLabel => 'සුරකින්න'; @override - String get scanTextButtonLabel => 'පෙළ පරිලෝකනය කරන්න'; + String get scanTextButtonLabel => 'පෙළ ස්කෑන් කරන්න'; @override String get scrimLabel => 'ස්ක්‍රිම්'; @@ -34063,6 +35323,9 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'සෙවීම'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'සියල්ල තෝරන්න'; @@ -34087,6 +35350,9 @@ class MaterialLocalizationSi extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ගිණුම් පෙන්වන්න'; @@ -34176,7 +35442,7 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zavrieť'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Rozbalené'; @override String get collapsedIconTapHint => 'Rozbaliť'; @@ -34236,22 +35502,22 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigačná ponuka'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Zbalené'; @override String get expandedIconTapHint => 'Zbaliť'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'rozbalíte dvojitým klepnutím'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Rozbaliť a zobraziť ďalšie podrobnosti'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'zbalíte dvojitým klepnutím'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Zbaliť'; @override String get firstPageTooltip => 'Prvá strana'; @@ -34439,9 +35705,15 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencie'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Ponuka panela s ponukami'; + @override + String get menuDismissLabel => 'Zavrieť ponuku'; + @override String get modalBarrierDismissLabel => 'Odmietnuť'; @@ -34527,7 +35799,7 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Uložiť'; @override - String get scanTextButtonLabel => 'Naskenujte text'; + String get scanTextButtonLabel => 'Naskenovať text'; @override String get scrimLabel => 'Scrim'; @@ -34541,6 +35813,9 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Hľadať'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Vybrať všetko'; @@ -34565,6 +35840,9 @@ class MaterialLocalizationSk extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Zobraziť účty'; @@ -34654,7 +35932,7 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Zapiranje'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Razširjeno'; @override String get collapsedIconTapHint => 'Razširiti'; @@ -34714,22 +35992,22 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { String get drawerLabel => 'Meni za krmarjenje'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Strnjeno'; @override String get expandedIconTapHint => 'Strniti'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'za razširitev se dvakrat dotaknite'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Razširitev za več podrobnosti'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'za strnitev se dvakrat dotaknite'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Strni'; @override String get firstPageTooltip => 'Prva stran'; @@ -34917,9 +36195,15 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licence'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Meni menijske vrstice'; + @override + String get menuDismissLabel => 'Opusti meni'; + @override String get modalBarrierDismissLabel => 'Opusti'; @@ -35005,7 +36289,7 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Shrani'; @override - String get scanTextButtonLabel => 'Skeniraj besedilo'; + String get scanTextButtonLabel => 'Optično preberite besedilo'; @override String get scrimLabel => 'Scrim'; @@ -35019,6 +36303,9 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Iskanje'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Izberi vse'; @@ -35043,6 +36330,9 @@ class MaterialLocalizationSl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Prikaz računov'; @@ -35132,7 +36422,7 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Mbyll'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'U zgjerua'; @override String get collapsedIconTapHint => 'Zgjero'; @@ -35192,22 +36482,22 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { String get drawerLabel => 'Menyja e navigimit'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'U palos'; @override String get expandedIconTapHint => 'Palos'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'trokit dy herë për ta zgjeruar'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Zgjero për më shumë detaje'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'trokit dy herë për ta palosur'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Palos'; @override String get firstPageTooltip => 'Faqja e parë'; @@ -35395,9 +36685,15 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licencat'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menyja e shiritit të menysë'; + @override + String get menuDismissLabel => 'Hiqe menynë'; + @override String get modalBarrierDismissLabel => 'Hiq'; @@ -35483,7 +36779,7 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Ruaj'; @override - String get scanTextButtonLabel => 'Skanoni tekstin'; + String get scanTextButtonLabel => 'Skano tekstin'; @override String get scrimLabel => 'Kanavacë'; @@ -35497,6 +36793,9 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Kërko'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Zgjidh të gjitha'; @@ -35521,6 +36820,9 @@ class MaterialLocalizationSq extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Shfaq llogaritë'; @@ -35610,7 +36912,7 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Затворите'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Проширено је'; @override String get collapsedIconTapHint => 'Прошири'; @@ -35670,22 +36972,22 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { String get drawerLabel => 'Мени за навигацију'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Скупљено је'; @override String get expandedIconTapHint => 'Скупи'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'двапут додирните да бисте проширили'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Проширите за још детаља'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'двапут додирните да бисте скупили'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Скупите'; @override String get firstPageTooltip => 'Прва страница'; @@ -35873,9 +37175,15 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Лиценце'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Мени трака менија'; + @override + String get menuDismissLabel => 'Одбаците мени'; + @override String get modalBarrierDismissLabel => 'Одбаци'; @@ -35961,7 +37269,7 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Сачувај'; @override - String get scanTextButtonLabel => 'Скенирајте текст'; + String get scanTextButtonLabel => 'Скенирај текст'; @override String get scrimLabel => 'Скрим'; @@ -35975,6 +37283,9 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Претражите'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Изабери све'; @@ -35999,6 +37310,9 @@ class MaterialLocalizationSr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Прикажи налоге'; @@ -36106,6 +37420,9 @@ class MaterialLocalizationSrLatn extends MaterialLocalizationSr { @override String get closeButtonTooltip => 'Zatvorite'; + @override + String get collapsedHint => 'Prošireno je'; + @override String get collapsedIconTapHint => 'Proširi'; @@ -36160,9 +37477,24 @@ class MaterialLocalizationSrLatn extends MaterialLocalizationSr { @override String get drawerLabel => 'Meni za navigaciju'; + @override + String get expandedHint => 'Skupljeno je'; + @override String get expandedIconTapHint => 'Skupi'; + @override + String get expansionTileCollapsedHint => 'dvaput dodirnite da biste proširili'; + + @override + String get expansionTileCollapsedTapHint => 'Proširite za još detalja'; + + @override + String get expansionTileExpandedHint => 'dvaput dodirnite da biste skupili'; + + @override + String get expansionTileExpandedTapHint => 'Skupite'; + @override String get firstPageTooltip => 'Prva stranica'; @@ -36217,6 +37549,9 @@ class MaterialLocalizationSrLatn extends MaterialLocalizationSr { @override String get menuBarMenuLabel => 'Meni traka menija'; + @override + String get menuDismissLabel => 'Odbacite meni'; + @override String get modalBarrierDismissLabel => 'Odbaci'; @@ -36292,6 +37627,9 @@ class MaterialLocalizationSrLatn extends MaterialLocalizationSr { @override String get saveButtonLabel => 'Sačuvaj'; + @override + String get scanTextButtonLabel => 'Skeniraj tekst'; + @override String get scrimLabel => 'Skrim'; @@ -36402,7 +37740,7 @@ class MaterialLocalizationSv extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Stäng'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Utökades'; @override String get collapsedIconTapHint => 'Utöka'; @@ -36462,22 +37800,22 @@ class MaterialLocalizationSv extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigeringsmeny'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Komprimerades'; @override String get expandedIconTapHint => 'Dölj'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'tryck snabbt två gånger för att utöka'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Utöka för mer information'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'tryck snabbt två gånger för att komprimera'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Komprimera'; @override String get firstPageTooltip => 'Första sidan'; @@ -36665,9 +38003,15 @@ class MaterialLocalizationSv extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Licenser'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menyrad'; + @override + String get menuDismissLabel => 'Stäng menyn'; + @override String get modalBarrierDismissLabel => 'Stäng'; @@ -36767,6 +38111,9 @@ class MaterialLocalizationSv extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Sök'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Markera allt'; @@ -36791,6 +38138,9 @@ class MaterialLocalizationSv extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Visa konton'; @@ -36880,7 +38230,7 @@ class MaterialLocalizationSw extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Funga'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Imepanuliwa'; @override String get collapsedIconTapHint => 'Panua'; @@ -36940,22 +38290,22 @@ class MaterialLocalizationSw extends GlobalMaterialLocalizations { String get drawerLabel => 'Menyu ya kusogeza'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Imekunjwa'; @override String get expandedIconTapHint => 'Kunja'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'gusa mara mbili ili upanue'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Panua ili upate maelezo zaidi'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'gusa mara mbili ili ukunje'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Kunja'; @override String get firstPageTooltip => 'Ukurasa wa kwanza'; @@ -37143,9 +38493,15 @@ class MaterialLocalizationSw extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Leseni'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menyu ya upau wa menyu'; + @override + String get menuDismissLabel => 'Ondoa menyu'; + @override String get modalBarrierDismissLabel => 'Ondoa'; @@ -37245,6 +38601,9 @@ class MaterialLocalizationSw extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Tafuta'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Chagua vyote'; @@ -37269,6 +38628,9 @@ class MaterialLocalizationSw extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'Hamna kilicho chaguliwa'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Onyesha akaunti'; @@ -37358,7 +38720,7 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'மூடுக'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'விரிவாக்கப்பட்டது'; @override String get collapsedIconTapHint => 'விரிக்கும்'; @@ -37418,22 +38780,22 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { String get drawerLabel => 'வழிசெலுத்தல் மெனு'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'சுருக்கப்பட்டது'; @override String get expandedIconTapHint => 'சுருக்கும்'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'விரிவாக்க இருமுறை தட்டுங்கள்'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'கூடுதல் விவரங்களுக்கு விரிவாக்கலாம்'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'சுருக்க இருமுறை தட்டவும்'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'சுருக்கும்'; @override String get firstPageTooltip => 'முதல் பக்கத்திற்குச் செல்லும்'; @@ -37621,9 +38983,15 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'உரிமங்கள்'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'மெனு பட்டியின் மெனு'; + @override + String get menuDismissLabel => 'மெனுவை மூடும்'; + @override String get modalBarrierDismissLabel => 'நிராகரிக்கும்'; @@ -37709,7 +39077,7 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { String get saveButtonLabel => 'சேமி'; @override - String get scanTextButtonLabel => 'உரையை ஸ்கேன் செய்யவும்'; + String get scanTextButtonLabel => 'வார்த்தைகளை ஸ்கேன் செய்'; @override String get scrimLabel => 'ஸ்க்ரிம்'; @@ -37723,6 +39091,9 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'தேடல்'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'அனைத்தையும் தேர்ந்தெடு'; @@ -37747,6 +39118,9 @@ class MaterialLocalizationTa extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => 'எந்த வரிசையும் தேர்ந்தெடுக்கவில்லை'; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'கணக்குகளைக் காட்டும்'; @@ -37836,7 +39210,7 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'మూసివేయి'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'విస్తరించబడింది'; @override String get collapsedIconTapHint => 'విస్తరించు'; @@ -37896,22 +39270,22 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { String get drawerLabel => 'నావిగేషన్ మెనూ'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'కుదించబడింది'; @override String get expandedIconTapHint => 'కుదించు'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'విస్తరించడానికి డబుల్ ట్యాప్ చేయండి'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'మరిన్ని వివరాల కోసం విస్తరించండి'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'కుదించడానికి డబుల్ ట్యాప్ చేయండి'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'కుదించండి'; @override String get firstPageTooltip => 'మొదటి పేజీ'; @@ -38099,9 +39473,15 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'లైసెన్స్‌లు'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'మెనూ బార్ మెనూ'; + @override + String get menuDismissLabel => 'మెనూను తీసివేయండి'; + @override String get modalBarrierDismissLabel => 'విస్మరించు'; @@ -38187,7 +39567,7 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { String get saveButtonLabel => 'సేవ్ చేయండి'; @override - String get scanTextButtonLabel => 'వచనాన్ని స్కాన్ చేయండి'; + String get scanTextButtonLabel => 'టెక్స్ట్‌ను స్కాన్ చేయండి'; @override String get scrimLabel => 'స్క్రిమ్'; @@ -38201,6 +39581,9 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'వెతకండి'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'అన్నింటినీ ఎంచుకోండి'; @@ -38225,6 +39608,9 @@ class MaterialLocalizationTe extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'ఖాతాలను చూపు'; @@ -38314,7 +39700,7 @@ class MaterialLocalizationTh extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'ปิด'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'ขยาย'; @override String get collapsedIconTapHint => 'ขยาย'; @@ -38374,22 +39760,22 @@ class MaterialLocalizationTh extends GlobalMaterialLocalizations { String get drawerLabel => 'เมนูการนำทาง'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'ยุบ'; @override String get expandedIconTapHint => 'ยุบ'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'แตะสองครั้งเพื่อขยาย'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'ขยายเพื่อดูรายละเอียดเพิ่มเติม'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'แตะสองครั้งเพื่อยุบ'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'ยุบ'; @override String get firstPageTooltip => 'หน้าแรก'; @@ -38577,9 +39963,15 @@ class MaterialLocalizationTh extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'ใบอนุญาต'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'เมนูในแถบเมนู'; + @override + String get menuDismissLabel => 'ปิดเมนู'; + @override String get modalBarrierDismissLabel => 'ปิด'; @@ -38679,6 +40071,9 @@ class MaterialLocalizationTh extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'ค้นหา'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'เลือกทั้งหมด'; @@ -38703,6 +40098,9 @@ class MaterialLocalizationTh extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'แสดงบัญชี'; @@ -38792,7 +40190,7 @@ class MaterialLocalizationTl extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Isara'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Naka-expand'; @override String get collapsedIconTapHint => 'I-expand'; @@ -38852,22 +40250,22 @@ class MaterialLocalizationTl extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu ng navigation'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Naka-collapse'; @override String get expandedIconTapHint => 'I-collapse'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'i-double tap para i-expand'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'I-expand para sa higit pang detalye'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'i-double tap para i-collapse'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'I-collapse'; @override String get firstPageTooltip => 'Unang page'; @@ -39055,9 +40453,15 @@ class MaterialLocalizationTl extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Mga Lisensya'; + @override + String get lookUpButtonLabel => 'Tumingin sa Itaas'; + @override String get menuBarMenuLabel => 'Menu sa menu bar'; + @override + String get menuDismissLabel => 'I-dismiss ang menu'; + @override String get modalBarrierDismissLabel => 'I-dismiss'; @@ -39157,6 +40561,9 @@ class MaterialLocalizationTl extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Maghanap'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Piliin lahat'; @@ -39181,6 +40588,9 @@ class MaterialLocalizationTl extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Ipakita ang mga account'; @@ -39270,7 +40680,7 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Kapat'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Genişletildi'; @override String get collapsedIconTapHint => 'Genişlet'; @@ -39330,22 +40740,22 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { String get drawerLabel => 'Gezinme menüsü'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Daraltıldı'; @override String get expandedIconTapHint => 'Daralt'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'genişletmek için iki kez dokunun'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Daha fazla ayrıntı için genişletin'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'daraltmak için iki kez dokunun'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Daralt'; @override String get firstPageTooltip => 'İlk sayfa'; @@ -39533,9 +40943,15 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Lisanslar'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Menü çubuğu menüsü'; + @override + String get menuDismissLabel => 'Menüyü kapat'; + @override String get modalBarrierDismissLabel => 'Kapat'; @@ -39621,7 +41037,7 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Kaydet'; @override - String get scanTextButtonLabel => 'Metni tara'; + String get scanTextButtonLabel => 'Metin tara'; @override String get scrimLabel => 'opaklık katmanı'; @@ -39635,6 +41051,9 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Ara'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Tümünü seç'; @@ -39659,6 +41078,9 @@ class MaterialLocalizationTr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Hesapları göster'; @@ -39748,7 +41170,7 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Закрити'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Розгорнуто'; @override String get collapsedIconTapHint => 'Розгорнути'; @@ -39808,22 +41230,22 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { String get drawerLabel => 'Меню навігації'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Згорнуто'; @override String get expandedIconTapHint => 'Згорнути'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'двічі торкніться, щоб розгорнути'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Розгорнути й дізнатися більше'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'двічі торкніться, щоб згорнути'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Згорнути'; @override String get firstPageTooltip => 'Перша сторінка'; @@ -40011,9 +41433,15 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Ліцензії'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Панель меню'; + @override + String get menuDismissLabel => 'Закрити меню'; + @override String get modalBarrierDismissLabel => 'Закрити'; @@ -40099,7 +41527,7 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { String get saveButtonLabel => 'Зберегти'; @override - String get scanTextButtonLabel => 'Сканувати текст'; + String get scanTextButtonLabel => 'Відсканувати текст'; @override String get scrimLabel => 'Маскувальний фон'; @@ -40113,6 +41541,9 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Пошук'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Вибрати всі'; @@ -40137,6 +41568,9 @@ class MaterialLocalizationUk extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Показати облікові записи'; @@ -40226,7 +41660,7 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'بند کریں'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'پھیلا ہوا'; @override String get collapsedIconTapHint => 'پھیلائیں'; @@ -40286,22 +41720,22 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { String get drawerLabel => 'نیویگیشن مینیو'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'سکڑا ہوا'; @override String get expandedIconTapHint => 'سکیڑیں'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'پھیلانے کے لیے دوبار تھپتھپائیں'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'مزید تفصیلات کے لیے پھیلائیں'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'سکیڑنے کے لیے دوبار تھپتھپائیں'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'سکیڑیں'; @override String get firstPageTooltip => 'پہلا صفحہ'; @@ -40489,9 +41923,15 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'لائسنسز'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'مینیو بار کا مینیو'; + @override + String get menuDismissLabel => 'مینو برخاست کریں'; + @override String get modalBarrierDismissLabel => 'برخاست کریں'; @@ -40577,7 +42017,7 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { String get saveButtonLabel => 'محفوظ کریں'; @override - String get scanTextButtonLabel => 'متن کو اسکین کریں'; + String get scanTextButtonLabel => 'ٹیکسٹ اسکین کریں'; @override String get scrimLabel => 'اسکریم'; @@ -40591,6 +42031,9 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'تلاش'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'سبھی کو منتخب کریں'; @@ -40615,6 +42058,9 @@ class MaterialLocalizationUr extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'اکاؤنٹس دکھائیں'; @@ -40704,7 +42150,7 @@ class MaterialLocalizationUz extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Yopish'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Yoyilgan'; @override String get collapsedIconTapHint => 'Yoyish'; @@ -40764,22 +42210,22 @@ class MaterialLocalizationUz extends GlobalMaterialLocalizations { String get drawerLabel => 'Navigatsiya menyusi'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Yigʻilgan'; @override String get expandedIconTapHint => 'Kichraytirish'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'yoyish uchun ikki marta bosing'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Batafsil koʻrish uchun yoying'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'yigʻish uchun ikki marta bosing'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Yigʻish'; @override String get firstPageTooltip => 'Birinchi sahifa'; @@ -40967,9 +42413,15 @@ class MaterialLocalizationUz extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Litsenziyalar'; + @override + String get lookUpButtonLabel => 'Tepaga qarang'; + @override String get menuBarMenuLabel => 'Menyu paneli'; + @override + String get menuDismissLabel => 'Menyuni yopish'; + @override String get modalBarrierDismissLabel => 'Yopish'; @@ -41069,6 +42521,9 @@ class MaterialLocalizationUz extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Qidirish'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Hammasi'; @@ -41093,6 +42548,9 @@ class MaterialLocalizationUz extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Hisoblarni koʻrsatish'; @@ -41182,7 +42640,7 @@ class MaterialLocalizationVi extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Đóng'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Đã mở rộng'; @override String get collapsedIconTapHint => 'Mở rộng'; @@ -41242,22 +42700,22 @@ class MaterialLocalizationVi extends GlobalMaterialLocalizations { String get drawerLabel => 'Menu di chuyển'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Đã thu gọn'; @override String get expandedIconTapHint => 'Thu gọn'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'nhấn đúp để mở rộng'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Mở rộng để xem thêm chi tiết'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'nhấn đúp để thu gọn'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Thu gọn'; @override String get firstPageTooltip => 'Trang đầu'; @@ -41445,9 +42903,15 @@ class MaterialLocalizationVi extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Giấy phép'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Trình đơn của thanh trình đơn'; + @override + String get menuDismissLabel => 'Đóng trình đơn'; + @override String get modalBarrierDismissLabel => 'Bỏ qua'; @@ -41547,6 +43011,9 @@ class MaterialLocalizationVi extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Tìm kiếm'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Chọn tất cả'; @@ -41571,6 +43038,9 @@ class MaterialLocalizationVi extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Hiển thị tài khoản'; @@ -41660,7 +43130,7 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { String get closeButtonTooltip => '关闭'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => '已展开'; @override String get collapsedIconTapHint => '展开'; @@ -41720,22 +43190,22 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { String get drawerLabel => '导航菜单'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => '已收起'; @override String get expandedIconTapHint => '收起'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => '点按两次即可展开'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => '展开查看更多详情'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => '点按两次即可收起'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => '收起'; @override String get firstPageTooltip => '第一页'; @@ -41923,9 +43393,15 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { @override String get licensesPageTitle => '许可'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => '菜单栏的菜单'; + @override + String get menuDismissLabel => '关闭菜单'; + @override String get modalBarrierDismissLabel => '关闭'; @@ -42011,7 +43487,7 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { String get saveButtonLabel => '保存'; @override - String get scanTextButtonLabel => '扫描文本'; + String get scanTextButtonLabel => '扫描文字'; @override String get scrimLabel => '纱罩'; @@ -42025,6 +43501,9 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { @override String get searchFieldLabel => '搜索'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => '全选'; @@ -42049,6 +43528,9 @@ class MaterialLocalizationZh extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => '显示帐号'; @@ -42147,6 +43629,9 @@ class MaterialLocalizationZhHant extends MaterialLocalizationZh { @override String get closeButtonTooltip => '關閉'; + @override + String get collapsedHint => '已展開'; + @override String get collapsedIconTapHint => '展開'; @@ -42198,9 +43683,24 @@ class MaterialLocalizationZhHant extends MaterialLocalizationZh { @override String get drawerLabel => '導覽選單'; + @override + String get expandedHint => '已收合'; + @override String get expandedIconTapHint => '收合'; + @override + String get expansionTileCollapsedHint => '㩒兩下就可以展開'; + + @override + String get expansionTileCollapsedTapHint => '展開就可以查看詳情'; + + @override + String get expansionTileExpandedHint => '㩒兩下就可以收合'; + + @override + String get expansionTileExpandedTapHint => '收合'; + @override String get firstPageTooltip => '第一頁'; @@ -42324,9 +43824,15 @@ class MaterialLocalizationZhHant extends MaterialLocalizationZh { @override String get licensesPageTitle => '授權'; + @override + String get lookUpButtonLabel => '查詢'; + @override String get menuBarMenuLabel => '選單列選單'; + @override + String get menuDismissLabel => '閂選單'; + @override String get modalBarrierDismissLabel => '拒絕'; @@ -42393,6 +43899,9 @@ class MaterialLocalizationZhHant extends MaterialLocalizationZh { @override String get saveButtonLabel => '儲存'; + @override + String get scanTextButtonLabel => '掃瞄文字'; + @override String get scrimLabel => 'Scrim'; @@ -42488,6 +43997,21 @@ class MaterialLocalizationZhHantTw extends MaterialLocalizationZhHant { required super.twoDigitZeroPaddedFormat, }); + @override + String get scanTextButtonLabel => '掃描文字'; + + @override + String get menuDismissLabel => '關閉選單'; + + @override + String get expansionTileExpandedHint => '輕觸兩下即可收合'; + + @override + String get expansionTileCollapsedHint => '輕觸兩下即可展開'; + + @override + String get expansionTileCollapsedTapHint => '展開更多詳細資料'; + @override String get scrimLabel => '紗罩'; @@ -42631,7 +44155,7 @@ class MaterialLocalizationZu extends GlobalMaterialLocalizations { String get closeButtonTooltip => 'Vala'; @override - String get collapsedHint => 'Expanded'; + String get collapsedHint => 'Kunwetshiwe'; @override String get collapsedIconTapHint => 'Nweba'; @@ -42691,22 +44215,22 @@ class MaterialLocalizationZu extends GlobalMaterialLocalizations { String get drawerLabel => 'Imenyu yokuzulazula'; @override - String get expandedHint => 'Collapsed'; + String get expandedHint => 'Kugoqiwe'; @override String get expandedIconTapHint => 'Goqa'; @override - String get expansionTileCollapsedHint => 'double tap to expand'; + String get expansionTileCollapsedHint => 'Thepha kabili ukuze unwebe'; @override - String get expansionTileCollapsedTapHint => 'Expand for more details'; + String get expansionTileCollapsedTapHint => 'Nweba ukuze uthole imininingwane eyengeziwe'; @override - String get expansionTileExpandedHint => "double tap to collapse'"; + String get expansionTileExpandedHint => 'thepha kabili ukuze ugoqe'; @override - String get expansionTileExpandedTapHint => 'Collapse'; + String get expansionTileExpandedTapHint => 'Goqa'; @override String get firstPageTooltip => 'Ikhasi lokuqala'; @@ -42894,9 +44418,15 @@ class MaterialLocalizationZu extends GlobalMaterialLocalizations { @override String get licensesPageTitle => 'Amalayisense'; + @override + String get lookUpButtonLabel => 'Look Up'; + @override String get menuBarMenuLabel => 'Imenyu yebha yemenyu'; + @override + String get menuDismissLabel => 'Chitha imenyu'; + @override String get modalBarrierDismissLabel => 'Cashisa'; @@ -42996,6 +44526,9 @@ class MaterialLocalizationZu extends GlobalMaterialLocalizations { @override String get searchFieldLabel => 'Sesha'; + @override + String get searchWebButtonLabel => 'Search Web'; + @override String get selectAllButtonLabel => 'Khetha konke'; @@ -43020,6 +44553,9 @@ class MaterialLocalizationZu extends GlobalMaterialLocalizations { @override String? get selectedRowCountTitleZero => null; + @override + String get shareButtonLabel => 'Share...'; + @override String get showAccountsLabel => 'Bonisa ama-akhawunti'; diff --git a/packages/flutter_localizations/lib/src/l10n/material_af.arb b/packages/flutter_localizations/lib/src/l10n/material_af.arb index c593ac8c8228e..654ccdf1eacba 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_af.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_af.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Gaan voort", "copyButtonLabel": "Kopieer", "cutButtonLabel": "Knip", - "scanTextButtonLabel": "Skena umbhalo", + "scanTextButtonLabel": "Skandeer teks", "okButtonLabel": "OK", "pasteButtonLabel": "Plak", "selectAllButtonLabel": "Kies alles", @@ -135,10 +135,14 @@ "bottomSheetLabel": "Onderste blad", "scrimOnTapHint": "Maak $modalRouteContentName toe", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dubbeltik om in te vou", + "expansionTileCollapsedHint": "dubbeltik om uit te vou", + "expansionTileExpandedTapHint": "Vou in", + "expansionTileCollapsedTapHint": "Vou uit vir meer besonderhede", + "expandedHint": "Ingevou", + "collapsedHint": "Uitgevou", + "menuDismissLabel": "Maak kieslys toe", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_am.arb b/packages/flutter_localizations/lib/src/l10n/material_am.arb index 3538c87c0f2b5..20158502e0817 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_am.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_am.arb @@ -7,10 +7,10 @@ "deleteButtonTooltip": "ሰርዝ", "nextMonthTooltip": "ቀጣይ ወር", "previousMonthTooltip": "ቀዳሚ ወር", - "nextPageTooltip": "ቀጣይ ገጽ", - "firstPageTooltip": "የመጀመሪያው ገጽ", - "lastPageTooltip": "የመጨረሻው ገጽ", - "previousPageTooltip": "ቀዳሚ ገጽ", + "nextPageTooltip": "ቀጣይ ገፅ", + "firstPageTooltip": "የመጀመሪያው ገፅ", + "lastPageTooltip": "የመጨረሻው ገፅ", + "previousPageTooltip": "ቀዳሚ ገፅ", "showMenuTooltip": "ምናሌን አሳይ", "aboutListTileTitle": "ስለ $applicationName", "licensesPageTitle": "ፈቃዶች", @@ -25,7 +25,7 @@ "continueButtonLabel": "ቀጥል", "copyButtonLabel": "ቅዳ", "cutButtonLabel": "ቁረጥ", - "scanTextButtonLabel": "ጽሑፍ ይቃኙ", + "scanTextButtonLabel": "ጽሁፍን ቃኝ", "okButtonLabel": "እሺ", "pasteButtonLabel": "ለጥፍ", "selectAllButtonLabel": "ሁሉንም ምረጥ", @@ -135,10 +135,14 @@ "bottomSheetLabel": "የግርጌ ሉህ", "scrimOnTapHint": "$modalRouteContentNameን ዝጋ", "keyboardKeyShift": "ቀያይር", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ለመሰብሰብ ሁለቴ መታ ያድርጉ", + "expansionTileCollapsedHint": "ለመዘርጋት ድርብ ሁለቴ መታ ያድርጉ", + "expansionTileExpandedTapHint": "ሰብስብ", + "expansionTileCollapsedTapHint": "ለተጨማሪ ዝርዝሮች ይዘርጉ", + "expandedHint": "ተሰብስቧል", + "collapsedHint": "ተዘርግቷል", + "menuDismissLabel": "ምናሌን አሰናብት", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ar.arb b/packages/flutter_localizations/lib/src/l10n/material_ar.arb index 73f9d2b1181a6..18ee13a83e302 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ar.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ar.arb @@ -35,7 +35,7 @@ "continueButtonLabel": "المتابعة", "copyButtonLabel": "نسخ", "cutButtonLabel": "قص", - "scanTextButtonLabel": "مسح النص", + "scanTextButtonLabel": "مسح النص ضوئيًا", "okButtonLabel": "حسنًا", "pasteButtonLabel": "لصق", "selectAllButtonLabel": "اختيار الكل", @@ -146,10 +146,14 @@ "bottomSheetLabel": "بطاقة سفلية", "scrimOnTapHint": "إغلاق \"$modalRouteContentName\"", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "يُرجى النقر مرّتين للتصغير.", + "expansionTileCollapsedHint": "انقر مرّتين للتوسيع", + "expansionTileExpandedTapHint": "تصغير", + "expansionTileCollapsedTapHint": "وسِّع المربّع لعرض مزيد من التفاصيل.", + "expandedHint": "مصغَّر", + "collapsedHint": "موسَّع", + "menuDismissLabel": "إغلاق القائمة", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_as.arb b/packages/flutter_localizations/lib/src/l10n/material_as.arb index f358a744115e2..79fb39ea71c1f 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_as.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_as.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "অব্যাহত ৰাখক", "copyButtonLabel": "প্ৰতিলিপি কৰক", "cutButtonLabel": "কাট কৰক", - "scanTextButtonLabel": "স্কেন টেক্সট", + "scanTextButtonLabel": "পাঠ স্কেন কৰক", "okButtonLabel": "ঠিক আছে", "pasteButtonLabel": "পে'ষ্ট কৰক", "selectAllButtonLabel": "সকলো বাছনি কৰক", @@ -135,10 +135,14 @@ "bottomSheetLabel": "তলৰ শ্বীট", "scrimOnTapHint": "$modalRouteContentName বন্ধ কৰক", "keyboardKeyShift": "শ্বিফ্ট", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "সংকোচন কৰিবলৈ দুবাৰ টিপক", + "expansionTileCollapsedHint": "বিস্তাৰ কৰিবলৈ দুবাৰ টিপক", + "expansionTileExpandedTapHint": "সংকোচন কৰক", + "expansionTileCollapsedTapHint": "অধিক সবিশেষ জানিবলৈ বিস্তাৰ কৰক", + "expandedHint": "সংকোচন কৰা আছে", + "collapsedHint": "বিস্তাৰ কৰা আছে", + "menuDismissLabel": "অগ্ৰাহ্য কৰাৰ মেনু", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_az.arb b/packages/flutter_localizations/lib/src/l10n/material_az.arb index ea0736f368180..cc2a3321445bf 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_az.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_az.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "Aşağıdakı Vərəq", "scrimOnTapHint": "Bağlayın: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "yığcamlaşdırmaq üçün iki dəfə toxunun", + "expansionTileCollapsedHint": "genişləndirmək üçün iki dəfə toxunun", + "expansionTileExpandedTapHint": "Yığcamlaşdırın", + "expansionTileCollapsedTapHint": "Daha çox detallar üçün genişləndirin", + "expandedHint": "Yığcamlaşdırıldı", + "collapsedHint": "Genişləndirildi", + "menuDismissLabel": "Menyunu qapadın", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_be.arb b/packages/flutter_localizations/lib/src/l10n/material_be.arb index d58cb145704de..2c6dbca5b316c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_be.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_be.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Працягнуць", "copyButtonLabel": "Капіраваць", "cutButtonLabel": "Выразаць", - "scanTextButtonLabel": "Сканаваць тэкст", + "scanTextButtonLabel": "Сканіраваць тэкст", "okButtonLabel": "ОК", "pasteButtonLabel": "Уставіць", "selectAllButtonLabel": "Выбраць усе", @@ -141,10 +141,14 @@ "bottomSheetLabel": "Ніжні аркуш", "scrimOnTapHint": "Закрыць: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "двойчы націснуць, каб згарнуць", + "expansionTileCollapsedHint": "двойчы націснуць, каб разгарнуць", + "expansionTileExpandedTapHint": "Згарнуць", + "expansionTileCollapsedTapHint": "Разгарніце, каб даведацца больш", + "expandedHint": "Згорнута", + "collapsedHint": "Разгорнута", + "menuDismissLabel": "Закрыць меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_bg.arb b/packages/flutter_localizations/lib/src/l10n/material_bg.arb index fb6074c48d6de..ad124c3a09e24 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_bg.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_bg.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Напред", "copyButtonLabel": "Копиране", "cutButtonLabel": "Изрязване", - "scanTextButtonLabel": "Сканиране на текст", + "scanTextButtonLabel": "Сканирайте текст", "okButtonLabel": "OK", "pasteButtonLabel": "Поставяне", "selectAllButtonLabel": "Избиране на всички", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Долен лист", "scrimOnTapHint": "Затваряне на $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "докоснете два пъти за свиване", + "expansionTileCollapsedHint": "докоснете два пъти за разгъване", + "expansionTileExpandedTapHint": "Свиване", + "expansionTileCollapsedTapHint": "Разгъване за още подробности", + "expandedHint": "Свито", + "collapsedHint": "Разгънато", + "menuDismissLabel": "Отхвърляне на менюто", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_bn.arb b/packages/flutter_localizations/lib/src/l10n/material_bn.arb index 03587bc348245..77d039992b6a6 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_bn.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_bn.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "চালিয়ে যান", "copyButtonLabel": "কপি করুন", "cutButtonLabel": "কাট করুন", - "scanTextButtonLabel": "পাঠ্য স্ক্যান করুন", + "scanTextButtonLabel": "টেক্সট স্ক্যান করুন", "okButtonLabel": "ঠিক আছে", "pasteButtonLabel": "পেস্ট করুন", "selectAllButtonLabel": "সব বেছে নিন", @@ -135,10 +135,14 @@ "bottomSheetLabel": "স্ক্রিনের নিচে অ্যাটাচ করা শিট", "scrimOnTapHint": "$modalRouteContentName বন্ধ করুন", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "আড়াল করতে ডবল ট্যাপ করুন", + "expansionTileCollapsedHint": "বড় করে দেখতে ডবল ট্যাপ করুন", + "expansionTileExpandedTapHint": "আড়াল করুন", + "expansionTileCollapsedTapHint": "আরও বিবরণ পেতে বড় করে দেখুন", + "expandedHint": "আড়াল করা হয়েছে", + "collapsedHint": "বড় করা হয়েছে", + "menuDismissLabel": "বাতিল করার মেনু", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_bs.arb b/packages/flutter_localizations/lib/src/l10n/material_bs.arb index bbe68a1dafc4a..424b0e7f9079c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_bs.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_bs.arb @@ -139,10 +139,14 @@ "bottomSheetLabel": "Donja tabela", "scrimOnTapHint": "Zatvori: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "sužavanje dvostrukim dodirom", + "expansionTileCollapsedHint": "proširivanje dvostrukim dodirom", + "expansionTileExpandedTapHint": "Sužavanje", + "expansionTileCollapsedTapHint": "Proširivanje za više detalja", + "expandedHint": "Suženo", + "collapsedHint": "Prošireno", + "menuDismissLabel": "Odbacivanje menija", + "lookUpButtonLabel": "Pogled prema gore", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ca.arb b/packages/flutter_localizations/lib/src/l10n/material_ca.arb index aecdb082777ea..3f2b74212aad9 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ca.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ca.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Continua", "copyButtonLabel": "Copia", "cutButtonLabel": "Retalla", - "scanTextButtonLabel": "Escaneja el text", + "scanTextButtonLabel": "Escaneja text", "okButtonLabel": "D'ACORD", "pasteButtonLabel": "Enganxa", "selectAllButtonLabel": "Selecciona-ho tot", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Full inferior", "scrimOnTapHint": "Tanca $modalRouteContentName", "keyboardKeyShift": "Maj", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "fes doble toc per replegar", + "expansionTileCollapsedHint": "fes doble toc per desplegar", + "expansionTileExpandedTapHint": "Replega", + "expansionTileCollapsedTapHint": "Desplega per obtenir més informació", + "expandedHint": "S'ha replegat", + "collapsedHint": "S'ha desplegat", + "menuDismissLabel": "Ignora el menú", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_cs.arb b/packages/flutter_localizations/lib/src/l10n/material_cs.arb index 1e17c2e1c9a5b..dccaa7511212c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_cs.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_cs.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Pokračovat", "copyButtonLabel": "Kopírovat", "cutButtonLabel": "Vyjmout", - "scanTextButtonLabel": "Naskenujte text", + "scanTextButtonLabel": "Naskenovat text", "okButtonLabel": "OK", "pasteButtonLabel": "Vložit", "selectAllButtonLabel": "Vybrat vše", @@ -142,10 +142,14 @@ "bottomSheetLabel": "Spodní tabulka", "scrimOnTapHint": "Zavřít $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dvojitým klepnutím sbalíte", + "expansionTileCollapsedHint": "dvojitým klepnutím rozbalíte", + "expansionTileExpandedTapHint": "Sbalit", + "expansionTileCollapsedTapHint": "Rozbalte pro další podrobnosti", + "expandedHint": "Sbaleno", + "collapsedHint": "Rozbaleno", + "menuDismissLabel": "Zavřít nabídku", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_cy.arb b/packages/flutter_localizations/lib/src/l10n/material_cy.arb index 1cf88afa69ea4..61ce3f86520be 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_cy.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_cy.arb @@ -145,11 +145,15 @@ "keyboardKeySelect": "Select", "keyboardKeyShift": "Shift", "keyboardKeySpace": "Space", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded", - "scanTextButtonLabel": "Scan text" + "expansionTileExpandedHint": "tapiwch ddwywaith i grebachu", + "expansionTileCollapsedHint": "tapiwch ddwywaith i ehangu", + "expansionTileExpandedTapHint": "Crebachu", + "expansionTileCollapsedTapHint": "Ehangwch am ragor o fanylion", + "expandedHint": "Wedi'i grebachu", + "collapsedHint": "Wedi'i ehangu", + "scanTextButtonLabel": "Sganio testun", + "menuDismissLabel": "Diystyru'r ddewislen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_da.arb b/packages/flutter_localizations/lib/src/l10n/material_da.arb index 52c309eda3564..582686b1da882 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_da.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_da.arb @@ -1,6 +1,6 @@ { "scriptCategory": "English-like", - "timeOfDayFormat": "HH:mm", + "timeOfDayFormat": "HH.mm", "openAppDrawerTooltip": "Åbn navigationsmenuen", "backButtonTooltip": "Tilbage", "closeButtonTooltip": "Luk", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Felt i bunden", "scrimOnTapHint": "Luk $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "tryk to gange for at skjule", + "expansionTileCollapsedHint": "tryk to gange for at udvide", + "expansionTileExpandedTapHint": "Skjul", + "expansionTileCollapsedTapHint": "Udvid for at få flere oplysninger", + "expandedHint": "Skjult", + "collapsedHint": "Udvidet", + "menuDismissLabel": "Luk menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_de.arb b/packages/flutter_localizations/lib/src/l10n/material_de.arb index 9a5fbefb09190..7739ab93f08f0 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_de.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_de.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Ansicht am unteren Rand", "scrimOnTapHint": "$modalRouteContentName schließen", "keyboardKeyShift": "Umschalttaste", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "Zum Minimieren doppeltippen", + "expansionTileCollapsedHint": "Zum Maximieren doppeltippen", + "expansionTileExpandedTapHint": "Minimieren", + "expansionTileCollapsedTapHint": "Für weitere Details maximieren", + "expandedHint": "Minimiert", + "collapsedHint": "Maximiert", + "menuDismissLabel": "Menü schließen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_el.arb b/packages/flutter_localizations/lib/src/l10n/material_el.arb index e9f3408a997ce..48a6551b2ad8b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_el.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_el.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Φύλλο κάτω μέρους", "scrimOnTapHint": "Κλείσιμο $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "πατήστε δύο φορές για σύμπτυξη", + "expansionTileCollapsedHint": "πατήστε δύο φορές για ανάπτυξη", + "expansionTileExpandedTapHint": "Σύμπτυξη", + "expansionTileCollapsedTapHint": "Ανάπτυξη για περισσότερες λεπτομέρειες", + "expandedHint": "Συμπτύχθηκε", + "collapsedHint": "Αναπτύχθηκε", + "menuDismissLabel": "Παράβλεψη μενού", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_en.arb b/packages/flutter_localizations/lib/src/l10n/material_en.arb index dd9746dcd5956..bcce0e2b39801 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en.arb @@ -73,7 +73,7 @@ "scrimLabel": "Scrim", "@scrimLabel": { - "description": "The label for the scrim rendered underneath the content of a modal route." + "description": "The label for the scrim rendered underneath the content of a bottom sheet (used as the 'modalRouteContentName' of the 'scrimOnTapHint' message)." }, "bottomSheetLabel": "Bottom Sheet", @@ -83,7 +83,7 @@ "scrimOnTapHint": "Close $modalRouteContentName", "@scrimOnTapHint": { - "description": "The onTapHint for the scrim rendered underneath the content of a modal route which users can tap to dismiss the content", + "description": "The onTapHint for the scrim rendered underneath the content of a modal route (especially a bottom sheet) which users can tap to dismiss the content.", "parameters": "modalRouteContentName" }, @@ -197,6 +197,21 @@ "description": "The label for scan text buttons and menu items for starting the insertion of text via OCR." }, + "lookUpButtonLabel": "Look Up", + "@lookUpButtonLabel": { + "description": "The label for the Look Up button and menu items on iOS." + }, + + "searchWebButtonLabel": "Search Web", + "@searchWebButtonLabel": { + "description": "The label for the Search Web button and menu items on iOS." + }, + + "shareButtonLabel": "Share...", + "@shareButtonLabel": { + "description": "The label for the Share button and menu items on iOS." + }, + "okButtonLabel": "OK", "@okButtonLabel": { "description": "The label for OK buttons and menu items." @@ -242,6 +257,11 @@ "description": "Label read out by accessibility tools (TalkBack or VoiceOver) for a modal barrier to indicate that a tap dismisses the barrier. A modal barrier can for example be found behind a alert or popup to block user interaction with elements behind it." }, + "menuDismissLabel": "Dismiss menu", + "@menuDismissLabel": { + "description": "Label read out by accessibility tools (TalkBack or VoiceOver) for the area around a menu to indicate that a tap dismisses the menu." + }, + "dateSeparator": "/", "@dateSeparator": { "description": "The character string used to separate the parts of a compact date format. For example 'mm/dd/yyyy' has a separator of '/'." diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_AU.arb b/packages/flutter_localizations/lib/src/l10n/material_en_AU.arb index b2cc211b94cc5..e9891cde9b804 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_AU.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_AU.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_GB.arb b/packages/flutter_localizations/lib/src/l10n/material_en_GB.arb index 4ff18cdd7898b..22a307061e77c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_GB.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_GB.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_IE.arb b/packages/flutter_localizations/lib/src/l10n/material_en_IE.arb index 4ff18cdd7898b..22a307061e77c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_IE.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_IE.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_IN.arb b/packages/flutter_localizations/lib/src/l10n/material_en_IN.arb index b2cc211b94cc5..e9891cde9b804 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_IN.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_IN.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_NZ.arb b/packages/flutter_localizations/lib/src/l10n/material_en_NZ.arb index a4962bf652008..2fc0ba78f24f1 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_NZ.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_NZ.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_SG.arb b/packages/flutter_localizations/lib/src/l10n/material_en_SG.arb index b2cc211b94cc5..e9891cde9b804 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_SG.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_SG.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb b/packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb index 4ff18cdd7898b..22a307061e77c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Scan text", + "lookUpButtonLabel": "Look up", + "menuDismissLabel": "Dismiss menu", + "expansionTileExpandedHint": "double-tap to collapse", + "expansionTileCollapsedHint": "double-tap to expand", + "expansionTileExpandedTapHint": "Collapse", + "expansionTileCollapsedTapHint": "Expand for more details", + "expandedHint": "Collapsed", + "collapsedHint": "Expanded", "scrimLabel": "Scrim", "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Close $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es.arb b/packages/flutter_localizations/lib/src/l10n/material_es.arb index 59eed0c4324de..044c124f1867d 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", "keyboardKeyShift": "Mayús", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "toca dos veces para contraer", + "expansionTileCollapsedHint": "toca dos veces para desplegar", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Desplegar para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Desplegado", + "menuDismissLabel": "Cerrar menú", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_419.arb b/packages/flutter_localizations/lib/src/l10n/material_es_419.arb index f5b6bed2e5812..12d41aa41ffbb 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_419.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_419.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_AR.arb b/packages/flutter_localizations/lib/src/l10n/material_es_AR.arb index f5b6bed2e5812..12d41aa41ffbb 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_AR.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_AR.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_BO.arb b/packages/flutter_localizations/lib/src/l10n/material_es_BO.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_BO.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_BO.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_CL.arb b/packages/flutter_localizations/lib/src/l10n/material_es_CL.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_CL.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_CL.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_CO.arb b/packages/flutter_localizations/lib/src/l10n/material_es_CO.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_CO.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_CO.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_CR.arb b/packages/flutter_localizations/lib/src/l10n/material_es_CR.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_CR.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_CR.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_DO.arb b/packages/flutter_localizations/lib/src/l10n/material_es_DO.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_DO.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_DO.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_EC.arb b/packages/flutter_localizations/lib/src/l10n/material_es_EC.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_EC.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_EC.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_GT.arb b/packages/flutter_localizations/lib/src/l10n/material_es_GT.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_GT.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_GT.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_HN.arb b/packages/flutter_localizations/lib/src/l10n/material_es_HN.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_HN.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_HN.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_MX.arb b/packages/flutter_localizations/lib/src/l10n/material_es_MX.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_MX.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_MX.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_NI.arb b/packages/flutter_localizations/lib/src/l10n/material_es_NI.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_NI.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_NI.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_PA.arb b/packages/flutter_localizations/lib/src/l10n/material_es_PA.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_PA.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_PA.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_PE.arb b/packages/flutter_localizations/lib/src/l10n/material_es_PE.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_PE.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_PE.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_PR.arb b/packages/flutter_localizations/lib/src/l10n/material_es_PR.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_PR.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_PR.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_PY.arb b/packages/flutter_localizations/lib/src/l10n/material_es_PY.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_PY.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_PY.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_SV.arb b/packages/flutter_localizations/lib/src/l10n/material_es_SV.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_SV.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_SV.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_US.arb b/packages/flutter_localizations/lib/src/l10n/material_es_US.arb index 677ed109cf03a..47144eb6515d5 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_US.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_US.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_UY.arb b/packages/flutter_localizations/lib/src/l10n/material_es_UY.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_UY.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_UY.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_es_VE.arb b/packages/flutter_localizations/lib/src/l10n/material_es_VE.arb index 266a3b710b537..9e7f8d7fffe25 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_es_VE.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_es_VE.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Analizar texto", + "lookUpButtonLabel": "Mirar hacia arriba", + "menuDismissLabel": "Descartar menú", + "expansionTileExpandedHint": "presiona dos veces para contraer", + "expansionTileCollapsedHint": "presiona dos veces para expandir", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Expandir para ver más detalles", + "expandedHint": "Contraído", + "collapsedHint": "Expandido", "scrimLabel": "Lámina", "bottomSheetLabel": "Hoja inferior", "scrimOnTapHint": "Cerrar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_et.arb b/packages/flutter_localizations/lib/src/l10n/material_et.arb index 97fb40a2a7ede..6f207f1d4d3c4 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_et.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_et.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Jätka", "copyButtonLabel": "Kopeeri", "cutButtonLabel": "Lõika", - "scanTextButtonLabel": "Skanni teksti", + "scanTextButtonLabel": "Skanni tekst", "okButtonLabel": "OK", "pasteButtonLabel": "Kleebi", "selectAllButtonLabel": "Vali kõik", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Alumine leht", "scrimOnTapHint": "Sule $modalRouteContentName", "keyboardKeyShift": "Tõstuklahv", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "topeltpuudutage ahendamiseks", + "expansionTileCollapsedHint": "topeltpuudutage laiendamiseks", + "expansionTileExpandedTapHint": "Ahenda", + "expansionTileCollapsedTapHint": "Laiendage lisateabe nägemiseks", + "expandedHint": "Ahendatud", + "collapsedHint": "Laiendatud", + "menuDismissLabel": "Sulge menüü", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_eu.arb b/packages/flutter_localizations/lib/src/l10n/material_eu.arb index 8e299211b2f8c..741eacdd0ad4d 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_eu.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_eu.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "Behealdeko orria", "scrimOnTapHint": "Itxi $modalRouteContentName", "keyboardKeyShift": "Maius", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "tolesteko, sakatu birritan", + "expansionTileCollapsedHint": "zabaltzeko, sakatu birritan", + "expansionTileExpandedTapHint": "Tolestu", + "expansionTileCollapsedTapHint": "Zabaldu hau xehetasun gehiago lortzeko", + "expandedHint": "Tolestuta", + "collapsedHint": "Zabalduta", + "menuDismissLabel": "Baztertu menua", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_fa.arb b/packages/flutter_localizations/lib/src/l10n/material_fa.arb index 20c7fbcfd9937..35d66dd050c81 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_fa.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_fa.arb @@ -14,7 +14,7 @@ "lastPageTooltip": "صفحه آخر", "showMenuTooltip": "نمایش منو", "aboutListTileTitle": "درباره $applicationName", - "licensesPageTitle": "مجوزها", + "licensesPageTitle": "پروانه‌ها", "pageRowsInfoTitle": "$firstRow–$lastRow از $rowCount", "pageRowsInfoTitleApproximate": "$firstRow–$lastRow از حدود $rowCount", "rowsPerPageTitle": "ردیف در هر صفحه:", @@ -25,7 +25,7 @@ "continueButtonLabel": "ادامه", "copyButtonLabel": "کپی", "cutButtonLabel": "برش", - "scanTextButtonLabel": "اسکن متن", + "scanTextButtonLabel": "اسکن کردن نوشتار", "okButtonLabel": "تأیید", "pasteButtonLabel": "جای‌گذاری", "selectAllButtonLabel": "انتخاب همه", @@ -87,7 +87,7 @@ "keyboardKeyAlt": "دگرساز", "keyboardKeyAltGraph": "دگرساز راست", "keyboardKeyBackspace": "پس‌بَر", - "keyboardKeyCapsLock": "حالت حروف بزرگ", + "keyboardKeyCapsLock": "Caps Lock", "keyboardKeyChannelDown": "کانال پایین", "keyboardKeyChannelUp": "کانال بالا", "keyboardKeyControl": "مهار", @@ -136,10 +136,14 @@ "bottomSheetLabel": "برگ زیرین", "scrimOnTapHint": "بستن $modalRouteContentName", "keyboardKeyShift": "کلید تبدیل", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "برای جمع کردن، دوضربه بزنید", + "expansionTileCollapsedHint": "برای ازهم بازکردن، دوضربه بزنید", + "expansionTileExpandedTapHint": "جمع کردن", + "expansionTileCollapsedTapHint": "ازهم بازکردن برای جزئیات بیشتر", + "expandedHint": "جمع‌شده", + "collapsedHint": "ازهم بازشده", + "menuDismissLabel": "بستن منو", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_fi.arb b/packages/flutter_localizations/lib/src/l10n/material_fi.arb index 8db1a1af41adb..283690abdd086 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_fi.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_fi.arb @@ -17,7 +17,7 @@ "pageRowsInfoTitle": "$firstRow–$lastRow/$rowCount", "pageRowsInfoTitleApproximate": "$firstRow–$lastRow/~$rowCount", "rowsPerPageTitle": "Riviä/sivu:", - "tabLabel": "Välilehti $tabIndex/$tabCount", + "tabLabel": "Välilehti $tabIndex kautta $tabCount", "selectedRowCountTitleOne": "1 kohde valittu", "selectedRowCountTitleOther": "$selectedRowCount kohdetta valittu", "cancelButtonLabel": "Peru", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Alapaneeli", "scrimOnTapHint": "Sulje $modalRouteContentName", "keyboardKeyShift": "Vaihto", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "tiivistä kaksoisnapauttamalla", + "expansionTileCollapsedHint": "laajenna kaksoisnapauttamalla", + "expansionTileExpandedTapHint": "Tiivistä", + "expansionTileCollapsedTapHint": "Katso lisätietoja laajentamalla", + "expandedHint": "Tiivistetty", + "collapsedHint": "Laajennettu", + "menuDismissLabel": "Hylkää valikko", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_fil.arb b/packages/flutter_localizations/lib/src/l10n/material_fil.arb index fbf38a18b99a2..81500d0ee0cb3 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_fil.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_fil.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Bottom Sheet", "scrimOnTapHint": "Isara ang $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "i-double tap para i-collapse", + "expansionTileCollapsedHint": "i-double tap para i-expand", + "expansionTileExpandedTapHint": "I-collapse", + "expansionTileCollapsedTapHint": "I-expand para sa higit pang detalye", + "expandedHint": "Naka-collapse", + "collapsedHint": "Naka-expand", + "menuDismissLabel": "I-dismiss ang menu", + "lookUpButtonLabel": "Tumingin sa Itaas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_fr.arb b/packages/flutter_localizations/lib/src/l10n/material_fr.arb index accbb33f232e9..73ca135a2b17f 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_fr.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_fr.arb @@ -26,7 +26,7 @@ "continueButtonLabel": "Continuer", "copyButtonLabel": "Copier", "cutButtonLabel": "Couper", - "scanTextButtonLabel": "Numériser du texte", + "scanTextButtonLabel": "Scanner du texte", "okButtonLabel": "OK", "pasteButtonLabel": "Coller", "selectAllButtonLabel": "Tout sélectionner", @@ -137,10 +137,14 @@ "bottomSheetLabel": "Bottom sheet", "scrimOnTapHint": "Fermer $modalRouteContentName", "keyboardKeyShift": "Maj", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "appuyez deux fois pour réduire", + "expansionTileCollapsedHint": "appuyez deux fois pour développer", + "expansionTileExpandedTapHint": "Réduire", + "expansionTileCollapsedTapHint": "Développer pour en savoir plus", + "expandedHint": "Réduit", + "collapsedHint": "Développé", + "menuDismissLabel": "Fermer le menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_fr_CA.arb b/packages/flutter_localizations/lib/src/l10n/material_fr_CA.arb index 62fb9071bb3a5..0764fcca20a9a 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_fr_CA.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_fr_CA.arb @@ -1,4 +1,12 @@ { + "scanTextButtonLabel": "Balayer un texte", + "menuDismissLabel": "Ignorer le menu", + "expansionTileExpandedHint": "toucher deux fois pour réduire", + "expansionTileCollapsedHint": "toucher deux fois pour développer", + "expansionTileExpandedTapHint": "Réduire", + "expansionTileCollapsedTapHint": "Développer le panneau pour plus de détails", + "expandedHint": "Réduit", + "collapsedHint": "Développé", "scrimLabel": "Grille", "bottomSheetLabel": "Zone de contenu dans le bas de l'écran", "scrimOnTapHint": "Fermer $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_gl.arb b/packages/flutter_localizations/lib/src/l10n/material_gl.arb index 2557795963089..c4989e91aa616 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_gl.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_gl.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Panel inferior", "scrimOnTapHint": "Pechar $modalRouteContentName", "keyboardKeyShift": "Maiús", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "toca dúas veces para contraer", + "expansionTileCollapsedHint": "tocar dúas veces para despregar", + "expansionTileExpandedTapHint": "Contraer", + "expansionTileCollapsedTapHint": "Despregar para obter máis detalles", + "expandedHint": "Contraído", + "collapsedHint": "Despregado", + "menuDismissLabel": "Pechar menú", + "lookUpButtonLabel": "Mirar cara arriba", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_gsw.arb b/packages/flutter_localizations/lib/src/l10n/material_gsw.arb index 1d803e7b2ff91..6e459e9d0c27c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_gsw.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_gsw.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Ansicht am unteren Rand", "scrimOnTapHint": "$modalRouteContentName schließen", "keyboardKeyShift": "Umschalttaste", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "Zum Minimieren doppeltippen", + "expansionTileCollapsedHint": "Zum Maximieren doppeltippen", + "expansionTileExpandedTapHint": "Minimieren", + "expansionTileCollapsedTapHint": "Für weitere Details maximieren", + "expandedHint": "Minimiert", + "collapsedHint": "Maximiert", + "menuDismissLabel": "Menü schließen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_gu.arb b/packages/flutter_localizations/lib/src/l10n/material_gu.arb index 371690c084b97..ce1c833d9babb 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_gu.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_gu.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "ચાલુ રાખો", "copyButtonLabel": "કૉપિ કરો", "cutButtonLabel": "કાપો", - "scanTextButtonLabel": "ટેક્સ્ટ સ્કેન કરો", + "scanTextButtonLabel": "ટેક્સ્ટ સ્કૅન કરો", "okButtonLabel": "ઓકે", "pasteButtonLabel": "પેસ્ટ કરો", "selectAllButtonLabel": "બધા પસંદ કરો", @@ -135,10 +135,14 @@ "bottomSheetLabel": "બોટમ શીટ", "scrimOnTapHint": "$modalRouteContentNameને બંધ કરો", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "નાની કરવા માટે બે વાર ટૅપ કરો", + "expansionTileCollapsedHint": "મોટી કરવા માટે બે વાર ટૅપ કરો", + "expansionTileExpandedTapHint": "નાની કરો", + "expansionTileCollapsedTapHint": "વધુ વિગતો માટે મોટી કરો", + "expandedHint": "નાની કરી", + "collapsedHint": "મોટી કરી", + "menuDismissLabel": "મેનૂ છોડી દો", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_he.arb b/packages/flutter_localizations/lib/src/l10n/material_he.arb index c1f33f5bb61f2..7e5ed5c95455b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_he.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_he.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "המשך", "copyButtonLabel": "העתקה", "cutButtonLabel": "גזירה", - "scanTextButtonLabel": "סרוק טקסט", + "scanTextButtonLabel": "סריקת טקסט", "okButtonLabel": "אישור", "pasteButtonLabel": "הדבקה", "selectAllButtonLabel": "בחירת הכול", @@ -142,10 +142,14 @@ "bottomSheetLabel": "גיליון תחתון", "scrimOnTapHint": "סגירת $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "כדי לכווץ, יש להקיש הקשה כפולה", + "expansionTileCollapsedHint": "כדי להרחיב, יש להקיש הקשה כפולה", + "expansionTileExpandedTapHint": "כיווץ", + "expansionTileCollapsedTapHint": "ניתן להרחיב להצגת פרטים נוספים", + "expandedHint": "מכווץ", + "collapsedHint": "מורחב", + "menuDismissLabel": "סגירת התפריט", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_hi.arb b/packages/flutter_localizations/lib/src/l10n/material_hi.arb index 78fe9a1d30228..38ac3fd0bdb9b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_hi.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_hi.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "जारी रखें", "copyButtonLabel": "कॉपी करें", "cutButtonLabel": "काटें", - "scanTextButtonLabel": "पाठ स्कैन करें", + "scanTextButtonLabel": "टेक्स्ट स्कैन करें", "okButtonLabel": "ठीक है", "pasteButtonLabel": "चिपकाएं", "selectAllButtonLabel": "सभी को चुनें", @@ -136,10 +136,14 @@ "bottomSheetLabel": "बॉटम शीट", "scrimOnTapHint": "$modalRouteContentName को बंद करें", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "छोटा करने के लिए दो बार टैप करें", + "expansionTileCollapsedHint": "बड़ा करने के लिए दो बार टैप करें", + "expansionTileExpandedTapHint": "छोटा करें", + "expansionTileCollapsedTapHint": "ज़्यादा जानकारी के लिए बड़ा करें", + "expandedHint": "छोटा किया गया", + "collapsedHint": "बड़ा किया गया", + "menuDismissLabel": "मेन्यू खारिज करें", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_hr.arb b/packages/flutter_localizations/lib/src/l10n/material_hr.arb index 7535eb9e70ed8..59cf13db5ece5 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_hr.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_hr.arb @@ -28,7 +28,7 @@ "continueButtonLabel": "Nastavi", "copyButtonLabel": "Kopiraj", "cutButtonLabel": "Izreži", - "scanTextButtonLabel": "Skeniraj tekst", + "scanTextButtonLabel": "Skeniranje teksta", "okButtonLabel": "U REDU", "pasteButtonLabel": "Zalijepi", "selectAllButtonLabel": "Odaberi sve", @@ -139,10 +139,14 @@ "bottomSheetLabel": "Donja tablica", "scrimOnTapHint": "Zatvori $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dvaput dodirnite za sažimanje", + "expansionTileCollapsedHint": "dvaput dodirnite za proširivanje", + "expansionTileExpandedTapHint": "Sažmi", + "expansionTileCollapsedTapHint": "Proširite da biste saznali više", + "expandedHint": "Sažeto", + "collapsedHint": "Prošireno", + "menuDismissLabel": "Odbacivanje izbornika", + "lookUpButtonLabel": "Pogled prema gore", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_hu.arb b/packages/flutter_localizations/lib/src/l10n/material_hu.arb index 88c57ae8b3c57..df2ba6d9ccda8 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_hu.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_hu.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Alsó lap", "scrimOnTapHint": "$modalRouteContentName bezárása", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "duplán koppintva összecsukhatja", + "expansionTileCollapsedHint": "duplán koppintva kibonthatja", + "expansionTileExpandedTapHint": "Összecsukás", + "expansionTileCollapsedTapHint": "Bontsa ki a további részletek megtekintéséhez", + "expandedHint": "Összecsukva", + "collapsedHint": "Kibontva", + "menuDismissLabel": "Menü bezárása", + "lookUpButtonLabel": "Felfelé nézés", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_hy.arb b/packages/flutter_localizations/lib/src/l10n/material_hy.arb index 02d1ab87b93ad..8a005fda07db1 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_hy.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_hy.arb @@ -30,7 +30,7 @@ "continueButtonLabel": "Շարունակել", "copyButtonLabel": "Պատճենել", "cutButtonLabel": "Կտրել", - "scanTextButtonLabel": "Սկանավորեք տեքստը", + "scanTextButtonLabel": "Սկանավորել տեքստ", "okButtonLabel": "Եղավ", "pasteButtonLabel": "Տեղադրել", "selectAllButtonLabel": "Նշել բոլորը", @@ -141,10 +141,14 @@ "bottomSheetLabel": "Ներքևի էկրան", "scrimOnTapHint": "Փակել՝ $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "կրկնակի հպեք ծալելու համար", + "expansionTileCollapsedHint": "կրկնակի հպեք ծավալելու համար", + "expansionTileExpandedTapHint": "Ծալել", + "expansionTileCollapsedTapHint": "ծավալեք՝ մանրամասները տեսնելու համար", + "expandedHint": "Ծալված է", + "collapsedHint": "Ծավալված է", + "menuDismissLabel": "Փակել ընտրացանկը", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_id.arb b/packages/flutter_localizations/lib/src/l10n/material_id.arb index 131bface74bec..c43af93ec3991 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_id.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_id.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Sheet Bawah", "scrimOnTapHint": "Tutup $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ketuk dua kali untuk menciutkan", + "expansionTileCollapsedHint": "ketuk dua kali untuk meluaskan", + "expansionTileExpandedTapHint": "Ciutkan", + "expansionTileCollapsedTapHint": "Luaskan untuk mengetahui detail selengkapnya", + "expandedHint": "Diciutkan", + "collapsedHint": "Diluaskan", + "menuDismissLabel": "Tutup menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_is.arb b/packages/flutter_localizations/lib/src/l10n/material_is.arb index 30078d757f967..8dbc6518dd4b3 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_is.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_is.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Áfram", "copyButtonLabel": "Afrita", "cutButtonLabel": "Klippa", - "scanTextButtonLabel": "Skannaðu texta", + "scanTextButtonLabel": "Skanna texta", "okButtonLabel": "Í lagi", "pasteButtonLabel": "Líma", "selectAllButtonLabel": "Velja allt", @@ -135,10 +135,14 @@ "bottomSheetLabel": "Blað neðst", "scrimOnTapHint": "Loka $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ýttu tvisvar til að minnka", + "expansionTileCollapsedHint": "ýttu tvisvar til að stækka", + "expansionTileExpandedTapHint": "Minnka", + "expansionTileCollapsedTapHint": "Stækka til að sjá frekari upplýsingar", + "expandedHint": "Minnkað", + "collapsedHint": "Stækkað", + "menuDismissLabel": "Loka valmynd", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_it.arb b/packages/flutter_localizations/lib/src/l10n/material_it.arb index 1f35104a9d98d..f8094612375f7 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_it.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_it.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Continua", "copyButtonLabel": "Copia", "cutButtonLabel": "Taglia", - "scanTextButtonLabel": "Scansiona il testo", + "scanTextButtonLabel": "Scansiona testo", "okButtonLabel": "OK", "pasteButtonLabel": "Incolla", "selectAllButtonLabel": "Seleziona tutto", @@ -136,10 +136,14 @@ "bottomSheetLabel": "Riquadro inferiore", "scrimOnTapHint": "Chiudi $modalRouteContentName", "keyboardKeyShift": "Maiusc", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "tocca due volte per comprimere", + "expansionTileCollapsedHint": "Tocca due volte per espandere", + "expansionTileExpandedTapHint": "comprimere", + "expansionTileCollapsedTapHint": "espandere e visualizzare altri dettagli", + "expandedHint": "Compresso", + "collapsedHint": "Espanso", + "menuDismissLabel": "Ignora menu", + "lookUpButtonLabel": "Cerca", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ja.arb b/packages/flutter_localizations/lib/src/l10n/material_ja.arb index 58e3c810ba73b..02700410405be 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ja.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ja.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "ボトムシート", "scrimOnTapHint": "$modalRouteContentName を閉じる", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ダブルタップすると閉じます", + "expansionTileCollapsedHint": "開くにはダブルタップします", + "expansionTileExpandedTapHint": "閉じる", + "expansionTileCollapsedTapHint": "開いて詳細を表示", + "expandedHint": "閉じました", + "collapsedHint": "開きました", + "menuDismissLabel": "メニューを閉じる", + "lookUpButtonLabel": "調べる", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ka.arb b/packages/flutter_localizations/lib/src/l10n/material_ka.arb index b221c94792889..1fc8909e336c0 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ka.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ka.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "ქვედა ფურცელი", "scrimOnTapHint": "$modalRouteContentName-ის დახურვა", "keyboardKeyShift": "ცვლა", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ორმაგად შეეხეთ ჩასაკეცად", + "expansionTileCollapsedHint": "გასაფართოებლად ორჯერ შეეხეთ", + "expansionTileExpandedTapHint": "ჩაკეცვა", + "expansionTileCollapsedTapHint": "მეტი დეტალებისთვის გააფართოეთ", + "expandedHint": "ჩაკეცილია", + "collapsedHint": "გაფართოებულია", + "menuDismissLabel": "მენიუს უარყოფა", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_kk.arb b/packages/flutter_localizations/lib/src/l10n/material_kk.arb index a318f00a48881..efd070ee1b8d1 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_kk.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_kk.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Төменгі парақша", "scrimOnTapHint": "$modalRouteContentName жабу", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "жию үшін екі рет түртіңіз", + "expansionTileCollapsedHint": "жаю үшін екі рет түртіңіз", + "expansionTileExpandedTapHint": "Жию", + "expansionTileCollapsedTapHint": "Толық мәлімет алу үшін жайыңыз.", + "expandedHint": "Жиылды", + "collapsedHint": "Жайылды", + "menuDismissLabel": "Мәзірді жабу", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_km.arb b/packages/flutter_localizations/lib/src/l10n/material_km.arb index 9c2a624991e25..c14c56ae2c1cf 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_km.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_km.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "បន្ត", "copyButtonLabel": "ចម្លង", "cutButtonLabel": "កាត់", - "scanTextButtonLabel": "ស្កេនអត្ថបទ", + "scanTextButtonLabel": "ស្កេន​អក្សរ", "okButtonLabel": "យល់ព្រម", "pasteButtonLabel": "ដាក់​ចូល", "selectAllButtonLabel": "ជ្រើសរើស​ទាំងអស់", @@ -136,10 +136,14 @@ "bottomSheetLabel": "សន្លឹក​ខាងក្រោម", "scrimOnTapHint": "បិទ $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ចុចពីរដង ដើម្បីបង្រួម", + "expansionTileCollapsedHint": "ចុចពីរដង ដើម្បីពង្រីក", + "expansionTileExpandedTapHint": "បង្រួម", + "expansionTileCollapsedTapHint": "ពង្រីក​ដើម្បីទទួលបាន​ព័ត៌មានលម្អិត​បន្ថែម", + "expandedHint": "បាន​បង្រួម", + "collapsedHint": "បាន​ពង្រីក", + "menuDismissLabel": "ច្រានចោល​ម៉ឺនុយ", + "lookUpButtonLabel": "រកមើល", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_kn.arb b/packages/flutter_localizations/lib/src/l10n/material_kn.arb index 624941d984c0d..21cef3bf69d8f 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_kn.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_kn.arb @@ -68,7 +68,7 @@ "invalidDateFormatLabel": "\u0c85\u0cae\u0cbe\u0ca8\u0ccd\u0caf\u0cb5\u0cbe\u0ca6\u0020\u0cab\u0cbe\u0cb0\u0ccd\u0cae\u0ccd\u0caf\u0cbe\u0c9f\u0ccd\u002e", "invalidDateRangeLabel": "\u0c85\u0cae\u0cbe\u0ca8\u0ccd\u0caf\u0020\u0cb6\u0ccd\u0cb0\u0cc7\u0ca3\u0cbf\u002e", "dateOutOfRangeLabel": "\u0cb5\u0ccd\u0caf\u0cbe\u0caa\u0ccd\u0ca4\u0cbf\u0caf\u0020\u0cb9\u0cca\u0cb0\u0c97\u0cbf\u0ca6\u0cc6", - "saveButtonLabel": "\u0c89\u0cb3\u0cbf\u0cb8\u0cbf", + "saveButtonLabel": "\u0cb8\u0cc7\u0cb5\u0ccd\u0020\u0cae\u0cbe\u0ca1\u0cbf", "datePickerHelpText": "\u0ca6\u0cbf\u0ca8\u0cbe\u0c82\u0c95\u0cb5\u0ca8\u0ccd\u0ca8\u0cc1\u0020\u0c86\u0caf\u0ccd\u0c95\u0cc6\u0cae\u0cbe\u0ca1\u0cbf", "dateRangePickerHelpText": "\u0ca6\u0cbf\u0ca8\u0cbe\u0c82\u0c95\u0ca6\u0020\u0cb5\u0ccd\u0caf\u0cbe\u0caa\u0ccd\u0ca4\u0cbf\u0caf\u0ca8\u0ccd\u0ca8\u0cc1\u0020\u0c86\u0caf\u0ccd\u0c95\u0cc6\u0cae\u0cbe\u0ca1\u0cbf", "calendarModeButtonLabel": "\u0c95\u0ccd\u0caf\u0cbe\u0cb2\u0cc6\u0c82\u0ca1\u0cb0\u0ccd\u200c\u0c97\u0cc6\u0020\u0cac\u0ca6\u0cb2\u0cbf\u0cb8\u0cbf", @@ -135,10 +135,14 @@ "bottomSheetLabel": "\u0c95\u0cc6\u0cb3\u0cad\u0cbe\u0c97\u0ca6\u0020\u0cb6\u0cc0\u0c9f\u0ccd", "scrimOnTapHint": "\u0024\u006d\u006f\u0064\u0061\u006c\u0052\u006f\u0075\u0074\u0065\u0043\u006f\u006e\u0074\u0065\u006e\u0074\u004e\u0061\u006d\u0065\u0020\u0c85\u0ca8\u0ccd\u0ca8\u0cc1\u0020\u0cae\u0cc1\u0c9a\u0ccd\u0c9a\u0cbf\u0cb0\u0cbf", "keyboardKeyShift": "\u0053\u0068\u0069\u0066\u0074", - "expansionTileExpandedHint": "\u0064\u006f\u0075\u0062\u006c\u0065\u0020\u0074\u0061\u0070\u0020\u0074\u006f\u0020\u0063\u006f\u006c\u006c\u0061\u0070\u0073\u0065\u0027", - "expansionTileCollapsedHint": "\u0064\u006f\u0075\u0062\u006c\u0065\u0020\u0074\u0061\u0070\u0020\u0074\u006f\u0020\u0065\u0078\u0070\u0061\u006e\u0064", - "expansionTileExpandedTapHint": "\u0043\u006f\u006c\u006c\u0061\u0070\u0073\u0065", - "expansionTileCollapsedTapHint": "\u0045\u0078\u0070\u0061\u006e\u0064\u0020\u0066\u006f\u0072\u0020\u006d\u006f\u0072\u0065\u0020\u0064\u0065\u0074\u0061\u0069\u006c\u0073", - "expandedHint": "\u0043\u006f\u006c\u006c\u0061\u0070\u0073\u0065\u0064", - "collapsedHint": "\u0045\u0078\u0070\u0061\u006e\u0064\u0065\u0064" + "expansionTileExpandedHint": "\u0c95\u0cc1\u0c97\u0ccd\u0c97\u0cbf\u0cb8\u0cb2\u0cc1\u0020\u0ca1\u0cac\u0cb2\u0ccd\u0020\u0c9f\u0ccd\u0caf\u0cbe\u0caa\u0ccd\u0020\u0cae\u0cbe\u0ca1\u0cbf", + "expansionTileCollapsedHint": "\u0cb5\u0cbf\u0cb8\u0ccd\u0ca4\u0cb0\u0cbf\u0cb8\u0cb2\u0cc1\u0020\u0ca1\u0cac\u0cb2\u0ccd\u0020\u0c9f\u0ccd\u0caf\u0cbe\u0caa\u0ccd\u0020\u0cae\u0cbe\u0ca1\u0cbf", + "expansionTileExpandedTapHint": "\u0c95\u0cc1\u0c97\u0ccd\u0c97\u0cbf\u0cb8\u0cbf", + "expansionTileCollapsedTapHint": "\u0c87\u0ca8\u0ccd\u0ca8\u0cb7\u0ccd\u0c9f\u0cc1\u0020\u0cb5\u0cbf\u0cb5\u0cb0\u0c97\u0cb3\u0cbf\u0c97\u0cbe\u0c97\u0cbf\u0020\u0cb5\u0cbf\u0cb8\u0ccd\u0ca4\u0cb0\u0cbf\u0cb8\u0cbf", + "expandedHint": "\u0c95\u0cc1\u0c97\u0ccd\u0c97\u0cbf\u0cb8\u0cb2\u0cbe\u0c97\u0cbf\u0ca6\u0cc6", + "collapsedHint": "\u0cb5\u0cbf\u0cb8\u0ccd\u0ca4\u0cb0\u0cbf\u0cb8\u0cb2\u0cbe\u0c97\u0cbf\u0ca6\u0cc6", + "menuDismissLabel": "\u0cae\u0cc6\u0ca8\u0cc1\u0cb5\u0ca8\u0ccd\u0ca8\u0cc1\u0020\u0cb5\u0c9c\u0cbe\u0c97\u0cc6\u0cc2\u0cb3\u0cbf\u0cb8\u0cbf", + "lookUpButtonLabel": "\u004c\u006f\u006f\u006b\u0020\u0055\u0070", + "searchWebButtonLabel": "\u0053\u0065\u0061\u0072\u0063\u0068\u0020\u0057\u0065\u0062", + "shareButtonLabel": "\u0053\u0068\u0061\u0072\u0065\u002e\u002e\u002e" } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ko.arb b/packages/flutter_localizations/lib/src/l10n/material_ko.arb index 4b2eeee024c9a..fa2856b05255b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ko.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ko.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "계속", "copyButtonLabel": "복사", "cutButtonLabel": "잘라냄", - "scanTextButtonLabel": "스캔 텍스트", + "scanTextButtonLabel": "텍스트 스캔", "okButtonLabel": "확인", "pasteButtonLabel": "붙여넣기", "selectAllButtonLabel": "전체 선택", @@ -136,10 +136,14 @@ "bottomSheetLabel": "하단 시트", "scrimOnTapHint": "$modalRouteContentName 닫기", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "두 번 탭하여 접기", + "expansionTileCollapsedHint": "두 번 탭하여 펼치기", + "expansionTileExpandedTapHint": "접기", + "expansionTileCollapsedTapHint": "자세히 알아보려면 펼치기", + "expandedHint": "접힘", + "collapsedHint": "펼침", + "menuDismissLabel": "메뉴 닫기", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ky.arb b/packages/flutter_localizations/lib/src/l10n/material_ky.arb index 568e08e294238..d924c49546930 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ky.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ky.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "Ылдыйкы экран", "scrimOnTapHint": "$modalRouteContentName жабуу", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "жыйыштыруу үчүн эки жолу таптаңыз", + "expansionTileCollapsedHint": "жайып көрсөтүү үчүн эки жолу таптаңыз", + "expansionTileExpandedTapHint": "Жыйыштыруу", + "expansionTileCollapsedTapHint": "Толук маалымат алуу үчүн жайып көрүңүз", + "expandedHint": "Жыйыштырылды", + "collapsedHint": "Жайылып көрсөтүлдү", + "menuDismissLabel": "Менюну жабуу", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_lo.arb b/packages/flutter_localizations/lib/src/l10n/material_lo.arb index 2b02046d148c5..69aefa7a22f7c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_lo.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_lo.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "ຊີດລຸ່ມສຸດ", "scrimOnTapHint": "ປິດ $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ແຕະສອງເທື່ອເພື່ອຫຍໍ້ລົງ", + "expansionTileCollapsedHint": "ແຕະສອງເທື່ອເພື່ອຂະຫຍາຍ", + "expansionTileExpandedTapHint": "ຫຍໍ້ລົງ", + "expansionTileCollapsedTapHint": "ຂະຫຍາຍສຳລັບຂໍ້ມູນເພີ່ມເຕີມ", + "expandedHint": "ຫຍໍ້ລົງແລ້ວ", + "collapsedHint": "ຂະຫຍາຍແລ້ວ", + "menuDismissLabel": "ປິດເມນູ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_lt.arb b/packages/flutter_localizations/lib/src/l10n/material_lt.arb index 62d77cb56ac26..79138cabaf381 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_lt.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_lt.arb @@ -142,10 +142,14 @@ "bottomSheetLabel": "Apatinis lapas", "scrimOnTapHint": "Uždaryti „$modalRouteContentName“", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dukart palieskite, kad sutrauktumėte", + "expansionTileCollapsedHint": "dukart palieskite, kad išskleistumėte", + "expansionTileExpandedTapHint": "Sutraukti", + "expansionTileCollapsedTapHint": "Išskleiskite, jei reikia daugiau išsamios informacijos", + "expandedHint": "Sutraukta", + "collapsedHint": "Išskleista", + "menuDismissLabel": "Atsisakyti meniu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_lv.arb b/packages/flutter_localizations/lib/src/l10n/material_lv.arb index 3c84e58cea342..32c496d9a9564 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_lv.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_lv.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Ekrāna apakšdaļas lapa", "scrimOnTapHint": "Aizvērt $modalRouteContentName", "keyboardKeyShift": "Pārslēgšanas taustiņš", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dubultskāriens, lai sakļautu", + "expansionTileCollapsedHint": "dubultskāriens, lai izvērstu", + "expansionTileExpandedTapHint": "Sakļaut", + "expansionTileCollapsedTapHint": "Izvērst, lai iegūtu plašāku informāciju", + "expandedHint": "Sakļauts", + "collapsedHint": "Izvērsts", + "menuDismissLabel": "Nerādīt izvēlni", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_mk.arb b/packages/flutter_localizations/lib/src/l10n/material_mk.arb index 99586cbc339ed..6b43ceb0ac867 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_mk.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_mk.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Продолжи", "copyButtonLabel": "Копирај", "cutButtonLabel": "Исечи", - "scanTextButtonLabel": "Скенирајте текст", + "scanTextButtonLabel": "Скенирајте го текстот", "okButtonLabel": "Во ред", "pasteButtonLabel": "Залепи", "selectAllButtonLabel": "Избери ги сите", @@ -36,7 +36,7 @@ "timePickerMinuteModeAnnouncement": "Изберете минути", "modalBarrierDismissLabel": "Отфрли", "signedInLabel": "Најавени сте", - "hideAccountsLabel": "Сокриј сметки", + "hideAccountsLabel": "Скриј сметки", "showAccountsLabel": "Прикажи сметки", "drawerLabel": "Мени за навигација", "popupMenuLabel": "Скокачко мени", @@ -135,10 +135,14 @@ "bottomSheetLabel": "Долен лист", "scrimOnTapHint": "Затворете ја $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "допрете двапати за собирање", + "expansionTileCollapsedHint": "допри двапати за проширување", + "expansionTileExpandedTapHint": "Собери", + "expansionTileCollapsedTapHint": "Прошири за повеќе детали", + "expandedHint": "Собрано", + "collapsedHint": "Проширено", + "menuDismissLabel": "Отфрлете го менито", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ml.arb b/packages/flutter_localizations/lib/src/l10n/material_ml.arb index 28896359cc47d..2a6737a51dad7 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ml.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ml.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "തുടരുക", "copyButtonLabel": "പകർത്തുക", "cutButtonLabel": "മുറിക്കുക", - "scanTextButtonLabel": "ടെക്സ്റ്റ് സ്കാൻ ചെയ്യുക", + "scanTextButtonLabel": "ടെക്സ്റ്റ് സ്‌കാൻ ചെയ്യുക", "okButtonLabel": "ശരി", "pasteButtonLabel": "ഒട്ടിക്കുക", "selectAllButtonLabel": "എല്ലാം തിരഞ്ഞെടുക്കുക", @@ -135,10 +135,14 @@ "bottomSheetLabel": "ബോട്ടം ഷീറ്റ്", "scrimOnTapHint": "$modalRouteContentName അടയ്ക്കുക", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ചുരുക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യുക", + "expansionTileCollapsedHint": "വികസിപ്പിക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യുക", + "expansionTileExpandedTapHint": "ചുരുക്കുക", + "expansionTileCollapsedTapHint": "കൂടുതൽ വിശദാംശങ്ങൾക്ക് വികസിപ്പിക്കുക", + "expandedHint": "ചുരുക്കി", + "collapsedHint": "വികസിപ്പിച്ചു", + "menuDismissLabel": "മെനു ഡിസ്മിസ് ചെയ്യുക", + "lookUpButtonLabel": "മുകളിലേക്ക് നോക്കുക", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_mn.arb b/packages/flutter_localizations/lib/src/l10n/material_mn.arb index a960b7b141edb..20f54fd252f9b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_mn.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_mn.arb @@ -26,7 +26,7 @@ "continueButtonLabel": "Үргэлжлүүлэх", "copyButtonLabel": "Хуулах", "cutButtonLabel": "Таслах", - "scanTextButtonLabel": "Текст сканнердах", + "scanTextButtonLabel": "Текстийг скан хийх", "okButtonLabel": "OK", "pasteButtonLabel": "Буулгах", "selectAllButtonLabel": "Бүгдийг сонгох", @@ -137,10 +137,14 @@ "bottomSheetLabel": "Доод хүснэгт", "scrimOnTapHint": "$modalRouteContentName-г хаах", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "хураахын тулд хоёр товшино уу", + "expansionTileCollapsedHint": "дэлгэхийн тулд хоёр товшино уу", + "expansionTileExpandedTapHint": "Хураах", + "expansionTileCollapsedTapHint": "Илүү дэлгэрэнгүй авах бол дэлгэнэ үү", + "expandedHint": "Хураасан", + "collapsedHint": "Дэлгэсэн", + "menuDismissLabel": "Цэсийг хаах", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_mr.arb b/packages/flutter_localizations/lib/src/l10n/material_mr.arb index ccc1ab9019bd7..0889a9b10e709 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_mr.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_mr.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "तळाशी असलेली शीट", "scrimOnTapHint": "$modalRouteContentName बंद करा", "keyboardKeyShift": "शिफ्ट", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "कोलॅप्स करण्यासाठी दोनदा टॅप करा", + "expansionTileCollapsedHint": "विस्तार करण्‍यासाठी दोनदा टॅप करा", + "expansionTileExpandedTapHint": "कोलॅप्स करा", + "expansionTileCollapsedTapHint": "आणखी तपशिलांसाठी विस्तार करा", + "expandedHint": "कोलॅप्स केले", + "collapsedHint": "विस्तार केले", + "menuDismissLabel": "मेनू डिसमिस करा", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ms.arb b/packages/flutter_localizations/lib/src/l10n/material_ms.arb index 2efd94a47bbd9..352736c017803 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ms.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ms.arb @@ -26,7 +26,7 @@ "continueButtonLabel": "Teruskan", "copyButtonLabel": "Salin", "cutButtonLabel": "Potong", - "scanTextButtonLabel": "Pindai teks", + "scanTextButtonLabel": "Imbas teks", "okButtonLabel": "OK", "pasteButtonLabel": "Tampal", "selectAllButtonLabel": "Pilih semua", @@ -137,10 +137,14 @@ "bottomSheetLabel": "Helaian Bawah", "scrimOnTapHint": "Tutup $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ketik dua kali untuk kuncupkan", + "expansionTileCollapsedHint": "ketik dua kali untuk kembangkan", + "expansionTileExpandedTapHint": "Kuncupkan", + "expansionTileCollapsedTapHint": "Kembangkan untuk mendapatkan butiran lanjut", + "expandedHint": "Dikuncupkan", + "collapsedHint": "Dikembangkan", + "menuDismissLabel": "Ketepikan menu", + "lookUpButtonLabel": "Lihat ke Atas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_my.arb b/packages/flutter_localizations/lib/src/l10n/material_my.arb index 06cde1220d69b..bdb51efcb5ec2 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_my.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_my.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "ရှေ့ဆက်ရန်", "copyButtonLabel": "မိတ္တူကူးရန်", "cutButtonLabel": "ဖြတ်ယူရန်", - "scanTextButtonLabel": "စာသားကို စကင်ဖတ်ပါ။", + "scanTextButtonLabel": "စာသား စကင်ဖတ်ရန်", "okButtonLabel": "OK", "pasteButtonLabel": "ကူးထည့်ရန်", "selectAllButtonLabel": "အားလုံး ရွေးရန်", @@ -135,10 +135,14 @@ "bottomSheetLabel": "အောက်ခြေအပိုဆောင်း စာမျက်နှာ", "scrimOnTapHint": "$modalRouteContentName ပိတ်ရန်", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ခေါက်ရန် နှစ်ချက်တို့ပါ", + "expansionTileCollapsedHint": "ဖြန့်ရန် နှစ်ချက်တို့ပါ", + "expansionTileExpandedTapHint": "ခေါက်ရန်", + "expansionTileCollapsedTapHint": "အသေးစိတ်အတွက် ဖြန့်ရန်", + "expandedHint": "ခေါက်ထားသည်", + "collapsedHint": "ဖြန့်ထားသည်", + "menuDismissLabel": "မီနူးကိုပယ်ပါ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_nb.arb b/packages/flutter_localizations/lib/src/l10n/material_nb.arb index 0a4810524f235..51a9204053277 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_nb.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_nb.arb @@ -28,7 +28,7 @@ "continueButtonLabel": "Fortsett", "copyButtonLabel": "Kopiér", "cutButtonLabel": "Klipp ut", - "scanTextButtonLabel": "Scan tekst", + "scanTextButtonLabel": "Skann tekst", "okButtonLabel": "OK", "pasteButtonLabel": "Lim inn", "selectAllButtonLabel": "Velg alle", @@ -134,10 +134,14 @@ "bottomSheetLabel": "Felt nederst", "scrimOnTapHint": "Lukk $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dobbelttrykk for å skjule", + "expansionTileCollapsedHint": "dobbelttrykk for å vise", + "expansionTileExpandedTapHint": "Skjul", + "expansionTileCollapsedTapHint": "Vis for å se mer informasjon", + "expandedHint": "Skjules", + "collapsedHint": "Vises", + "menuDismissLabel": "Lukk menyen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ne.arb b/packages/flutter_localizations/lib/src/l10n/material_ne.arb index 7c53636103d85..682ae42d45097 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ne.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ne.arb @@ -7,7 +7,7 @@ "deleteButtonTooltip": "मेट्नुहोस्", "nextMonthTooltip": "अर्को महिना", "previousMonthTooltip": "अघिल्लो महिना", - "nextPageTooltip": "अर्को पृष्ठ", + "nextPageTooltip": "अर्को पेज", "previousPageTooltip": "अघिल्लो पृष्ठ", "firstPageTooltip": "प्रथम पेज", "lastPageTooltip": "अन्तिम पेज", @@ -25,7 +25,7 @@ "continueButtonLabel": "जारी राख्नुहोस्", "copyButtonLabel": "प्रतिलिपि गर्नुहोस्", "cutButtonLabel": "काट्नुहोस्", - "scanTextButtonLabel": "पाठ स्क्यान गर्नुहोस्", + "scanTextButtonLabel": "टेक्स्ट स्क्यान गर्नुहोस्", "okButtonLabel": "ठिक छ", "pasteButtonLabel": "टाँस्नुहोस्", "selectAllButtonLabel": "सबै बटनहरू चयन गर्नुहोस्", @@ -135,10 +135,14 @@ "bottomSheetLabel": "पुछारको पाना", "scrimOnTapHint": "$modalRouteContentName बन्द गर्नुहोस्", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "कोल्याप्स गर्न डबल ट्याप गर्नुहोस्", + "expansionTileCollapsedHint": "एक्स्पान्ड गर्न डबल ट्याप गर्नुहोस्", + "expansionTileExpandedTapHint": "कोल्याप्स गर्नुहोस्", + "expansionTileCollapsedTapHint": "थप विवरण हेर्न एक्स्पान्ड गर्नुहोस्", + "expandedHint": "कोल्याप्स गरियो", + "collapsedHint": "एक्स्पान्ड गरियो", + "menuDismissLabel": "मेनु खारेज गर्नुहोस्", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_nl.arb b/packages/flutter_localizations/lib/src/l10n/material_nl.arb index 16d0bceebf9e0..ed6fa8d09939b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_nl.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_nl.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Blad onderaan", "scrimOnTapHint": "$modalRouteContentName sluiten", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dubbeltik om samen te vouwen", + "expansionTileCollapsedHint": "dubbeltik om uit te vouwen", + "expansionTileExpandedTapHint": "Samenvouwen", + "expansionTileCollapsedTapHint": "Uitvouwen voor meer informatie", + "expandedHint": "Samengevouwen", + "collapsedHint": "Uitgevouwen", + "menuDismissLabel": "Menu sluiten", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_no.arb b/packages/flutter_localizations/lib/src/l10n/material_no.arb index a311a70b250c2..51a9204053277 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_no.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_no.arb @@ -134,10 +134,14 @@ "bottomSheetLabel": "Felt nederst", "scrimOnTapHint": "Lukk $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "dobbelttrykk for å skjule", + "expansionTileCollapsedHint": "dobbelttrykk for å vise", + "expansionTileExpandedTapHint": "Skjul", + "expansionTileCollapsedTapHint": "Vis for å se mer informasjon", + "expandedHint": "Skjules", + "collapsedHint": "Vises", + "menuDismissLabel": "Lukk menyen", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_or.arb b/packages/flutter_localizations/lib/src/l10n/material_or.arb index 299d57c067bcf..f912d335d0cad 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_or.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_or.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "ଜାରି ରଖନ୍ତୁ", "copyButtonLabel": "କପି କରନ୍ତୁ", "cutButtonLabel": "କଟ୍ କରନ୍ତୁ", - "scanTextButtonLabel": "ପାଠ୍ୟ ସ୍କାନ୍ କରନ୍ତୁ", + "scanTextButtonLabel": "ଟେକ୍ସଟ୍ ସ୍କାନ୍ କରନ୍ତୁ", "okButtonLabel": "ଠିକ୍ ଅଛି", "pasteButtonLabel": "ପେଷ୍ଟ କରନ୍ତୁ", "selectAllButtonLabel": "ସବୁ ଚୟନ କରନ୍ତୁ", @@ -135,10 +135,14 @@ "bottomSheetLabel": "ବଟମ ସିଟ", "scrimOnTapHint": "$modalRouteContentNameକୁ ବନ୍ଦ କରନ୍ତୁ", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ସଙ୍କୁଚିତ କରିବା ପାଇଁ ଦୁଇଥର ଟାପ କରନ୍ତୁ", + "expansionTileCollapsedHint": "ବିସ୍ତାର କରିବା ପାଇଁ ଦୁଇଥର ଟାପ କରନ୍ତୁ", + "expansionTileExpandedTapHint": "ସଙ୍କୁଚିତ କରନ୍ତୁ", + "expansionTileCollapsedTapHint": "ଅଧିକ ବିବରଣୀ ପାଇଁ ବିସ୍ତାର କରନ୍ତୁ", + "expandedHint": "ସଙ୍କୁଚିତ କରାଯାଇଛି", + "collapsedHint": "ବିସ୍ତାର କରାଯାଇଛି", + "menuDismissLabel": "ମେନୁ ଖାରଜ କରନ୍ତୁ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_pa.arb b/packages/flutter_localizations/lib/src/l10n/material_pa.arb index d48c4611a6da6..3fef6da95ffd8 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_pa.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_pa.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "ਜਾਰੀ ਰੱਖੋ", "copyButtonLabel": "ਕਾਪੀ ਕਰੋ", "cutButtonLabel": "ਕੱਟ ਕਰੋ", - "scanTextButtonLabel": "ਟੈਕਸਟ ਸਕੈਨ ਕਰੋ", + "scanTextButtonLabel": "ਲਿਖਤ ਨੂੰ ਸਕੈਨ ਕਰੋ", "okButtonLabel": "ਠੀਕ ਹੈ", "pasteButtonLabel": "ਪੇਸਟ ਕਰੋ", "selectAllButtonLabel": "ਸਭ ਚੁਣੋ", @@ -135,10 +135,14 @@ "bottomSheetLabel": "ਹੇਠਲੀ ਸ਼ੀਟ", "scrimOnTapHint": "$modalRouteContentName ਨੂੰ ਬੰਦ ਕਰੋ", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "ਸਮੇਟਣ ਲਈ ਡਬਲ ਟੈਪ ਕਰੋ", + "expansionTileCollapsedHint": "ਵਿਸਤਾਰ ਕਰਨ ਲਈ ਡਬਲ ਟੈਪ ਕਰੋ", + "expansionTileExpandedTapHint": "ਸਮੇਟੋ", + "expansionTileCollapsedTapHint": "ਹੋਰ ਵੇਰਵਿਆਂ ਲਈ ਵਿਸਤਾਰ ਕਰੋ", + "expandedHint": "ਸਮੇਟਿਆ ਗਿਆ", + "collapsedHint": "ਵਿਸਤਾਰ ਕੀਤਾ ਗਿਆ", + "menuDismissLabel": "ਮੀਨੂ ਖਾਰਜ ਕਰੋ", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_pl.arb b/packages/flutter_localizations/lib/src/l10n/material_pl.arb index 47695b170de7e..a7a4fe5d09997 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_pl.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_pl.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Dalej", "copyButtonLabel": "Kopiuj", "cutButtonLabel": "Wytnij", - "scanTextButtonLabel": "Zeskanuj tekst", + "scanTextButtonLabel": "Skanuj tekst", "okButtonLabel": "OK", "pasteButtonLabel": "Wklej", "selectAllButtonLabel": "Zaznacz wszystko", @@ -142,10 +142,14 @@ "bottomSheetLabel": "Plansza dolna", "scrimOnTapHint": "Zamknij: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "kliknij dwukrotnie, aby zwinąć", + "expansionTileCollapsedHint": "kliknij dwukrotnie, aby rozwinąć", + "expansionTileExpandedTapHint": "Zwiń", + "expansionTileCollapsedTapHint": "Rozwiń, aby wyświetlić więcej informacji", + "expandedHint": "Zwinięto", + "collapsedHint": "Rozwinięto", + "menuDismissLabel": "Zamknij menu", + "lookUpButtonLabel": "Sprawdź", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ps.arb b/packages/flutter_localizations/lib/src/l10n/material_ps.arb index 042b8055dbc7f..6660397de90fd 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ps.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ps.arb @@ -142,5 +142,9 @@ "expansionTileExpandedTapHint": "Collapse", "expansionTileCollapsedTapHint": "Expand for more details", "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "collapsedHint": "Expanded", + "menuDismissLabel": "Dismiss menu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_pt.arb b/packages/flutter_localizations/lib/src/l10n/material_pt.arb index c671af7dab373..daf092cf82109 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_pt.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_pt.arb @@ -81,8 +81,8 @@ "timePickerHourLabel": "Hora", "timePickerMinuteLabel": "Minuto", "invalidTimeLabel": "Insira um horário válido", - "dialModeButtonLabel": "Alternar para o modo de seleção de discagem", - "inputTimeModeButtonLabel": "Alternar para o modo de entrada de texto", + "dialModeButtonLabel": "Mudar para o modo de seleção de discagem", + "inputTimeModeButtonLabel": "Mudar para o modo de entrada de texto", "licensesPackageDetailTextZero": "No licenses", "licensesPackageDetailTextOne": "1 licença", "licensesPackageDetailTextOther": "$licenseCount licenças", @@ -138,10 +138,14 @@ "bottomSheetLabel": "Página inferior", "scrimOnTapHint": "Fechar $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "toque duas vezes para fechar", + "expansionTileCollapsedHint": "Toque duas vezes para abrir", + "expansionTileExpandedTapHint": "Feche", + "expansionTileCollapsedTapHint": "Abra para mostrar mais detalhes", + "expandedHint": "Fechado.", + "collapsedHint": "Aberto.", + "menuDismissLabel": "Dispensar menu", + "lookUpButtonLabel": "Pesquisar", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_pt_PT.arb b/packages/flutter_localizations/lib/src/l10n/material_pt_PT.arb index fbddcae14b80f..a7aa74964fe41 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_pt_PT.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_pt_PT.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "Digitalizar texto", + "lookUpButtonLabel": "Procurar", + "menuDismissLabel": "Ignorar menu", + "expansionTileExpandedHint": "toque duas vezes para reduzir", + "expansionTileCollapsedHint": "toque duas vezes para expandir", + "expansionTileExpandedTapHint": "Reduzir", + "expansionTileCollapsedTapHint": "Expandir para obter mais detalhes", + "expandedHint": "Reduzido", + "collapsedHint": "Expandido", "scrimLabel": "Scrim", "bottomSheetLabel": "Secção inferior", "scrimOnTapHint": "Fechar $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_ro.arb b/packages/flutter_localizations/lib/src/l10n/material_ro.arb index fcd7130a5d472..d85a49ff1fdc2 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ro.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ro.arb @@ -140,10 +140,14 @@ "bottomSheetLabel": "Foaie din partea de jos", "scrimOnTapHint": "Închideți $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "atingeți de două ori pentru a restrânge", + "expansionTileCollapsedHint": "atingeți de două ori pentru a extinde", + "expansionTileExpandedTapHint": "Restrângeți", + "expansionTileCollapsedTapHint": "Extindeți pentru mai multe detalii", + "expandedHint": "Restrâns", + "collapsedHint": "Extins", + "menuDismissLabel": "Respingeți meniul", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ru.arb b/packages/flutter_localizations/lib/src/l10n/material_ru.arb index 9b81a9cc66bc8..a73207d20f4c7 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ru.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ru.arb @@ -143,10 +143,14 @@ "bottomSheetLabel": "Нижний экран", "scrimOnTapHint": "Закрыть $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "нажмите дважды, чтобы свернуть", + "expansionTileCollapsedHint": "нажмите дважды, чтобы развернуть", + "expansionTileExpandedTapHint": "Свернуть", + "expansionTileCollapsedTapHint": "Развернуть дополнительные сведения", + "expandedHint": "Свернуто", + "collapsedHint": "Развернуто", + "menuDismissLabel": "Закрыть меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_si.arb b/packages/flutter_localizations/lib/src/l10n/material_si.arb index 4ac3ecfe99298..4d236f2a8c8a0 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_si.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_si.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "ඉදිරියට යන්න", "copyButtonLabel": "පිටපත් කරන්න", "cutButtonLabel": "කපන්න", - "scanTextButtonLabel": "පෙළ පරිලෝකනය කරන්න", + "scanTextButtonLabel": "පෙළ ස්කෑන් කරන්න", "okButtonLabel": "හරි", "pasteButtonLabel": "අලවන්න", "selectAllButtonLabel": "සියල්ල තෝරන්න", @@ -135,10 +135,14 @@ "bottomSheetLabel": "පහළම පත්‍රය", "scrimOnTapHint": "$modalRouteContentName වසන්න", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "හැකිළවීමට දෙවරක් තට්ටු කරන්න", + "expansionTileCollapsedHint": "විහිදුවීමට දෙවරක් තට්ටු කරන්න", + "expansionTileExpandedTapHint": "හකුළන්න", + "expansionTileCollapsedTapHint": "වැඩි විස්තර සඳහා පුළුල් කරන්න", + "expandedHint": "හකුළන ලදි", + "collapsedHint": "දිග හරින ලදි", + "menuDismissLabel": "මෙනුව අස් කරන්න", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sk.arb b/packages/flutter_localizations/lib/src/l10n/material_sk.arb index a85cfaf3b0936..5b2792a96a22b 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sk.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sk.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Pokračovať", "copyButtonLabel": "Kopírovať", "cutButtonLabel": "Vystrihnúť", - "scanTextButtonLabel": "Naskenujte text", + "scanTextButtonLabel": "Naskenovať text", "okButtonLabel": "OK", "pasteButtonLabel": "Prilepiť", "selectAllButtonLabel": "Vybrať všetko", @@ -142,10 +142,14 @@ "bottomSheetLabel": "Dolný hárok", "scrimOnTapHint": "Zavrieť $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "zbalíte dvojitým klepnutím", + "expansionTileCollapsedHint": "rozbalíte dvojitým klepnutím", + "expansionTileExpandedTapHint": "Zbaliť", + "expansionTileCollapsedTapHint": "Rozbaliť a zobraziť ďalšie podrobnosti", + "expandedHint": "Zbalené", + "collapsedHint": "Rozbalené", + "menuDismissLabel": "Zavrieť ponuku", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sl.arb b/packages/flutter_localizations/lib/src/l10n/material_sl.arb index 7fe2795860d4b..a850fcb0e73f4 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sl.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sl.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Naprej", "copyButtonLabel": "Kopiraj", "cutButtonLabel": "Izreži", - "scanTextButtonLabel": "Skeniraj besedilo", + "scanTextButtonLabel": "Optično preberite besedilo", "okButtonLabel": "V REDU", "pasteButtonLabel": "Prilepi", "selectAllButtonLabel": "Izberi vse", @@ -142,10 +142,14 @@ "bottomSheetLabel": "Razdelek na dnu zaslona", "scrimOnTapHint": "Zapiranje »$modalRouteContentName«", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "za strnitev se dvakrat dotaknite", + "expansionTileCollapsedHint": "za razširitev se dvakrat dotaknite", + "expansionTileExpandedTapHint": "Strni", + "expansionTileCollapsedTapHint": "Razširitev za več podrobnosti", + "expandedHint": "Strnjeno", + "collapsedHint": "Razširjeno", + "menuDismissLabel": "Opusti meni", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sq.arb b/packages/flutter_localizations/lib/src/l10n/material_sq.arb index 9db947b7c5025..82e7bf36597cb 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sq.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sq.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Vazhdo", "copyButtonLabel": "Kopjo", "cutButtonLabel": "Prit", - "scanTextButtonLabel": "Skanoni tekstin", + "scanTextButtonLabel": "Skano tekstin", "okButtonLabel": "Në rregull", "pasteButtonLabel": "Ngjit", "selectAllButtonLabel": "Zgjidh të gjitha", @@ -135,10 +135,14 @@ "bottomSheetLabel": "Fleta e poshtme", "scrimOnTapHint": "Mbyll $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "trokit dy herë për ta palosur", + "expansionTileCollapsedHint": "trokit dy herë për ta zgjeruar", + "expansionTileExpandedTapHint": "Palos", + "expansionTileCollapsedTapHint": "Zgjero për më shumë detaje", + "expandedHint": "U palos", + "collapsedHint": "U zgjerua", + "menuDismissLabel": "Hiqe menynë", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sr.arb b/packages/flutter_localizations/lib/src/l10n/material_sr.arb index 05865175bdb61..b1b745fb08273 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sr.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sr.arb @@ -28,7 +28,7 @@ "continueButtonLabel": "Настави", "copyButtonLabel": "Копирај", "cutButtonLabel": "Исеци", - "scanTextButtonLabel": "Скенирајте текст", + "scanTextButtonLabel": "Скенирај текст", "okButtonLabel": "Потврди", "pasteButtonLabel": "Налепи", "selectAllButtonLabel": "Изабери све", @@ -139,10 +139,14 @@ "bottomSheetLabel": "Доња табела", "scrimOnTapHint": "Затвори: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "двапут додирните да бисте скупили", + "expansionTileCollapsedHint": "двапут додирните да бисте проширили", + "expansionTileExpandedTapHint": "Скупите", + "expansionTileCollapsedTapHint": "Проширите за још детаља", + "expandedHint": "Скупљено је", + "collapsedHint": "Проширено је", + "menuDismissLabel": "Одбаците мени", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sr_Latn.arb b/packages/flutter_localizations/lib/src/l10n/material_sr_Latn.arb index 624b55cf0a643..d320ce96f4d9e 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sr_Latn.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sr_Latn.arb @@ -1,4 +1,12 @@ { + "scanTextButtonLabel": "Skeniraj tekst", + "menuDismissLabel": "Odbacite meni", + "expansionTileExpandedHint": "dvaput dodirnite da biste skupili", + "expansionTileCollapsedHint": "dvaput dodirnite da biste proširili", + "expansionTileExpandedTapHint": "Skupite", + "expansionTileCollapsedTapHint": "Proširite za još detalja", + "expandedHint": "Skupljeno je", + "collapsedHint": "Prošireno je", "scrimLabel": "Skrim", "bottomSheetLabel": "Donja tabela", "scrimOnTapHint": "Zatvori: $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_sv.arb b/packages/flutter_localizations/lib/src/l10n/material_sv.arb index 5e4573804a750..6c3925b1cd261 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sv.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sv.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Ark på nedre delen av skärmen", "scrimOnTapHint": "Stäng $modalRouteContentName", "keyboardKeyShift": "Skift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "tryck snabbt två gånger för att komprimera", + "expansionTileCollapsedHint": "tryck snabbt två gånger för att utöka", + "expansionTileExpandedTapHint": "Komprimera", + "expansionTileCollapsedTapHint": "Utöka för mer information", + "expandedHint": "Komprimerades", + "collapsedHint": "Utökades", + "menuDismissLabel": "Stäng menyn", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_sw.arb b/packages/flutter_localizations/lib/src/l10n/material_sw.arb index b31a119b598b0..8d42d166ea43a 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_sw.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_sw.arb @@ -137,10 +137,14 @@ "bottomSheetLabel": "Safu ya Chini", "scrimOnTapHint": "Funga $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "gusa mara mbili ili ukunje", + "expansionTileCollapsedHint": "gusa mara mbili ili upanue", + "expansionTileExpandedTapHint": "Kunja", + "expansionTileCollapsedTapHint": "Panua ili upate maelezo zaidi", + "expandedHint": "Imekunjwa", + "collapsedHint": "Imepanuliwa", + "menuDismissLabel": "Ondoa menyu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ta.arb b/packages/flutter_localizations/lib/src/l10n/material_ta.arb index f6fd8c3b3b6cc..6dbde632081c6 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ta.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ta.arb @@ -17,7 +17,7 @@ "reorderItemLeft": "இடப்புறம் நகர்த்தவும்", "reorderItemRight": "வலப்புறம் நகர்த்தவும்", "cutButtonLabel": "வெட்டு", - "scanTextButtonLabel": "உரையை ஸ்கேன் செய்யவும்", + "scanTextButtonLabel": "வார்த்தைகளை ஸ்கேன் செய்", "pasteButtonLabel": "ஒட்டு", "previousMonthTooltip": "முந்தைய மாதம்", "nextMonthTooltip": "அடுத்த மாதம்", @@ -137,10 +137,14 @@ "bottomSheetLabel": "கீழ்த் திரை", "scrimOnTapHint": "$modalRouteContentName ஐ மூடுக", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "சுருக்க இருமுறை தட்டவும்", + "expansionTileCollapsedHint": "விரிவாக்க இருமுறை தட்டுங்கள்", + "expansionTileExpandedTapHint": "சுருக்கும்", + "expansionTileCollapsedTapHint": "கூடுதல் விவரங்களுக்கு விரிவாக்கலாம்", + "expandedHint": "சுருக்கப்பட்டது", + "collapsedHint": "விரிவாக்கப்பட்டது", + "menuDismissLabel": "மெனுவை மூடும்", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_te.arb b/packages/flutter_localizations/lib/src/l10n/material_te.arb index 4220e89b98015..bc1abeef65607 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_te.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_te.arb @@ -24,7 +24,7 @@ "closeButtonLabel": "మూసివేయండి", "continueButtonLabel": "కొనసాగించండి", "copyButtonLabel": "కాపీ చేయి", - "scanTextButtonLabel": "వచనాన్ని స్కాన్ చేయండి", + "scanTextButtonLabel": "టెక్స్ట్‌ను స్కాన్ చేయండి", "cutButtonLabel": "కత్తిరించండి", "okButtonLabel": "సరే", "pasteButtonLabel": "పేస్ట్ చేయండి", @@ -135,10 +135,14 @@ "bottomSheetLabel": "దిగువున ఉన్న షీట్", "scrimOnTapHint": "$modalRouteContentName‌ను మూసివేయండి", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "కుదించడానికి డబుల్ ట్యాప్ చేయండి", + "expansionTileCollapsedHint": "విస్తరించడానికి డబుల్ ట్యాప్ చేయండి", + "expansionTileExpandedTapHint": "కుదించండి", + "expansionTileCollapsedTapHint": "మరిన్ని వివరాల కోసం విస్తరించండి", + "expandedHint": "కుదించబడింది", + "collapsedHint": "విస్తరించబడింది", + "menuDismissLabel": "మెనూను తీసివేయండి", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_th.arb b/packages/flutter_localizations/lib/src/l10n/material_th.arb index a0180b3b9b372..e12ffad42c386 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_th.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_th.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Bottom Sheet", "scrimOnTapHint": "ปิด $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "แตะสองครั้งเพื่อยุบ", + "expansionTileCollapsedHint": "แตะสองครั้งเพื่อขยาย", + "expansionTileExpandedTapHint": "ยุบ", + "expansionTileCollapsedTapHint": "ขยายเพื่อดูรายละเอียดเพิ่มเติม", + "expandedHint": "ยุบ", + "collapsedHint": "ขยาย", + "menuDismissLabel": "ปิดเมนู", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_tl.arb b/packages/flutter_localizations/lib/src/l10n/material_tl.arb index fbf38a18b99a2..81500d0ee0cb3 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_tl.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_tl.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Bottom Sheet", "scrimOnTapHint": "Isara ang $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "i-double tap para i-collapse", + "expansionTileCollapsedHint": "i-double tap para i-expand", + "expansionTileExpandedTapHint": "I-collapse", + "expansionTileCollapsedTapHint": "I-expand para sa higit pang detalye", + "expandedHint": "Naka-collapse", + "collapsedHint": "Naka-expand", + "menuDismissLabel": "I-dismiss ang menu", + "lookUpButtonLabel": "Tumingin sa Itaas", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_tr.arb b/packages/flutter_localizations/lib/src/l10n/material_tr.arb index b91d74d61651f..bfcbc7458b51c 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_tr.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_tr.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "Devam", "copyButtonLabel": "Kopyala", "cutButtonLabel": "Kes", - "scanTextButtonLabel": "Metni tara", + "scanTextButtonLabel": "Metin tara", "okButtonLabel": "Tamam", "pasteButtonLabel": "Yapıştır", "selectAllButtonLabel": "Tümünü seç", @@ -136,10 +136,14 @@ "bottomSheetLabel": "alt sayfa", "scrimOnTapHint": "$modalRouteContentName içeriğini kapat", "keyboardKeyShift": "üst karakter", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "daraltmak için iki kez dokunun", + "expansionTileCollapsedHint": "genişletmek için iki kez dokunun", + "expansionTileExpandedTapHint": "Daralt", + "expansionTileCollapsedTapHint": "Daha fazla ayrıntı için genişletin", + "expandedHint": "Daraltıldı", + "collapsedHint": "Genişletildi", + "menuDismissLabel": "Menüyü kapat", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_uk.arb b/packages/flutter_localizations/lib/src/l10n/material_uk.arb index fe9dcddaaebf4..40f9098644dde 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_uk.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_uk.arb @@ -31,7 +31,7 @@ "continueButtonLabel": "Продовжити", "copyButtonLabel": "Копіювати", "cutButtonLabel": "Вирізати", - "scanTextButtonLabel": "Сканувати текст", + "scanTextButtonLabel": "Відсканувати текст", "okButtonLabel": "OK", "pasteButtonLabel": "Вставити", "selectAllButtonLabel": "Вибрати всі", @@ -142,10 +142,14 @@ "bottomSheetLabel": "Нижній екран", "scrimOnTapHint": "Закрити: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "двічі торкніться, щоб згорнути", + "expansionTileCollapsedHint": "двічі торкніться, щоб розгорнути", + "expansionTileExpandedTapHint": "Згорнути", + "expansionTileCollapsedTapHint": "Розгорнути й дізнатися більше", + "expandedHint": "Згорнуто", + "collapsedHint": "Розгорнуто", + "menuDismissLabel": "Закрити меню", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_ur.arb b/packages/flutter_localizations/lib/src/l10n/material_ur.arb index c11abd3d38b6b..cb6c453d4fba3 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_ur.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_ur.arb @@ -25,7 +25,7 @@ "continueButtonLabel": "جاری رکھیں", "copyButtonLabel": "کاپی کریں", "cutButtonLabel": "کٹ کریں", - "scanTextButtonLabel": "متن کو اسکین کریں", + "scanTextButtonLabel": "ٹیکسٹ اسکین کریں", "okButtonLabel": "ٹھیک ہے", "pasteButtonLabel": "پیسٹ کریں", "selectAllButtonLabel": "سبھی کو منتخب کریں", @@ -136,10 +136,14 @@ "bottomSheetLabel": "نیچے کی شیٹ", "scrimOnTapHint": "$modalRouteContentName بند کریں", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "سکیڑنے کے لیے دوبار تھپتھپائیں", + "expansionTileCollapsedHint": "پھیلانے کے لیے دوبار تھپتھپائیں", + "expansionTileExpandedTapHint": "سکیڑیں", + "expansionTileCollapsedTapHint": "مزید تفصیلات کے لیے پھیلائیں", + "expandedHint": "سکڑا ہوا", + "collapsedHint": "پھیلا ہوا", + "menuDismissLabel": "مینو برخاست کریں", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_uz.arb b/packages/flutter_localizations/lib/src/l10n/material_uz.arb index f773009cc91f2..842624888b050 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_uz.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_uz.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "Quyi ekran", "scrimOnTapHint": "Yopish: $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "yigʻish uchun ikki marta bosing", + "expansionTileCollapsedHint": "yoyish uchun ikki marta bosing", + "expansionTileExpandedTapHint": "Yigʻish", + "expansionTileCollapsedTapHint": "Batafsil koʻrish uchun yoying", + "expandedHint": "Yigʻilgan", + "collapsedHint": "Yoyilgan", + "menuDismissLabel": "Menyuni yopish", + "lookUpButtonLabel": "Tepaga qarang", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_vi.arb b/packages/flutter_localizations/lib/src/l10n/material_vi.arb index 8f57237cc0f32..32b1cd43aa2d1 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_vi.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_vi.arb @@ -136,10 +136,14 @@ "bottomSheetLabel": "Bảng dưới cùng", "scrimOnTapHint": "Đóng $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "nhấn đúp để thu gọn", + "expansionTileCollapsedHint": "nhấn đúp để mở rộng", + "expansionTileExpandedTapHint": "Thu gọn", + "expansionTileCollapsedTapHint": "Mở rộng để xem thêm chi tiết", + "expandedHint": "Đã thu gọn", + "collapsedHint": "Đã mở rộng", + "menuDismissLabel": "Đóng trình đơn", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_zh.arb b/packages/flutter_localizations/lib/src/l10n/material_zh.arb index 66a28a60bc1af..0d65ab605fb89 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_zh.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_zh.arb @@ -21,7 +21,7 @@ "closeButtonLabel": "关闭", "copyButtonLabel": "复制", "cutButtonLabel": "剪切", - "scanTextButtonLabel": "扫描文本", + "scanTextButtonLabel": "扫描文字", "okButtonLabel": "确定", "pasteButtonLabel": "粘贴", "selectAllButtonLabel": "全选", @@ -136,10 +136,14 @@ "bottomSheetLabel": "底部动作条", "scrimOnTapHint": "关闭 $modalRouteContentName", "keyboardKeyShift": "Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "点按两次即可收起", + "expansionTileCollapsedHint": "点按两次即可展开", + "expansionTileExpandedTapHint": "收起", + "expansionTileCollapsedTapHint": "展开查看更多详情", + "expandedHint": "已收起", + "collapsedHint": "已展开", + "menuDismissLabel": "关闭菜单", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/l10n/material_zh_HK.arb b/packages/flutter_localizations/lib/src/l10n/material_zh_HK.arb index e3b44e58ec62b..efadc607395fe 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_zh_HK.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_zh_HK.arb @@ -1,4 +1,13 @@ { + "scanTextButtonLabel": "掃瞄文字", + "lookUpButtonLabel": "查詢", + "menuDismissLabel": "閂選單", + "expansionTileExpandedHint": "㩒兩下就可以收合", + "expansionTileCollapsedHint": "㩒兩下就可以展開", + "expansionTileExpandedTapHint": "收合", + "expansionTileCollapsedTapHint": "展開就可以查看詳情", + "expandedHint": "已收合", + "collapsedHint": "已展開", "scrimLabel": "Scrim", "bottomSheetLabel": "頁底面板", "scrimOnTapHint": "關閉 $modalRouteContentName", diff --git a/packages/flutter_localizations/lib/src/l10n/material_zh_TW.arb b/packages/flutter_localizations/lib/src/l10n/material_zh_TW.arb index 610a3fbd1d463..33564f3ad11b6 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_zh_TW.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_zh_TW.arb @@ -1,4 +1,12 @@ { + "scanTextButtonLabel": "掃描文字", + "menuDismissLabel": "關閉選單", + "expansionTileExpandedHint": "輕觸兩下即可收合", + "expansionTileCollapsedHint": "輕觸兩下即可展開", + "expansionTileExpandedTapHint": "收合", + "expansionTileCollapsedTapHint": "展開更多詳細資料", + "expandedHint": "已收合", + "collapsedHint": "已展開", "scrimLabel": "紗罩", "bottomSheetLabel": "底部功能表", "scrimOnTapHint": "關閉「$modalRouteContentName」", diff --git a/packages/flutter_localizations/lib/src/l10n/material_zu.arb b/packages/flutter_localizations/lib/src/l10n/material_zu.arb index aae990ba20d7a..8cf22c29b915e 100644 --- a/packages/flutter_localizations/lib/src/l10n/material_zu.arb +++ b/packages/flutter_localizations/lib/src/l10n/material_zu.arb @@ -135,10 +135,14 @@ "bottomSheetLabel": "Ishidi Eliphansi", "scrimOnTapHint": "Vala i-$modalRouteContentName", "keyboardKeyShift": "U-Shift", - "expansionTileExpandedHint": "double tap to collapse'", - "expansionTileCollapsedHint": "double tap to expand", - "expansionTileExpandedTapHint": "Collapse", - "expansionTileCollapsedTapHint": "Expand for more details", - "expandedHint": "Collapsed", - "collapsedHint": "Expanded" + "expansionTileExpandedHint": "thepha kabili ukuze ugoqe", + "expansionTileCollapsedHint": "Thepha kabili ukuze unwebe", + "expansionTileExpandedTapHint": "Goqa", + "expansionTileCollapsedTapHint": "Nweba ukuze uthole imininingwane eyengeziwe", + "expandedHint": "Kugoqiwe", + "collapsedHint": "Kunwetshiwe", + "menuDismissLabel": "Chitha imenyu", + "lookUpButtonLabel": "Look Up", + "searchWebButtonLabel": "Search Web", + "shareButtonLabel": "Share..." } diff --git a/packages/flutter_localizations/lib/src/material_localizations.dart b/packages/flutter_localizations/lib/src/material_localizations.dart index 477940e384972..e415db1726ba7 100644 --- a/packages/flutter_localizations/lib/src/material_localizations.dart +++ b/packages/flutter_localizations/lib/src/material_localizations.dart @@ -11,6 +11,10 @@ import 'l10n/generated_material_localizations.dart'; import 'utils/date_localizations.dart' as util; import 'widgets_localizations.dart'; +// Examples can assume: +// import 'package:flutter_localizations/flutter_localizations.dart'; +// import 'package:flutter/material.dart'; + /// Implementation of localized strings for the material widgets using the /// `intl` package for date and time formatting. /// @@ -30,11 +34,11 @@ import 'widgets_localizations.dart'; /// app supports with [MaterialApp.supportedLocales]: /// /// ```dart -/// MaterialApp( +/// const MaterialApp( /// localizationsDelegates: GlobalMaterialLocalizations.delegates, -/// supportedLocales: [ -/// const Locale('en', 'US'), // American English -/// const Locale('he', 'IL'), // Israeli Hebrew +/// supportedLocales: <Locale>[ +/// Locale('en', 'US'), // American English +/// Locale('he', 'IL'), // Israeli Hebrew /// // ... /// ], /// // ... @@ -681,11 +685,11 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations { /// app supports with [MaterialApp.supportedLocales]: /// /// ```dart - /// MaterialApp( + /// const MaterialApp( /// localizationsDelegates: GlobalMaterialLocalizations.delegates, - /// supportedLocales: [ - /// const Locale('en', 'US'), // English - /// const Locale('he', 'IL'), // Hebrew + /// supportedLocales: <Locale>[ + /// Locale('en', 'US'), // English + /// Locale('he', 'IL'), // Hebrew /// ], /// // ... /// ) diff --git a/packages/flutter_localizations/pubspec.yaml b/packages/flutter_localizations/pubspec.yaml index df2d0b4195bb2..dc9623e9718fe 100644 --- a/packages/flutter_localizations/pubspec.yaml +++ b/packages/flutter_localizations/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_localizations environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -12,12 +12,12 @@ dependencies: characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -28,10 +28,10 @@ dev_dependencies: fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 837c +# PUBSPEC CHECKSUM: bfd8 diff --git a/packages/flutter_localizations/test/cupertino/date_picker_test.dart b/packages/flutter_localizations/test/cupertino/date_picker_test.dart new file mode 100644 index 0000000000000..d94b5343a9056 --- /dev/null +++ b/packages/flutter_localizations/test/cupertino/date_picker_test.dart @@ -0,0 +1,47 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Test correct month form for CupertinoDatePicker in monthYear mode', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: Center( + child: CupertinoDatePicker( + initialDateTime: DateTime(2023, 5), + onDateTimeChanged: (_) {}, + mode: CupertinoDatePickerMode.monthYear, + )), + ), + supportedLocales: const <Locale>[Locale('ru', 'RU')], + localizationsDelegates: GlobalCupertinoLocalizations.delegates, + ), + ); + + expect(find.text('Май'), findsWidgets); + }); + + testWidgets('Test correct month form for CupertinoDatePicker in date mode', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: Center( + child: CupertinoDatePicker( + initialDateTime: DateTime(2023, 5), + onDateTimeChanged: (_) {}, + mode: CupertinoDatePickerMode.date, + )), + ), + supportedLocales: const <Locale>[Locale('ru', 'RU')], + localizationsDelegates: GlobalCupertinoLocalizations.delegates, + ), + ); + + expect(find.text('мая'), findsWidgets); + }); +} diff --git a/packages/flutter_localizations/test/cupertino/translations_test.dart b/packages/flutter_localizations/test/cupertino/translations_test.dart index 78fcd0263a2d2..a827e0d498f6e 100644 --- a/packages/flutter_localizations/test/cupertino/translations_test.dart +++ b/packages/flutter_localizations/test/cupertino/translations_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; @@ -33,6 +34,11 @@ void main() { expect(localizations.datePickerMonth(11), isNotNull); expect(localizations.datePickerMonth(12), isNotNull); + expect(localizations.datePickerStandaloneMonth(1), isNotNull); + expect(localizations.datePickerStandaloneMonth(2), isNotNull); + expect(localizations.datePickerStandaloneMonth(11), isNotNull); + expect(localizations.datePickerStandaloneMonth(12), isNotNull); + expect(localizations.datePickerDayOfMonth(0), isNotNull); expect(localizations.datePickerDayOfMonth(1), isNotNull); expect(localizations.datePickerDayOfMonth(2), isNotNull); @@ -194,4 +200,66 @@ void main() { expect(file.readAsStringSync(), encodedArbFile); } }); + + // Regression test for https://github.com/flutter/flutter/issues/110451. + testWidgets('Finnish translation for tab label', (WidgetTester tester) async { + const Locale locale = Locale('fi'); + expect(GlobalCupertinoLocalizations.delegate.isSupported(locale), isTrue); + final CupertinoLocalizations localizations = await GlobalCupertinoLocalizations.delegate.load(locale); + expect(localizations, isA<CupertinoLocalizationFi>()); + expect(localizations.tabSemanticsLabel(tabIndex: 1, tabCount: 2), 'Välilehti 1 kautta 2'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/130874. + testWidgets('buildButtonItems builds a localized "No Replacements found" button when no suggestions', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + locale: const Locale('ru'), + localizationsDelegates: GlobalCupertinoLocalizations.delegates, + supportedLocales: const <Locale>[Locale('en'), Locale('ru')], + home: _FakeEditableText() + ), + ); + final _FakeEditableTextState editableTextState = + tester.state(find.byType(_FakeEditableText)); + final List<ContextMenuButtonItem>? buttonItems = + CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(editableTextState); + + expect(buttonItems, isNotNull); + expect(buttonItems, hasLength(1)); + expect(buttonItems!.first.label, 'Варианты замены не найдены'); + expect(buttonItems.first.onPressed, isNull); + }); + +} + +class _FakeEditableText extends EditableText { + _FakeEditableText() : super( + controller: TextEditingController(), + focusNode: FocusNode(), + backgroundCursorColor: CupertinoColors.white, + cursorColor: CupertinoColors.white, + style: const TextStyle(), + ); + + @override + _FakeEditableTextState createState() => _FakeEditableTextState(); +} + +class _FakeEditableTextState extends EditableTextState { + _FakeEditableTextState(); + + @override + TextEditingValue get currentTextEditingValue => TextEditingValue.empty; + + @override + SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { + return const SuggestionSpan( + TextRange( + start: 0, + end: 0, + ), + <String>[], + ); + } } diff --git a/packages/flutter_localizations/test/material/date_time_test.dart b/packages/flutter_localizations/test/material/date_time_test.dart index 6fb4aece19af3..4cab48f84b1a0 100644 --- a/packages/flutter_localizations/test/material/date_time_test.dart +++ b/packages/flutter_localizations/test/material/date_time_test.dart @@ -110,6 +110,7 @@ void main() { testWidgets('formats ${TimeOfDayFormat.HH_dot_mm}', (WidgetTester tester) async { expect(await formatTimeOfDay(tester, const Locale('fi'), const TimeOfDay(hour: 20, minute: 32)), '20.32'); expect(await formatTimeOfDay(tester, const Locale('fi'), const TimeOfDay(hour: 9, minute: 32)), '09.32'); + expect(await formatTimeOfDay(tester, const Locale('da'), const TimeOfDay(hour: 9, minute: 32)), '09.32'); }); testWidgets('formats ${TimeOfDayFormat.frenchCanadian}', (WidgetTester tester) async { diff --git a/packages/flutter_localizations/test/material/translations_test.dart b/packages/flutter_localizations/test/material/translations_test.dart index c914362031298..cb0e38ad2406f 100644 --- a/packages/flutter_localizations/test/material/translations_test.dart +++ b/packages/flutter_localizations/test/material/translations_test.dart @@ -528,4 +528,13 @@ void main() { expect(file.readAsStringSync(), encodedArbFile); } }); + + // Regression test for https://github.com/flutter/flutter/issues/110451. + testWidgets('Finnish translation for tab label', (WidgetTester tester) async { + const Locale locale = Locale('fi'); + expect(GlobalCupertinoLocalizations.delegate.isSupported(locale), isTrue); + final MaterialLocalizations localizations = await GlobalMaterialLocalizations.delegate.load(locale); + expect(localizations, isA<MaterialLocalizationFi>()); + expect(localizations.tabLabel(tabIndex: 1, tabCount: 2), 'Välilehti 1 kautta 2'); + }); } diff --git a/packages/flutter_localizations/test/text_test.dart b/packages/flutter_localizations/test/text_test.dart index 310cdf0ea2d86..bd3528a53f721 100644 --- a/packages/flutter_localizations/test/text_test.dart +++ b/packages/flutter_localizations/test/text_test.dart @@ -71,25 +71,11 @@ void main() { expect(find.text('hello, world'), findsOneWidget); expect(find.text('你好,世界'), findsOneWidget); - Offset topLeft = tester.getTopLeft(find.text('hello, world')); - Offset topRight = tester.getTopRight(find.text('hello, world')); - Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); - Offset bottomRight = tester.getBottomRight(find.text('hello, world')); + expect(tester.getTopLeft(find.text('hello, world')).dy, 298.0); + expect(tester.getBottomLeft(find.text('hello, world')).dy, 318.0); - expect(topLeft, const Offset(392.0, 298.0)); - expect(topRight, const Offset(562.0, 298.0)); - expect(bottomLeft, const Offset(392.0, 318.0)); - expect(bottomRight, const Offset(562.0, 318.0)); - - topLeft = tester.getTopLeft(find.text('你好,世界')); - topRight = tester.getTopRight(find.text('你好,世界')); - bottomLeft = tester.getBottomLeft(find.text('你好,世界')); - bottomRight = tester.getBottomRight(find.text('你好,世界')); - - expect(topLeft, const Offset(392.0, 346.0)); - expect(topRight, const Offset(463.0, 346.0)); - expect(bottomLeft, const Offset(392.0, 366.0)); - expect(bottomRight, const Offset(463.0, 366.0)); + expect(tester.getTopLeft(find.text('你好,世界')).dy, 346.0); + expect(tester.getBottomLeft(find.text('你好,世界')).dy, 366.0); }); testWidgets('Text baseline with EN locale', (WidgetTester tester) async { @@ -156,24 +142,10 @@ void main() { expect(find.text('hello, world'), findsOneWidget); expect(find.text('你好,世界'), findsOneWidget); - Offset topLeft = tester.getTopLeft(find.text('hello, world')); - Offset topRight = tester.getTopRight(find.text('hello, world')); - Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); - Offset bottomRight = tester.getBottomRight(find.text('hello, world')); - - expect(topLeft, const Offset(392.0, 298.0)); - expect(topRight, const Offset(562.0, 298.0)); - expect(bottomLeft, const Offset(392.0, 318.0)); - expect(bottomRight, const Offset(562.0, 318.0)); - - topLeft = tester.getTopLeft(find.text('你好,世界')); - topRight = tester.getTopRight(find.text('你好,世界')); - bottomLeft = tester.getBottomLeft(find.text('你好,世界')); - bottomRight = tester.getBottomRight(find.text('你好,世界')); + expect(tester.getTopLeft(find.text('hello, world')).dy, 298.0); + expect(tester.getBottomLeft(find.text('hello, world')).dy, 318.0); - expect(topLeft, const Offset(392.0, 346.0)); - expect(topRight, const Offset(463.0, 346.0)); - expect(bottomLeft, const Offset(392.0, 366.0)); - expect(bottomRight, const Offset(463.0, 366.0)); + expect(tester.getTopLeft(find.text('你好,世界')).dy, 346.0); + expect(tester.getBottomLeft(find.text('你好,世界')).dy, 366.0); }); } diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart index 55e0d20ff160d..9d76239f03742 100644 --- a/packages/flutter_test/lib/flutter_test.dart +++ b/packages/flutter_test/lib/flutter_test.dart @@ -27,7 +27,7 @@ /// with the following signature: /// /// ```dart -/// Future<void> testExecutable(FutureOr<void> Function() testMain); +/// Future<void> testExecutable(FutureOr<void> Function() testMain) async { } /// ``` /// /// The test framework will execute that method and pass it the `main()` method @@ -58,7 +58,6 @@ export 'dart:async' show Future; export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart'; export 'src/accessibility.dart'; -export 'src/all_elements.dart'; export 'src/animation_sheet.dart'; export 'src/binding.dart'; export 'src/controller.dart'; @@ -69,9 +68,11 @@ export 'src/frame_timing_summarizer.dart'; export 'src/goldens.dart'; export 'src/image.dart'; export 'src/matchers.dart'; +export 'src/mock_canvas.dart'; export 'src/mock_event_channel.dart'; export 'src/nonconst.dart'; export 'src/platform.dart'; +export 'src/recording_canvas.dart'; export 'src/restoration.dart'; export 'src/stack_manipulation.dart'; export 'src/test_async_utils.dart'; @@ -81,5 +82,6 @@ export 'src/test_exception_reporter.dart'; export 'src/test_pointer.dart'; export 'src/test_text_input.dart'; export 'src/test_vsync.dart'; +export 'src/tree_traversal.dart'; export 'src/widget_tester.dart'; export 'src/window.dart'; diff --git a/packages/flutter_test/lib/src/_binding_io.dart b/packages/flutter_test/lib/src/_binding_io.dart index 710823682ffae..a789b9aabe97c 100644 --- a/packages/flutter_test/lib/src/_binding_io.dart +++ b/packages/flutter_test/lib/src/_binding_io.dart @@ -71,17 +71,21 @@ void mockFlutterAssets() { class _MockHttpOverrides extends HttpOverrides { bool warningPrinted = false; @override - HttpClient createHttpClient(SecurityContext? _) { + HttpClient createHttpClient(SecurityContext? context) { if (!warningPrinted) { test_package.printOnFailure( - 'Warning: At least one test in this suite creates an HttpClient. When\n' - 'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n' - 'requests will return status code 400, and no network request will\n' - 'actually be made. Any test expecting a real network connection and\n' + 'Warning: At least one test in this suite creates an HttpClient. When ' + 'running a test suite that uses TestWidgetsFlutterBinding, all HTTP ' + 'requests will return status code 400, and no network request will ' + 'actually be made. Any test expecting a real network connection and ' 'status code will fail.\n' - 'To test code that needs an HttpClient, provide your own HttpClient\n' - 'implementation to the code under test, so that your test can\n' - 'consistently provide a testable response to the code under test.'); + 'To test code that needs an HttpClient, provide your own HttpClient ' + 'implementation to the code under test, so that your test can ' + 'consistently provide a testable response to the code under test.' + .split('\n') + .expand<String>((String line) => debugWordWrap(line, FlutterError.wrapWidth)) + .join('\n'), + ); warningPrinted = true; } return _MockHttpClient(); @@ -124,7 +128,7 @@ class _MockHttpClient implements HttpClient { bool Function(X509Certificate cert, String host, int port)? badCertificateCallback; @override - Function(String line)? keyLog; + void Function(String line)? keyLog; @override void close({ bool force = false }) { } diff --git a/packages/flutter_test/lib/src/_matchers_io.dart b/packages/flutter_test/lib/src/_matchers_io.dart index 74a3d497609b5..0c17612d28d53 100644 --- a/packages/flutter_test/lib/src/_matchers_io.dart +++ b/packages/flutter_test/lib/src/_matchers_io.dart @@ -60,8 +60,9 @@ class MatchesGoldenFile extends AsyncMatcher { final Uri testNameUri = goldenFileComparator.getTestUri(key, version); Uint8List? buffer; - if (item is Future<List<int>>) { - buffer = Uint8List.fromList(await item); + if (item is Future<List<int>?>) { + final List<int>? bytes = await item; + buffer = bytes == null ? null : Uint8List.fromList(bytes); } else if (item is List<int>) { buffer = Uint8List.fromList(item); } diff --git a/packages/flutter_test/lib/src/_matchers_web.dart b/packages/flutter_test/lib/src/_matchers_web.dart index 749b57164eda3..7f54720eb236c 100644 --- a/packages/flutter_test/lib/src/_matchers_web.dart +++ b/packages/flutter_test/lib/src/_matchers_web.dart @@ -58,8 +58,8 @@ class MatchesGoldenFile extends AsyncMatcher { final RenderObject renderObject = _findRepaintBoundary(element); final Size size = renderObject.paintBounds.size; final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; - final Element e = binding.rootElement!; final ui.FlutterView view = binding.platformDispatcher.implicitView!; + final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view); // Unlike `flutter_tester`, we don't have the ability to render an element // to an image directly. Instead, we will use `window.render()` to render @@ -78,7 +78,7 @@ class MatchesGoldenFile extends AsyncMatcher { return ex.message; } }); - _renderElement(view, _findRepaintBoundary(e)); + _renderElement(view, renderView); return result; } diff --git a/packages/flutter_test/lib/src/accessibility.dart b/packages/flutter_test/lib/src/accessibility.dart index a696837d3a623..513e652236d9c 100644 --- a/packages/flutter_test/lib/src/accessibility.dart +++ b/packages/flutter_test/lib/src/accessibility.dart @@ -57,6 +57,9 @@ class Evaluation { } } +// Examples can assume: +// typedef HomePage = Placeholder; + /// An accessibility guideline describes a recommendation an application should /// meet to be considered accessible. /// @@ -131,11 +134,10 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { @override FutureOr<Evaluation> evaluate(WidgetTester tester) { Evaluation result = const Evaluation.pass(); - for (final FlutterView view in tester.platformDispatcher.views) { + for (final RenderView view in tester.binding.renderViews) { result += _traverse( - view, - // TODO(pdblasi-google): Get the specific semantics root for this view when available - tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, + view.flutterView, + view.owner!.semanticsOwner!.rootSemanticsNode!, ); } @@ -239,10 +241,8 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline { FutureOr<Evaluation> evaluate(WidgetTester tester) { Evaluation result = const Evaluation.pass(); - // TODO(pdblasi-google): Use view to retrieve the appropriate root semantics node when available. - // ignore: unused_local_variable - for (final FlutterView view in tester.platformDispatcher.views) { - result += _traverse(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!); + for (final RenderView view in tester.binding.renderViews) { + result += _traverse(view.owner!.semanticsOwner!.rootSemanticsNode!); } return result; @@ -318,9 +318,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { @override Future<Evaluation> evaluate(WidgetTester tester) async { Evaluation result = const Evaluation.pass(); - for (final FlutterView view in tester.platformDispatcher.views) { - // TODO(pdblasi): This renderView will need to be retrieved from view when available. - final RenderView renderView = tester.binding.renderView; + for (final RenderView renderView in tester.binding.renderViews) { final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!; @@ -329,13 +327,15 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { () async { // Needs to be the same pixel ratio otherwise our dimensions won't match // the last transform layer. - final double ratio = 1 / view.devicePixelRatio; + final double ratio = 1 / renderView.flutterView.devicePixelRatio; image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio); - return image.toByteData(); + final ByteData? data = await image.toByteData(); + image.dispose(); + return data; }, ); - result += await _evaluateNode(root, tester, image, byteData!, view); + result += await _evaluateNode(root, tester, image, byteData!, renderView); } return result; @@ -346,7 +346,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { WidgetTester tester, ui.Image image, ByteData byteData, - FlutterView view, + RenderView renderView, ) async { Evaluation result = const Evaluation.pass(); @@ -368,7 +368,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { return true; }); for (final SemanticsNode child in children) { - result += await _evaluateNode(child, tester, image, byteData, view); + result += await _evaluateNode(child, tester, image, byteData, renderView); } if (shouldSkipNode(data)) { return result; @@ -376,7 +376,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { final String text = data.label.isEmpty ? data.value : data.label; final Iterable<Element> elements = find.text(text).hitTestable().evaluate(); for (final Element element in elements) { - result += await _evaluateElement(node, element, tester, image, byteData, view); + result += await _evaluateElement(node, element, tester, image, byteData, renderView); } return result; } @@ -387,7 +387,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { WidgetTester tester, ui.Image image, ByteData byteData, - FlutterView view, + RenderView renderView, ) async { // Look up inherited text properties to determine text size and weight. late bool isBold; @@ -408,7 +408,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { // not included in renderBox.getTransformTo(null). Manually multiply the // root transform to the global transform. final Matrix4 rootTransform = Matrix4.identity(); - tester.binding.renderView.applyPaintTransform(tester.binding.renderView.child!, rootTransform); + renderView.applyPaintTransform(renderView.child!, rootTransform); rootTransform.multiply(globalTransform); screenBounds = MatrixUtils.transformRect(rootTransform, renderBox.paintBounds); Rect nodeBounds = node.rect; @@ -443,7 +443,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { throw StateError('Unexpected widget type: ${widget.runtimeType}'); } - if (isNodeOffScreen(paintBoundsWithOffset, view)) { + if (isNodeOffScreen(paintBoundsWithOffset, renderView.flutterView)) { return const Evaluation.pass(); } @@ -562,9 +562,7 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline { Evaluation result = const Evaluation.pass(); for (final Element element in elements) { final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element)); - - // TODO(pdblasi): Obtain this renderView from view when possible. - final RenderView renderView = tester.binding.renderView; + final RenderView renderView = tester.binding.renderViews.firstWhere((RenderView r) => r.flutterView == view); final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; late final ui.Image image; diff --git a/packages/flutter_test/lib/src/all_elements.dart b/packages/flutter_test/lib/src/all_elements.dart deleted file mode 100644 index 76e62eb94b0a9..0000000000000 --- a/packages/flutter_test/lib/src/all_elements.dart +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -/// Provides an iterable that efficiently returns all the elements -/// rooted at the given element. See [CachingIterable] for details. -/// -/// This method must be called again if the tree changes. You cannot -/// call this function once, then reuse the iterable after having -/// changed the state of the tree, because the iterable returned by -/// this function caches the results and only walks the tree once. -/// -/// The same applies to any iterable obtained indirectly through this -/// one, for example the results of calling `where` on this iterable -/// are also cached. -Iterable<Element> collectAllElementsFrom( - Element rootElement, { - required bool skipOffstage, -}) { - return CachingIterable<Element>(_DepthFirstChildIterator(rootElement, skipOffstage)); -} - -/// Provides a recursive, efficient, depth first search of an element tree. -/// -/// [Element.visitChildren] does not guarantee order, but does guarantee stable -/// order. This iterator also guarantees stable order, and iterates in a left -/// to right order: -/// -/// 1 -/// / \ -/// 2 3 -/// / \ / \ -/// 4 5 6 7 -/// -/// Will iterate in order 2, 4, 5, 3, 6, 7. -/// -/// Performance is important here because this method is on the critical path -/// for flutter_driver and package:integration_test performance tests. -/// Performance is measured in the all_elements_bench microbenchmark. -/// Any changes to this implementation should check the before and after numbers -/// on that benchmark to avoid regressions in general performance test overhead. -/// -/// If we could use RTL order, we could save on performance, but numerous tests -/// have been written (and developers clearly expect) that LTR order will be -/// respected. -class _DepthFirstChildIterator implements Iterator<Element> { - _DepthFirstChildIterator(Element rootElement, this.skipOffstage) { - _fillChildren(rootElement); - } - - final bool skipOffstage; - - late Element _current; - - final List<Element> _stack = <Element>[]; - - @override - Element get current => _current; - - @override - bool moveNext() { - if (_stack.isEmpty) { - return false; - } - - _current = _stack.removeLast(); - _fillChildren(_current); - - return true; - } - - void _fillChildren(Element element) { - // If we did not have to follow LTR order and could instead use RTL, - // we could avoid reversing this and the operation would be measurably - // faster. Unfortunately, a lot of tests depend on LTR order. - final List<Element> reversed = <Element>[]; - if (skipOffstage) { - element.debugVisitOnstageChildren(reversed.add); - } else { - element.visitChildren(reversed.add); - } - // This is faster than _stack.addAll(reversed.reversed), presumably since - // we don't actually care about maintaining an iteration pointer. - while (reversed.isNotEmpty) { - _stack.add(reversed.removeLast()); - } - } -} diff --git a/packages/flutter_test/lib/src/animation_sheet.dart b/packages/flutter_test/lib/src/animation_sheet.dart index 22973287e9fee..3d0d0bf65ed1b 100644 --- a/packages/flutter_test/lib/src/animation_sheet.dart +++ b/packages/flutter_test/lib/src/animation_sheet.dart @@ -9,6 +9,36 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +// A Future<ui.Image> that stores the resolved result. +class _AsyncImage { + _AsyncImage(Future<ui.Image> task) { + _task = task.then((ui.Image image) { + _result = image; + }); + } + + // Returns the resolved image. + Future<ui.Image> result() async { + if (_result != null) { + return _result!; + } + await _task; + assert(_result != null); + return _result!; + } + + late final Future<void> _task; + ui.Image? _result; + + // Wait for a list of `_AsyncImage` and returns the list of its resolved + // images. + static Future<List<ui.Image>> resolveList(List<_AsyncImage> targets) { + final Iterable<Future<ui.Image>> images = targets.map<Future<ui.Image>>( + (_AsyncImage target) => target.result()); + return Future.wait<ui.Image>(images); + } +} + /// Records the frames of an animating widget, and later displays the frames as a /// grid in an animation sheet. /// @@ -20,6 +50,7 @@ import 'package:flutter/widgets.dart'; /// Using this class includes the following steps: /// /// * Create an instance of this class. +/// * Register [dispose] to the test's tear down callbacks. /// * Pump frames that render the target widget wrapped in [record]. Every frame /// that has `recording` being true will be recorded. /// * Acquire the output image with [collate] and compare against the golden @@ -33,6 +64,7 @@ import 'package:flutter/widgets.dart'; /// testWidgets('Inkwell animation sheet', (WidgetTester tester) async { /// // Create instance /// final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(48, 24)); +/// addTearDown(animationSheet.dispose); /// /// final Widget target = Material( /// child: Directionality( @@ -55,14 +87,14 @@ import 'package:flutter/widgets.dart'; /// // Start recording (`recording` is true) /// await tester.pumpFrames(animationSheet.record( /// target, -/// recording: true, +/// recording: true, // ignore: avoid_redundant_argument_values /// ), const Duration(seconds: 1)); /// /// await gesture.up(); /// /// await tester.pumpFrames(animationSheet.record( /// target, -/// recording: true, +/// recording: true, // ignore: avoid_redundant_argument_values /// ), const Duration(seconds: 1)); /// /// // Compare against golden file @@ -90,6 +122,24 @@ class AnimationSheetBuilder { this.allLayers = false, }) : assert(!kIsWeb); + /// Dispose all recorded frames and result images. + /// + /// This method must be called before the test case ends (usually as a tear + /// down callback) to properly deallocate the images. + /// + /// After this method is called, there will be no frames to [collate]. + Future<void> dispose() async { + final List<_AsyncImage> targets = <_AsyncImage>[ + ..._recordedFrames, + ..._results, + ]; + _recordedFrames.clear(); + _results.clear(); + for (final ui.Image image in await _AsyncImage.resolveList(targets)) { + image.dispose(); + } + } + /// The size of the child to be recorded. /// /// This size is applied as a tight layout constraint for the child, and is @@ -112,20 +162,7 @@ class AnimationSheetBuilder { /// Defaults to false. final bool allLayers; - final List<Future<ui.Image>> _recordedFrames = <Future<ui.Image>>[]; - Future<List<ui.Image>> get _frames async { - final List<ui.Image> frames = await Future.wait<ui.Image>(_recordedFrames, eagerError: true); - assert(() { - for (final ui.Image frame in frames) { - assert(frame.width == frameSize.width && frame.height == frameSize.height, - 'Unexpected size mismatch: frame has (${frame.width}, ${frame.height}) ' - 'while `frameSize` is $frameSize.' - ); - } - return true; - }()); - return frames; - } + final List<_AsyncImage> _recordedFrames = <_AsyncImage>[]; /// Returns a widget that renders a widget in a box that can be recorded. /// @@ -138,8 +175,6 @@ class AnimationSheetBuilder { /// [collate]. If neither condition is met, the frames are not recorded, which /// is useful during setup phases. /// - /// The `child` must not be null. - /// /// See also: /// /// * [WidgetTester.pumpFrames], which renders a widget in a series of frames @@ -152,22 +187,41 @@ class AnimationSheetBuilder { key: key, size: frameSize, allLayers: allLayers, - handleRecorded: recording ? _recordedFrames.add : null, + handleRecorded: !recording ? null : (Future<ui.Image> futureImage) { + _recordedFrames.add(_AsyncImage(() async { + final ui.Image image = await futureImage; + assert(image.width == frameSize.width && image.height == frameSize.height, + 'Unexpected size mismatch: frame has (${image.width}, ${image.height}) ' + 'while `frameSize` is $frameSize.' + ); + return image; + }())); + }, child: child, ); } + // The result images generated by `collate`. + // + // They're stored here to be disposed by [dispose]. + final List<_AsyncImage> _results = <_AsyncImage>[]; + /// Returns an result image by putting all frames together in a table. /// - /// This method returns a table of captured frames, `cellsPerRow` images - /// per row, from left to right, top to bottom. + /// This method returns an image that arranges the captured frames in a table, + /// which has `cellsPerRow` images per row with the order from left to right, + /// top to bottom. + /// + /// The result image of this method is managed by [AnimationSheetBuilder], + /// and should not be disposed by the caller. /// /// An example of using this method can be found at [AnimationSheetBuilder]. Future<ui.Image> collate(int cellsPerRow) async { - final List<ui.Image> frames = await _frames; - assert(frames.isNotEmpty, + assert(_recordedFrames.isNotEmpty, 'No frames are collected. Have you forgot to set `recording` to true?'); - return _collateFrames(frames, frameSize, cellsPerRow); + final _AsyncImage result = _AsyncImage(_collateFrames(_recordedFrames, frameSize, cellsPerRow)); + _results.add(result); + return result.result(); } } @@ -281,7 +335,8 @@ class _RenderPostFrameCallbacker extends RenderProxyBox { } } -Future<ui.Image> _collateFrames(List<ui.Image> frames, Size frameSize, int cellsPerRow) async { +Future<ui.Image> _collateFrames(List<_AsyncImage> futureFrames, Size frameSize, int cellsPerRow) async { + final List<ui.Image> frames = await _AsyncImage.resolveList(futureFrames); final int rowNum = (frames.length / cellsPerRow).ceil(); final ui.PictureRecorder recorder = ui.PictureRecorder(); diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 23caf1f33b3d3..7be1c7a71bbc0 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -143,6 +143,10 @@ class CapturedAccessibilityAnnouncement { final Assertiveness assertiveness; } +// Examples can assume: +// late TestWidgetsFlutterBinding binding; +// late Size someSize; + /// Base class for bindings used by widgets library tests. /// /// The [ensureInitialized] method creates (if necessary) and returns an @@ -240,6 +244,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// If [registerTestTextInput] returns true when this method is called, /// the [testTextInput] is configured to simulate the keyboard. void reset() { + _restorationManager?.dispose(); _restorationManager = null; resetGestureBinding(); testTextInput.reset(); @@ -488,15 +493,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase } /// Re-attempts the initialization of the lifecycle state after providing - /// test values in [TestWindow.initialLifecycleStateTestValue]. + /// test values in [TestPlatformDispatcher.initialLifecycleStateTestValue]. void readTestInitialLifecycleStateFromNativeWindow() { readInitialLifecycleStateFromNativeWindow(); } Size? _surfaceSize; - /// Artificially changes the surface size to `size` on the Widget binding, - /// then flushes microtasks. + /// Artificially changes the logical size of [WidgetTester.view] to the + /// specified size, then flushes microtasks. /// /// Set to null to use the default surface size. /// @@ -508,7 +513,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// addTearDown(() => binding.setSurfaceSize(null)); /// ``` /// - /// See also [TestFlutterView.physicalSize], which has a similar effect. + /// This method only affects the size of the [WidgetTester.view]. It does not + /// affect the size of any other views. Instead of this method, consider + /// setting [TestFlutterView.physicalSize], which works for any view, + /// including [WidgetTester.view]. // TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881 Future<void> setSurfaceSize(Size? size) { return TestAsyncUtils.guard<void>(() async { @@ -522,14 +530,37 @@ abstract class TestWidgetsFlutterBinding extends BindingBase } @override - ViewConfiguration createViewConfiguration() { - final FlutterView view = platformDispatcher.implicitView!; - final double devicePixelRatio = view.devicePixelRatio; - final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio; - return ViewConfiguration( - size: size, - devicePixelRatio: devicePixelRatio, - ); + void addRenderView(RenderView view) { + _insideAddRenderView = true; + try { + super.addRenderView(view); + } finally { + _insideAddRenderView = false; + } + } + + bool _insideAddRenderView = false; + + @override + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + if (_insideAddRenderView + && renderView.hasConfiguration + && renderView.configuration is TestViewConfiguration + && renderView == this.renderView) { // ignore: deprecated_member_use + // If a test has reached out to the now deprecated renderView property to set a custom TestViewConfiguration + // we are not replacing it. This is to maintain backwards compatibility with how things worked prior to the + // deprecation of that property. + // TODO(goderbauer): Remove this "if" when the deprecated renderView property is removed. + return renderView.configuration; + } + final FlutterView view = renderView.flutterView; + if (_surfaceSize != null && view == platformDispatcher.implicitView) { + return ViewConfiguration( + size: _surfaceSize!, + devicePixelRatio: view.devicePixelRatio, + ); + } + return super.createViewConfigurationFor(renderView); } /// Acts as if the application went idle. @@ -751,7 +782,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// /// The `description` is used by the [LiveTestWidgetsFlutterBinding] to /// show a label on the screen during the test. The description comes from - /// the value passed to [testWidgets]. It must not be null. + /// the value passed to [testWidgets]. Future<void> runTest( Future<void> Function() testBody, VoidCallback invariantTester, { @@ -1227,7 +1258,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { if (hasScheduledFrame) { _currentFakeAsync!.flushMicrotasks(); handleBeginFrame(Duration( - milliseconds: _clock!.now().millisecondsSinceEpoch, + microseconds: _clock!.now().microsecondsSinceEpoch, )); _currentFakeAsync!.flushMicrotasks(); handleDrawFrame(); @@ -1377,16 +1408,18 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { debugBuildingDirtyElements = true; buildOwner!.buildScope(rootElement!); if (_phase != EnginePhase.build) { - pipelineOwner.flushLayout(); + rootPipelineOwner.flushLayout(); if (_phase != EnginePhase.layout) { - pipelineOwner.flushCompositingBits(); + rootPipelineOwner.flushCompositingBits(); if (_phase != EnginePhase.compositingBits) { - pipelineOwner.flushPaint(); + rootPipelineOwner.flushPaint(); if (_phase != EnginePhase.paint && sendFramesToEngine) { _firstFrameSent = true; - renderView.compositeFrame(); // this sends the bits to the GPU + for (final RenderView renderView in renderViews) { + renderView.compositeFrame(); // this sends the bits to the GPU + } if (_phase != EnginePhase.composite) { - pipelineOwner.flushSemantics(); + rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS. assert(_phase == EnginePhase.flushSemantics || _phase == EnginePhase.sendSemanticsUpdate); } @@ -1520,7 +1553,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { /// ```dart /// TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); /// if (binding is LiveTestWidgetsFlutterBinding) { -/// binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.[thePolicy]; +/// binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps; /// } /// ``` /// {@endtemplate} @@ -1759,9 +1792,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } } - void _markViewNeedsPaint() { + void _markViewsNeedPaint([int? viewId]) { _viewNeedsPaint = true; - renderView.markNeedsPaint(); + final Iterable<RenderView> toMark = viewId == null + ? renderViews + : renderViews.where((RenderView renderView) => renderView.flutterView.viewId == viewId); + for (final RenderView renderView in toMark) { + renderView.markNeedsPaint(); + } } TextPainter? _label; @@ -1779,15 +1817,16 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr); _label!.text = TextSpan(text: value, style: _labelStyle); _label!.layout(); - _markViewNeedsPaint(); + _markViewsNeedPaint(); } - final Map<int, _LiveTestPointerRecord> _pointerIdToPointerRecord = <int, _LiveTestPointerRecord>{}; + final Expando<Map<int, _LiveTestPointerRecord>> _renderViewToPointerIdToPointerRecord = Expando<Map<int, _LiveTestPointerRecord>>(); void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) { assert(offset == Offset.zero); - if (_pointerIdToPointerRecord.isNotEmpty) { + final Map<int, _LiveTestPointerRecord>? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView]; + if (pointerIdToRecord != null && pointerIdToRecord.isNotEmpty) { final double radius = renderView.configuration.size.shortestSide * 0.05; final Path path = Path() ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius)) @@ -1800,7 +1839,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ..strokeWidth = radius / 10.0 ..style = PaintingStyle.stroke; bool dirty = false; - for (final _LiveTestPointerRecord record in _pointerIdToPointerRecord.values) { + for (final _LiveTestPointerRecord record in pointerIdToRecord.values) { paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0); canvas.drawPath(path.shift(record.position), paint); if (record.decay < 0) { @@ -1808,14 +1847,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } record.decay += 1; } - _pointerIdToPointerRecord + pointerIdToRecord .keys - .where((int pointer) => _pointerIdToPointerRecord[pointer]!.decay == 0) + .where((int pointer) => pointerIdToRecord[pointer]!.decay == 0) .toList() - .forEach(_pointerIdToPointerRecord.remove); + .forEach(pointerIdToRecord.remove); if (dirty) { scheduleMicrotask(() { - _markViewNeedsPaint(); + _markViewsNeedPaint(renderView.flutterView.viewId); }); } } @@ -1846,19 +1885,29 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { void handlePointerEvent(PointerEvent event) { switch (pointerEventSource) { case TestBindingEventSource.test: - final _LiveTestPointerRecord? record = _pointerIdToPointerRecord[event.pointer]; - if (record != null) { - record.position = event.position; - if (!event.down) { - record.decay = _kPointerDecay; + RenderView? target; + for (final RenderView renderView in renderViews) { + if (renderView.flutterView.viewId == event.viewId) { + target = renderView; + break; + } + } + if (target != null) { + final _LiveTestPointerRecord? record = _renderViewToPointerIdToPointerRecord[target]?[event.pointer]; + if (record != null) { + record.position = event.position; + if (!event.down) { + record.decay = _kPointerDecay; + } + _markViewsNeedPaint(event.viewId); + } else if (event.down) { + _renderViewToPointerIdToPointerRecord[target] ??= <int, _LiveTestPointerRecord>{}; + _renderViewToPointerIdToPointerRecord[target]![event.pointer] = _LiveTestPointerRecord( + event.pointer, + event.position, + ); + _markViewsNeedPaint(event.viewId); } - _markViewNeedsPaint(); - } else if (event.down) { - _pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord( - event.pointer, - event.position, - ); - _markViewNeedsPaint(); } super.handlePointerEvent(event); case TestBindingEventSource.device: @@ -1870,6 +1919,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { // The pointer events received with this source has a global position // (see [handlePointerEventForSource]). Transform it to the local // coordinate space used by the testing widgets. + final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId); final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView)); withPointerEventSource(TestBindingEventSource.device, () => super.handlePointerEvent(localEvent) @@ -1987,10 +2037,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } @override - ViewConfiguration createViewConfiguration() { + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + final FlutterView view = renderView.flutterView; + if (view == platformDispatcher.implicitView) { + return TestViewConfiguration.fromView( + size: _surfaceSize ?? _kDefaultTestViewportSize, + view: view, + ); + } + final double devicePixelRatio = view.devicePixelRatio; return TestViewConfiguration.fromView( - size: _surfaceSize ?? _kDefaultTestViewportSize, - view: platformDispatcher.implicitView!, + size: view.physicalSize / devicePixelRatio, + view: view, ); } diff --git a/packages/flutter_test/lib/src/buffer_matcher.dart b/packages/flutter_test/lib/src/buffer_matcher.dart deleted file mode 100644 index 6c621e6c27ac7..0000000000000 --- a/packages/flutter_test/lib/src/buffer_matcher.dart +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:matcher/expect.dart' show Description; -import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports -import 'package:test_api/hooks.dart' show TestFailure; - -import 'goldens.dart'; - -/// Matcher created by [bufferMatchesGoldenFile]. -class _BufferGoldenMatcher extends AsyncMatcher { - /// Creates an instance of [BufferGoldenMatcher]. Called by [bufferMatchesGoldenFile]. - const _BufferGoldenMatcher(this.key, this.version); - - /// The [key] to the golden image. - final Uri key; - - /// The [version] of the golden image. - final int? version; - - @override - Future<String?> matchAsync(dynamic item) async { - Uint8List buffer; - if (item is List<int>) { - buffer = Uint8List.fromList(item); - } else if (item is Future<List<int>>) { - buffer = Uint8List.fromList(await item); - } else { - throw AssertionError('Expected `List<int>` or `Future<List<int>>`, instead found: ${item.runtimeType}'); - } - final Uri testNameUri = goldenFileComparator.getTestUri(key, version); - if (autoUpdateGoldenFiles) { - await goldenFileComparator.update(testNameUri, buffer); - return null; - } - try { - final bool success = await goldenFileComparator.compare(buffer, testNameUri); - return success ? null : 'does not match'; - } on TestFailure catch (ex) { - return ex.message; - } - } - - @override - Description describe(Description description) { - final Uri testNameUri = goldenFileComparator.getTestUri(key, version); - return description.add('Byte buffer matches golden image "$testNameUri"'); - } -} - -/// Asserts that a [Future<List<int>>], or [List<int] matches the -/// golden image file identified by [key], with an optional [version] number. -/// -/// The [key] is the [String] representation of a URL. -/// -/// The [version] is a number that can be used to differentiate historical -/// golden files. This parameter is optional. -/// -/// {@tool snippet} -/// Sample invocations of [bufferMatchesGoldenFile]. -/// -/// ```dart -/// await expectLater( -/// const <int>[ /* bytes... */ ], -/// bufferMatchesGoldenFile('sample.png'), -/// ); -/// ``` -/// {@end-tool} -AsyncMatcher bufferMatchesGoldenFile(String key, {int? version}) { - return _BufferGoldenMatcher(Uri.parse(key), version); -} diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 4033ba0ad872c..9ab37a45edc71 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -9,11 +9,11 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'all_elements.dart'; import 'event_simulation.dart'; import 'finders.dart'; import 'test_async_utils.dart'; import 'test_pointer.dart'; +import 'tree_traversal.dart'; import 'window.dart'; /// The default drag touch slop used to break up a large drag into multiple @@ -24,6 +24,9 @@ const double kDragSlopDefault = 20.0; const String _defaultPlatform = kIsWeb ? 'web' : 'android'; +// Examples can assume: +// typedef MyWidget = Placeholder; + /// Class that programmatically interacts with the [Semantics] tree. /// /// Allows for testing of the [Semantics] tree, which is used by assistive @@ -36,7 +39,7 @@ class SemanticsController { /// Creates a [SemanticsController] that uses the given binding. Will be /// automatically created as part of instantiating a [WidgetController], but /// a custom implementation can be passed via the [WidgetController] constructor. - SemanticsController._(WidgetsBinding binding) : _binding = binding; + SemanticsController._(this._controller); static final int _scrollingActions = SemanticsAction.scrollUp.index | @@ -55,7 +58,7 @@ class SemanticsController { SemanticsFlag.isSlider.index | SemanticsFlag.isInMutuallyExclusiveGroup.index; - final WidgetsBinding _binding; + final WidgetController _controller; /// Attempts to find the [SemanticsNode] of first result from `finder`. /// @@ -71,9 +74,9 @@ class SemanticsController { /// /// Will throw a [StateError] if the finder returns more than one element or /// if no semantics are found or are not enabled. - SemanticsNode find(Finder finder) { + SemanticsNode find(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); - if (!_binding.semanticsEnabled) { + if (!_controller.binding.semanticsEnabled) { throw StateError('Semantics are not enabled.'); } final Iterable<Element> candidates = finder.evaluate(); @@ -109,6 +112,13 @@ class SemanticsController { /// tree. If `end` finds zero elements or more than one element, a /// [StateError] will be thrown. /// + /// If provided, the nodes for `end` and `start` must be part of the same + /// semantics tree, i.e. they must be part of the same view. + /// + /// If neither `start` or `end` is provided, `view` can be provided to specify + /// the semantics tree to traverse. If `view` is left unspecified, + /// [WidgetTester.view] is traversed by default. + /// /// Since the order is simulated, edge cases that differ between platforms /// (such as how the last visible item in a scrollable list is handled) may be /// inconsistent with platform behavior, but are expected to be sufficient for @@ -116,13 +126,13 @@ class SemanticsController { /// /// ## Sample Code /// - /// ``` + /// ```dart /// testWidgets('MyWidget', (WidgetTester tester) async { - /// await tester.pumpWidget(MyWidget()); + /// await tester.pumpWidget(const MyWidget()); /// /// expect( /// tester.semantics.simulatedAccessibilityTraversal(), - /// containsAllInOrder([ + /// containsAllInOrder(<Matcher>[ /// containsSemantics(label: 'My Widget'), /// containsSemantics(label: 'is awesome!', isChecked: true), /// ]), @@ -139,10 +149,47 @@ class SemanticsController { /// parts of the traversal. /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly /// match the order of the traversal. - Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) { + Iterable<SemanticsNode> simulatedAccessibilityTraversal({FinderBase<Element>? start, FinderBase<Element>? end, FlutterView? view}) { TestAsyncUtils.guardSync(); + FlutterView? startView; + FlutterView? endView; + if (start != null) { + startView = _controller.viewOf(start); + if (view != null && startView != view) { + throw StateError( + 'The start node is not part of the provided view.\n' + 'Finder: ${start.toString(describeSelf: true)}\n' + 'View of start node: $startView\n' + 'Specified view: $view' + ); + } + } + if (end != null) { + endView = _controller.viewOf(end); + if (view != null && endView != view) { + throw StateError( + 'The end node is not part of the provided view.\n' + 'Finder: ${end.toString(describeSelf: true)}\n' + 'View of end node: $endView\n' + 'Specified view: $view' + ); + } + } + if (endView != null && startView != null && endView != startView) { + throw StateError( + 'The start and end node are in different views.\n' + 'Start finder: ${start!.toString(describeSelf: true)}\n' + 'End finder: ${end!.toString(describeSelf: true)}\n' + 'View of start node: $startView\n' + 'View of end node: $endView' + ); + } + + final FlutterView actualView = view ?? startView ?? endView ?? _controller.view; + final RenderView renderView = _controller.binding.renderViews.firstWhere((RenderView r) => r.flutterView == actualView); + final List<SemanticsNode> traversal = <SemanticsNode>[]; - _traverse(_binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, traversal); + _traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal); int startIndex = 0; int endIndex = traversal.length - 1; @@ -153,7 +200,7 @@ class SemanticsController { if (startIndex == -1) { throw StateError( 'The expected starting node was not found.\n' - 'Finder: ${start.description}\n\n' + 'Finder: ${start.toString(describeSelf: true)}\n\n' 'Expected Start Node: $startNode\n\n' 'Traversal: [\n ${traversal.join('\n ')}\n]'); } @@ -165,7 +212,7 @@ class SemanticsController { if (endIndex == -1) { throw StateError( 'The expected ending node was not found.\n' - 'Finder: ${end.description}\n\n' + 'Finder: ${end.toString(describeSelf: true)}\n\n' 'Expected End Node: $endNode\n\n' 'Traversal: [\n ${traversal.join('\n ')}\n]'); } @@ -195,23 +242,29 @@ class SemanticsController { /// * [flutter/engine/AccessibilityBridge.java#SemanticsNode.isFocusable()](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L2641) /// * [flutter/engine/SemanticsObject.mm#SemanticsObject.isAccessibilityElement](https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm#L449) bool _isImportantForAccessibility(SemanticsNode node) { + if (node.isMergedIntoParent) { + // If this node is merged, all its information are present on an ancestor + // node. + return false; + } + final SemanticsData data = node.getSemanticsData(); // If the node scopes a route, it doesn't matter what other flags/actions it // has, it is _not_ important for accessibility, so we short circuit. - if (node.hasFlag(SemanticsFlag.scopesRoute)) { + if (data.hasFlag(SemanticsFlag.scopesRoute)) { return false; } - final bool hasNonScrollingAction = node.getSemanticsData().actions & ~_scrollingActions != 0; + final bool hasNonScrollingAction = data.actions & ~_scrollingActions != 0; if (hasNonScrollingAction) { return true; } - final bool hasImportantFlag = node.getSemanticsData().flags & _importantFlagsForAccessibility != 0; + final bool hasImportantFlag = data.flags & _importantFlagsForAccessibility != 0; if (hasImportantFlag) { return true; } - final bool hasContent = node.label.isNotEmpty || node.value.isNotEmpty || node.hint.isNotEmpty; + final bool hasContent = data.label.isNotEmpty || data.value.isNotEmpty || data.hint.isNotEmpty; if (hasContent) { return true; } @@ -229,8 +282,7 @@ class SemanticsController { /// Concrete subclasses must implement the [pump] method. abstract class WidgetController { /// Creates a widget controller that uses the given binding. - WidgetController(this.binding) - : _semantics = SemanticsController._(binding); + WidgetController(this.binding); /// A reference to the current instance of the binding. final WidgetsBinding binding; @@ -280,7 +332,7 @@ abstract class WidgetController { return _semantics; } - final SemanticsController _semantics; + late final SemanticsController _semantics = SemanticsController._(this); // FINDER API @@ -296,19 +348,21 @@ abstract class WidgetController { /// /// * [view] which returns the [TestFlutterView] used when only a single /// view is being used. - TestFlutterView viewOf(Finder finder) { - final View view = firstWidget<View>( + TestFlutterView viewOf(FinderBase<Element> finder) { + return _viewOf(finder) as TestFlutterView; + } + + FlutterView _viewOf(FinderBase<Element> finder) { + return firstWidget<View>( find.ancestor( of: finder, matching: find.byType(View), - ) - ); - - return view.view as TestFlutterView; + ), + ).view; } /// Checks if `finder` exists in the tree. - bool any(Finder finder) { + bool any(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().isNotEmpty; } @@ -329,7 +383,7 @@ abstract class WidgetController { /// /// * Use [firstWidget] if you expect to match several widgets but only want the first. /// * Use [widgetList] if you expect to match several widgets and want all of them. - T widget<T extends Widget>(Finder finder) { + T widget<T extends Widget>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().single.widget as T; } @@ -340,7 +394,7 @@ abstract class WidgetController { /// Throws a [StateError] if `finder` is empty. /// /// * Use [widget] if you only expect to match one widget. - T firstWidget<T extends Widget>(Finder finder) { + T firstWidget<T extends Widget>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().first.widget as T; } @@ -349,7 +403,7 @@ abstract class WidgetController { /// /// * Use [widget] if you only expect to match one widget. /// * Use [firstWidget] if you expect to match several but only want the first. - Iterable<T> widgetList<T extends Widget>(Finder finder) { + Iterable<T> widgetList<T extends Widget>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().map<T>((Element element) { final T result = element.widget as T; @@ -360,7 +414,7 @@ abstract class WidgetController { /// Find all layers that are children of the provided [finder]. /// /// The [finder] must match exactly one element. - Iterable<Layer> layerListOf(Finder finder) { + Iterable<Layer> layerListOf(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); final Element element = finder.evaluate().single; final RenderObject object = element.renderObject!; @@ -389,7 +443,7 @@ abstract class WidgetController { /// /// * Use [firstElement] if you expect to match several elements but only want the first. /// * Use [elementList] if you expect to match several elements and want all of them. - T element<T extends Element>(Finder finder) { + T element<T extends Element>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().single as T; } @@ -400,7 +454,7 @@ abstract class WidgetController { /// Throws a [StateError] if `finder` is empty. /// /// * Use [element] if you only expect to match one element. - T firstElement<T extends Element>(Finder finder) { + T firstElement<T extends Element>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().first as T; } @@ -409,7 +463,7 @@ abstract class WidgetController { /// /// * Use [element] if you only expect to match one element. /// * Use [firstElement] if you expect to match several but only want the first. - Iterable<T> elementList<T extends Element>(Finder finder) { + Iterable<T> elementList<T extends Element>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().cast<T>(); } @@ -431,7 +485,7 @@ abstract class WidgetController { /// /// * Use [firstState] if you expect to match several states but only want the first. /// * Use [stateList] if you expect to match several states and want all of them. - T state<T extends State>(Finder finder) { + T state<T extends State>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return _stateOf<T>(finder.evaluate().single, finder); } @@ -443,7 +497,7 @@ abstract class WidgetController { /// matching widget has no state. /// /// * Use [state] if you only expect to match one state. - T firstState<T extends State>(Finder finder) { + T firstState<T extends State>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return _stateOf<T>(finder.evaluate().first, finder); } @@ -455,17 +509,17 @@ abstract class WidgetController { /// /// * Use [state] if you only expect to match one state. /// * Use [firstState] if you expect to match several but only want the first. - Iterable<T> stateList<T extends State>(Finder finder) { + Iterable<T> stateList<T extends State>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder)); } - T _stateOf<T extends State>(Element element, Finder finder) { + T _stateOf<T extends State>(Element element, FinderBase<Element> finder) { TestAsyncUtils.guardSync(); if (element is StatefulElement) { return element.state as T; } - throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.'); + throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.describeMatch(Plurality.many)}, is not a StatefulWidget.'); } /// Render objects of all the widgets currently in the widget tree @@ -487,7 +541,7 @@ abstract class WidgetController { /// /// * Use [firstRenderObject] if you expect to match several render objects but only want the first. /// * Use [renderObjectList] if you expect to match several render objects and want all of them. - T renderObject<T extends RenderObject>(Finder finder) { + T renderObject<T extends RenderObject>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().single.renderObject! as T; } @@ -498,7 +552,7 @@ abstract class WidgetController { /// Throws a [StateError] if `finder` is empty. /// /// * Use [renderObject] if you only expect to match one render object. - T firstRenderObject<T extends RenderObject>(Finder finder) { + T firstRenderObject<T extends RenderObject>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().first.renderObject! as T; } @@ -507,7 +561,7 @@ abstract class WidgetController { /// /// * Use [renderObject] if you only expect to match one render object. /// * Use [firstRenderObject] if you expect to match several but only want the first. - Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) { + Iterable<T> renderObjectList<T extends RenderObject>(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); return finder.evaluate().map<T>((Element element) { final T result = element.renderObject! as T; @@ -516,7 +570,12 @@ abstract class WidgetController { } /// Returns a list of all the [Layer] objects in the rendering. - List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList(); + List<Layer> get layers { + return <Layer>[ + for (final RenderView renderView in binding.renderViews) + ..._walkLayers(renderView.debugLayer!) + ]; + } Iterable<Layer> _walkLayers(Layer layer) sync* { TestAsyncUtils.guardSync(); yield layer; @@ -550,7 +609,7 @@ abstract class WidgetController { /// For example, a test that verifies that tapping a disabled button does not /// trigger the button would set `warnIfMissed` to false, because the button /// would ignore the tap. - Future<void> tap(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { + Future<void> tap(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { return tapAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'tap'), pointer: pointer, buttons: buttons); } @@ -575,7 +634,7 @@ abstract class WidgetController { /// * [tap], which presses and releases a pointer at the given location. /// * [longPress], which presses and releases a pointer with a gap in /// between long enough to trigger the long-press gesture. - Future<TestGesture> press(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { + Future<TestGesture> press(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { return TestAsyncUtils.guard<TestGesture>(() { return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons); }); @@ -593,7 +652,7 @@ abstract class WidgetController { /// later verify that long-pressing the same location (using the same finder) /// has no effect (since the widget is now obscured), setting `warnIfMissed` /// to false on that second call. - Future<void> longPress(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { + Future<void> longPress(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) { return longPressAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'longPress'), pointer: pointer, buttons: buttons); } @@ -654,7 +713,7 @@ abstract class WidgetController { /// A fling is essentially a drag that ends at a particular speed. If you /// just want to drag and end without a fling, use [drag]. Future<void> fling( - Finder finder, + FinderBase<Element> finder, Offset offset, double speed, { int? pointer, @@ -734,7 +793,7 @@ abstract class WidgetController { /// A fling is essentially a drag that ends at a particular speed. If you /// just want to drag and end without a fling, use [drag]. Future<void> trackpadFling( - Finder finder, + FinderBase<Element> finder, Offset offset, double speed, { int? pointer, @@ -899,7 +958,7 @@ abstract class WidgetController { /// should be left to their default values. /// {@endtemplate} Future<void> drag( - Finder finder, + FinderBase<Element> finder, Offset offset, { int? pointer, int buttons = kPrimaryButton, @@ -1032,7 +1091,7 @@ abstract class WidgetController { /// more accurate time control. /// {@endtemplate} Future<void> timedDrag( - Finder finder, + FinderBase<Element> finder, Offset offset, Duration duration, { int? pointer, @@ -1190,10 +1249,10 @@ abstract class WidgetController { } /// Forwards the given location to the binding's hitTest logic. - HitTestResult hitTestOnBinding(Offset location) { + HitTestResult hitTestOnBinding(Offset location, { int? viewId }) { + viewId ??= view.viewId; final HitTestResult result = HitTestResult(); - // TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 - binding.hitTest(result, location); // ignore: deprecated_member_use + binding.hitTestInView(result, location, viewId); return result; } @@ -1229,14 +1288,14 @@ abstract class WidgetController { /// this method is being called from another that is forwarding its own /// `warnIfMissed` parameter (see e.g. the implementation of [tap]). /// {@endtemplate} - Offset getCenter(Finder finder, { bool warnIfMissed = false, String callee = 'getCenter' }) { + Offset getCenter(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getCenter' }) { return _getElementPoint(finder, (Size size) => size.center(Offset.zero), warnIfMissed: warnIfMissed, callee: callee); } /// Returns the point at the top left of the given widget. /// /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} - Offset getTopLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) { + Offset getTopLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) { return _getElementPoint(finder, (Size size) => Offset.zero, warnIfMissed: warnIfMissed, callee: callee); } @@ -1244,7 +1303,7 @@ abstract class WidgetController { /// point is not inside the object's hit test area. /// /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} - Offset getTopRight(Finder finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) { + Offset getTopRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) { return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee); } @@ -1252,7 +1311,7 @@ abstract class WidgetController { /// point is not inside the object's hit test area. /// /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} - Offset getBottomLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) { + Offset getBottomLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) { return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero), warnIfMissed: warnIfMissed, callee: callee); } @@ -1260,7 +1319,7 @@ abstract class WidgetController { /// point is not inside the object's hit test area. /// /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} - Offset getBottomRight(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) { + Offset getBottomRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) { return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee); } @@ -1287,7 +1346,7 @@ abstract class WidgetController { /// in the documentation for the [flutter_test] library. static bool hitTestWarningShouldBeFatal = false; - Offset _getElementPoint(Finder finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) { + Offset _getElementPoint(FinderBase<Element> finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) { TestAsyncUtils.guardSync(); final Iterable<Element> elements = finder.evaluate(); if (elements.isEmpty) { @@ -1313,9 +1372,9 @@ abstract class WidgetController { final RenderBox box = element.renderObject! as RenderBox; final Offset location = box.localToGlobal(sizeToPoint(box.size)); if (warnIfMissed) { + final FlutterView view = _viewOf(finder); final HitTestResult result = HitTestResult(); - // TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 - binding.hitTest(result, location); // ignore: deprecated_member_use + binding.hitTestInView(result, location, view.viewId); bool found = false; for (final HitTestEntry entry in result.path) { if (entry.target == box) { @@ -1324,15 +1383,16 @@ abstract class WidgetController { } } if (!found) { + final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view); bool outOfBounds = false; - outOfBounds = !(Offset.zero & binding.renderView.size).contains(location); + outOfBounds = !(Offset.zero & renderView.size).contains(location); if (hitTestWarningShouldBeFatal) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary('Finder specifies a widget that would not receive pointer events.'), ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'), ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'), if (outOfBounds) - ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.'), + ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.'), box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine), ErrorDescription('The hit test result at that offset is: $result'), ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'), @@ -1343,7 +1403,7 @@ abstract class WidgetController { '\n' 'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n' 'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n' - '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.\n" : ""}' + '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.\n" : ""}' 'The finder corresponds to this RenderBox: $box\n' 'The hit test result at that offset is: $result\n' '${StackTrace.current}' @@ -1357,7 +1417,7 @@ abstract class WidgetController { /// Returns the size of the given widget. This is only valid once /// the widget's render object has been laid out at least once. - Size getSize(Finder finder) { + Size getSize(FinderBase<Element> finder) { TestAsyncUtils.guardSync(); final Element element = finder.evaluate().single; final RenderBox box = element.renderObject! as RenderBox; @@ -1372,7 +1432,7 @@ abstract class WidgetController { /// Specify `platform` as one of the platforms allowed in /// [platform.Platform.operatingSystem] to make the event appear to be from /// that type of system. Defaults to "web" on web, and "android" everywhere - /// else. Must not be null. + /// else. /// /// Specify the `physicalKey` for the event to override what is included in /// the simulated event. If not specified, it uses a default from the US @@ -1417,7 +1477,7 @@ abstract class WidgetController { /// Specify `platform` as one of the platforms allowed in /// [platform.Platform.operatingSystem] to make the event appear to be from /// that type of system. Defaults to "web" on web, and "android" everywhere - /// else. Must not be null. + /// else. /// /// Specify the `physicalKey` for the event to override what is included in /// the simulated event. If not specified, it uses a default from the US @@ -1525,7 +1585,7 @@ abstract class WidgetController { /// Returns the rect of the given widget. This is only valid once /// the widget's render object has been laid out at least once. - Rect getRect(Finder finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder)); + Rect getRect(FinderBase<Element> finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder)); /// Attempts to find the [SemanticsNode] of first result from `finder`. /// @@ -1542,7 +1602,7 @@ abstract class WidgetController { /// Will throw a [StateError] if the finder returns more than one element or /// if no semantics are found or are not enabled. // TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670. - SemanticsNode getSemantics(Finder finder) => semantics.find(finder); + SemanticsNode getSemantics(FinderBase<Element> finder) => semantics.find(finder); /// Enable semantics in a test by creating a [SemanticsHandle]. /// @@ -1566,7 +1626,7 @@ abstract class WidgetController { /// /// * [Scrollable.ensureVisible], which is the production API used to /// implement this method. - Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder)); + Future<void> ensureVisible(FinderBase<Element> finder) => Scrollable.ensureVisible(element(finder)); /// Repeatedly scrolls a [Scrollable] by `delta` in the /// [Scrollable.axisDirection] direction until a widget matching `finder` is @@ -1591,9 +1651,9 @@ abstract class WidgetController { /// /// * [dragUntilVisible], which implements the body of this method. Future<void> scrollUntilVisible( - Finder finder, + FinderBase<Element> finder, double delta, { - Finder? scrollable, + FinderBase<Element>? scrollable, int maxScrolls = 50, Duration duration = const Duration(milliseconds: 50), } @@ -1634,8 +1694,8 @@ abstract class WidgetController { /// * [scrollUntilVisible], which wraps this method with an API that is more /// convenient when dealing with a [Scrollable]. Future<void> dragUntilVisible( - Finder finder, - Finder view, + FinderBase<Element> finder, + FinderBase<Element> view, Offset moveStep, { int maxIteration = 50, Duration duration = const Duration(milliseconds: 50), diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 1e881489cb504..4cff6b0d6e947 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart' show Tooltip; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'all_elements.dart'; +import 'binding.dart'; +import 'tree_traversal.dart'; /// Signature for [CommonFinders.byWidgetPredicate]. typedef WidgetPredicate = bool Function(Widget widget); @@ -14,15 +17,31 @@ typedef WidgetPredicate = bool Function(Widget widget); /// Signature for [CommonFinders.byElementPredicate]. typedef ElementPredicate = bool Function(Element element); -/// Some frequently used widget [Finder]s. +/// Signature for [CommonSemanticsFinders.byPredicate]. +typedef SemanticsNodePredicate = bool Function(SemanticsNode node); + +/// Signature for [FinderBase.describeMatch]. +typedef DescribeMatchCallback = String Function(Plurality plurality); + +/// Some frequently used [Finder]s and [SemanticsFinder]s. const CommonFinders find = CommonFinders._(); -/// Provides lightweight syntax for getting frequently used widget [Finder]s. +// Examples can assume: +// typedef Button = Placeholder; +// late WidgetTester tester; +// late String filePath; +// late Key backKey; + +/// Provides lightweight syntax for getting frequently used [Finder]s and +/// [SemanticsFinder]s through [semantics]. /// /// This class is instantiated once, as [find]. class CommonFinders { const CommonFinders._(); + /// Some frequently used semantics finders. + CommonSemanticsFinders get semantics => const CommonSemanticsFinders._(); + /// Finds [Text], [EditableText], and optionally [RichText] widgets /// containing string equal to the `text` argument. /// @@ -58,7 +77,7 @@ class CommonFinders { bool findRichText = false, bool skipOffstage = true, }) { - return _TextFinder( + return _TextWidgetFinder( text, findRichText: findRichText, skipOffstage: skipOffstage, @@ -102,7 +121,7 @@ class CommonFinders { bool findRichText = false, bool skipOffstage = true, }) { - return _TextContainingFinder( + return _TextContainingWidgetFinder( pattern, findRichText: findRichText, skipOffstage: skipOffstage @@ -115,12 +134,12 @@ class CommonFinders { /// ## Sample code /// /// ```dart - /// // Suppose you have a button with text 'Update' in it: - /// Button( + /// // Suppose there is a button with text 'Update' in it: + /// const Button( /// child: Text('Update') - /// ) + /// ); /// - /// // You can find and tap on it like this: + /// // It can be found and tapped like this: /// tester.tap(find.widgetWithText(Button, 'Update')); /// ``` /// @@ -144,9 +163,9 @@ class CommonFinders { /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. - Finder image(ImageProvider image, { bool skipOffstage = true }) => _WidgetImageFinder(image, skipOffstage: skipOffstage); + Finder image(ImageProvider image, { bool skipOffstage = true }) => _ImageWidgetFinder(image, skipOffstage: skipOffstage); - /// Finds widgets by searching for one with a particular [Key]. + /// Finds widgets by searching for one with the given `key`. /// /// ## Sample code /// @@ -156,7 +175,7 @@ class CommonFinders { /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. - Finder byKey(Key key, { bool skipOffstage = true }) => _KeyFinder(key, skipOffstage: skipOffstage); + Finder byKey(Key key, { bool skipOffstage = true }) => _KeyWidgetFinder(key, skipOffstage: skipOffstage); /// Finds widgets by searching for widgets implementing a particular type. /// @@ -174,13 +193,13 @@ class CommonFinders { /// /// See also: /// * [byType], which does not do subtype tests. - Finder bySubtype<T extends Widget>({ bool skipOffstage = true }) => _WidgetSubtypeFinder<T>(skipOffstage: skipOffstage); + Finder bySubtype<T extends Widget>({ bool skipOffstage = true }) => _SubtypeWidgetFinder<T>(skipOffstage: skipOffstage); /// Finds widgets by searching for widgets with a particular type. /// /// This does not do subclass tests, so for example - /// `byType(StatefulWidget)` will never find anything since that's - /// an abstract class. + /// `byType(StatefulWidget)` will never find anything since [StatefulWidget] + /// is an abstract class. /// /// The `type` argument must be a subclass of [Widget]. /// @@ -195,7 +214,7 @@ class CommonFinders { /// /// See also: /// * [bySubtype], which allows subtype tests. - Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage); + Finder byType(Type type, { bool skipOffstage = true }) => _TypeWidgetFinder(type, skipOffstage: skipOffstage); /// Finds [Icon] widgets containing icon data equal to the `icon` /// argument. @@ -208,7 +227,7 @@ class CommonFinders { /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. - Finder byIcon(IconData icon, { bool skipOffstage = true }) => _WidgetIconFinder(icon, skipOffstage: skipOffstage); + Finder byIcon(IconData icon, { bool skipOffstage = true }) => _IconWidgetFinder(icon, skipOffstage: skipOffstage); /// Looks for widgets that contain an [Icon] descendant displaying [IconData] /// `icon` in it. @@ -216,12 +235,12 @@ class CommonFinders { /// ## Sample code /// /// ```dart - /// // Suppose you have a button with icon 'arrow_forward' in it: - /// Button( + /// // Suppose there is a button with icon 'arrow_forward' in it: + /// const Button( /// child: Icon(Icons.arrow_forward) - /// ) + /// ); /// - /// // You can find and tap on it like this: + /// // It can be found and tapped like this: /// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward)); /// ``` /// @@ -234,19 +253,19 @@ class CommonFinders { ); } - /// Looks for widgets that contain an [Image] descendant displaying [ImageProvider] - /// `image` in it. + /// Looks for widgets that contain an [Image] descendant displaying + /// [ImageProvider] `image` in it. /// /// ## Sample code /// /// ```dart - /// // Suppose you have a button with image in it: + /// // Suppose there is a button with an image in it: /// Button( - /// child: Image.file(filePath) - /// ) + /// child: Image.file(File(filePath)) + /// ); /// - /// // You can find and tap on it like this: - /// tester.tap(find.widgetWithImage(Button, FileImage(filePath))); + /// // It can be found and tapped like this: + /// tester.tap(find.widgetWithImage(Button, FileImage(File(filePath)))); /// ``` /// /// If the `skipOffstage` argument is true (the default), then this skips @@ -262,7 +281,7 @@ class CommonFinders { /// /// This does not do subclass tests, so for example /// `byElementType(VirtualViewportElement)` will never find anything - /// since that's an abstract class. + /// since [RenderObjectElement] is an abstract class. /// /// The `type` argument must be a subclass of [Element]. /// @@ -274,39 +293,39 @@ class CommonFinders { /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. - Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeFinder(type, skipOffstage: skipOffstage); + Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeWidgetFinder(type, skipOffstage: skipOffstage); - /// Finds widgets whose current widget is the instance given by the + /// Finds widgets whose current widget is the instance given by the `widget` /// argument. /// /// ## Sample code /// /// ```dart - /// // Suppose you have a button created like this: - /// Widget myButton = Button( + /// // Suppose there is a button created like this: + /// Widget myButton = const Button( /// child: Text('Update') /// ); /// - /// // You can find and tap on it like this: + /// // It can be found and tapped like this: /// tester.tap(find.byWidget(myButton)); /// ``` /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. - Finder byWidget(Widget widget, { bool skipOffstage = true }) => _WidgetFinder(widget, skipOffstage: skipOffstage); + Finder byWidget(Widget widget, { bool skipOffstage = true }) => _ExactWidgetFinder(widget, skipOffstage: skipOffstage); - /// Finds widgets using a widget [predicate]. + /// Finds widgets using a widget `predicate`. /// /// ## Sample code /// /// ```dart /// expect(find.byWidgetPredicate( /// (Widget widget) => widget is Tooltip && widget.message == 'Back', - /// description: 'widget with tooltip "Back"', + /// description: 'with tooltip "Back"', /// ), findsOneWidget); /// ``` /// - /// If [description] is provided, then this uses it as the description of the + /// If `description` is provided, then this uses it as the description of the /// [Finder] and appears, for example, in the error message when the finder /// fails to locate the desired widget. Otherwise, the description prints the /// signature of the predicate function. @@ -314,10 +333,10 @@ class CommonFinders { /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. Finder byWidgetPredicate(WidgetPredicate predicate, { String? description, bool skipOffstage = true }) { - return _WidgetPredicateFinder(predicate, description: description, skipOffstage: skipOffstage); + return _WidgetPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage); } - /// Finds Tooltip widgets with the given message. + /// Finds [Tooltip] widgets with the given `message`. /// /// ## Sample code /// @@ -334,13 +353,13 @@ class CommonFinders { ); } - /// Finds widgets using an element [predicate]. + /// Finds widgets using an element `predicate`. /// /// ## Sample code /// /// ```dart /// expect(find.byElementPredicate( - /// // finds elements of type SingleChildRenderObjectElement, including + /// // Finds elements of type SingleChildRenderObjectElement, including /// // those that are actually subclasses of that type. /// // (contrast with byElementType, which only returns exact matches) /// (Element element) => element is SingleChildRenderObjectElement, @@ -348,7 +367,7 @@ class CommonFinders { /// ), findsOneWidget); /// ``` /// - /// If [description] is provided, then this uses it as the description of the + /// If `description` is provided, then this uses it as the description of the /// [Finder] and appears, for example, in the error message when the finder /// fails to locate the desired widget. Otherwise, the description prints the /// signature of the predicate function. @@ -356,36 +375,37 @@ class CommonFinders { /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. Finder byElementPredicate(ElementPredicate predicate, { String? description, bool skipOffstage = true }) { - return _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage); + return _ElementPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage); } - /// Finds widgets that are descendants of the [of] parameter and that match - /// the [matching] parameter. + /// Finds widgets that are descendants of the `of` parameter and that match + /// the `matching` parameter. /// /// ## Sample code /// /// ```dart /// expect(find.descendant( - /// of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1') + /// of: find.widgetWithText(Row, 'label_1'), + /// matching: find.text('value_1'), /// ), findsOneWidget); /// ``` /// - /// If the [matchRoot] argument is true then the widget(s) specified by [of] + /// If the `matchRoot` argument is true then the widget(s) specified by `of` /// will be matched along with the descendants. /// - /// If the [skipOffstage] argument is true (the default), then nodes that are + /// If the `skipOffstage` argument is true (the default), then nodes that are /// [Offstage] or that are from inactive [Route]s are skipped. Finder descendant({ - required Finder of, - required Finder matching, + required FinderBase<Element> of, + required FinderBase<Element> matching, bool matchRoot = false, bool skipOffstage = true, }) { - return _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage); + return _DescendantWidgetFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage); } - /// Finds widgets that are ancestors of the [of] parameter and that match - /// the [matching] parameter. + /// Finds widgets that are ancestors of the `of` parameter and that match + /// the `matching` parameter. /// /// ## Sample code /// @@ -396,21 +416,21 @@ class CommonFinders { /// tester.widget<Opacity>( /// find.ancestor( /// of: find.text('faded'), - /// matching: find.byType('Opacity'), + /// matching: find.byType(Opacity), /// ) /// ).opacity, /// 0.5 /// ); /// ``` /// - /// If the [matchRoot] argument is true then the widget(s) specified by [of] + /// If the `matchRoot` argument is true then the widget(s) specified by `of` /// will be matched along with the ancestors. Finder ancestor({ - required Finder of, - required Finder matching, + required FinderBase<Element> of, + required FinderBase<Element> matching, bool matchRoot = false, }) { - return _AncestorFinder(of, matching, matchRoot: matchRoot); + return _AncestorWidgetFinder(of, matching, matchLeaves: matchRoot); } /// Finds [Semantics] widgets matching the given `label`, either by @@ -460,58 +480,441 @@ class CommonFinders { } } -/// Searches a widget tree and returns nodes that match a particular -/// pattern. -abstract class Finder { - /// Initializes a Finder. Used by subclasses to initialize the [skipOffstage] - /// property. - Finder({ this.skipOffstage = true }); - /// Describes what the finder is looking for. The description should be - /// a brief English noun phrase describing the finder's pattern. - String get description; +/// Provides lightweight syntax for getting frequently used semantics finders. +/// +/// This class is instantiated once, as [CommonFinders.semantics], under [find]. +class CommonSemanticsFinders { + const CommonSemanticsFinders._(); - /// Returns all the elements in the given list that match this - /// finder's pattern. + /// Finds an ancestor of `of` that matches `matching`. /// - /// When implementing your own Finders that inherit directly from - /// [Finder], this is the main method to override. If your finder - /// can efficiently be described just in terms of a predicate - /// function, consider extending [MatchFinder] instead. - Iterable<Element> apply(Iterable<Element> candidates); + /// If `matchRoot` is true, then the results of `of` are included in the + /// search and results. + FinderBase<SemanticsNode> ancestor({ + required FinderBase<SemanticsNode> of, + required FinderBase<SemanticsNode> matching, + bool matchRoot = false, + }) { + return _AncestorSemanticsFinder(of, matching, matchRoot); + } - /// Whether this finder skips nodes that are offstage. + /// Finds a descendant of `of` that matches `matching`. /// - /// If this is true, then the elements are walked using - /// [Element.debugVisitOnstageChildren]. This skips offstage children of - /// [Offstage] widgets, as well as children of inactive [Route]s. - final bool skipOffstage; + /// If `matchRoot` is true, then the results of `of` are included in the + /// search and results. + FinderBase<SemanticsNode> descendant({ + required FinderBase<SemanticsNode> of, + required FinderBase<SemanticsNode> matching, + bool matchRoot = false, + }) { + return _DescendantSemanticsFinder(of, matching, matchRoot: matchRoot); + } + + /// Finds any [SemanticsNode]s matching the given `predicate`. + /// + /// If `describeMatch` is provided, it will be used to describe the + /// [FinderBase] and [FinderResult]s. + /// {@macro flutter_test.finders.FinderBase.describeMatch} + /// + /// {@template flutter_test.finders.CommonSemanticsFinders.viewParameter} + /// The `view` provided will be used to determine the semantics tree where + /// the search will be evaluated. If not provided, the search will be + /// evaluated against the semantics tree of [WidgetTester.view]. + /// {@endtemplate} + SemanticsFinder byPredicate( + SemanticsNodePredicate predicate, { + DescribeMatchCallback? describeMatch, + FlutterView? view, + }) { + return _PredicateSemanticsFinder( + predicate, + describeMatch, + _rootFromView(view), + ); + } + + /// Finds any [SemanticsNode]s that has a [SemanticsNode.label] that matches + /// the given `label`. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byLabel(Pattern label, {FlutterView? view}) { + return byPredicate( + (SemanticsNode node) => _matchesPattern(node.label, label), + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with label "$label"', + view: view, + ); + } - /// Returns all the [Element]s that will be considered by this finder. + /// Finds any [SemanticsNode]s that has a [SemanticsNode.value] that matches + /// the given `value`. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byValue(Pattern value, {FlutterView? view}) { + return byPredicate( + (SemanticsNode node) => _matchesPattern(node.value, value), + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with value "$value"', + view: view, + ); + } + + /// Finds any [SemanticsNode]s that has a [SemanticsNode.hint] that matches + /// the given `hint`. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byHint(Pattern hint, {FlutterView? view}) { + return byPredicate( + (SemanticsNode node) => _matchesPattern(node.hint, hint), + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with hint "$hint"', + view: view, + ); + } + + /// Finds any [SemanticsNode]s that has the given [SemanticsAction]. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byAction(SemanticsAction action, {FlutterView? view}) { + return byPredicate( + (SemanticsNode node) => node.getSemanticsData().hasAction(action), + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with action "$action"', + view: view, + ); + } + + /// Finds any [SemanticsNode]s that has at least one of the given + /// [SemanticsAction]s. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byAnyAction(List<SemanticsAction> actions, {FlutterView? view}) { + final int actionsInt = actions.fold(0, (int value, SemanticsAction action) => value | action.index); + return byPredicate( + (SemanticsNode node) => node.getSemanticsData().actions & actionsInt != 0, + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with any of the following actions: $actions', + view: view, + ); + } + + /// Finds any [SemanticsNode]s that has the given [SemanticsFlag]. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byFlag(SemanticsFlag flag, {FlutterView? view}) { + return byPredicate( + (SemanticsNode node) => node.hasFlag(flag), + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with flag "$flag"', + view: view, + ); + } + + /// Finds any [SemanticsNode]s that has at least one of the given + /// [SemanticsFlag]s. + /// + /// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter} + SemanticsFinder byAnyFlag(List<SemanticsFlag> flags, {FlutterView? view}) { + final int flagsInt = flags.fold(0, (int value, SemanticsFlag flag) => value | flag.index); + return byPredicate( + (SemanticsNode node) => node.getSemanticsData().flags & flagsInt != 0, + describeMatch: (Plurality plurality) => '${switch (plurality) { + Plurality.one => 'SemanticsNode', + Plurality.zero || Plurality.many => 'SemanticsNodes', + }} with any of the following flags: $flags', + view: view, + ); + } + + bool _matchesPattern(String target, Pattern pattern) { + if (pattern is RegExp) { + return pattern.hasMatch(target); + } else { + return pattern == target; + } + } + + SemanticsNode _rootFromView(FlutterView? view) { + view ??= TestWidgetsFlutterBinding.instance.platformDispatcher.implicitView; + assert(view != null, 'The given view was not available. Ensure WidgetTester.view is available or pass in a specific view using WidgetTester.viewOf.'); + final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews + .firstWhere((RenderView r) => r.flutterView == view); + + return renderView.owner!.semanticsOwner!.rootSemanticsNode!; + } +} + +/// Describes how a string of text should be pluralized. +enum Plurality { + /// Text should be pluralized to describe zero items. + zero, + /// Text should be pluralized to describe a single item. + one, + /// Text should be pluralized to describe more than one item. + many; + + static Plurality _fromNum(num source) { + assert(source >= 0, 'A Plurality can only be created with a positive number.'); + return switch (source) { + 0 => Plurality.zero, + 1 => Plurality.one, + _ => Plurality.many, + }; + } +} + +/// Encapsulates the logic for searching a list of candidates and filtering the +/// candidates to only those that meet the requirements defined by the finder. +/// +/// Implementations will need to implement [allCandidates] to define the total +/// possible search space and [findInCandidates] to define the requirements of +/// the finder. +/// +/// This library contains [Finder] and [SemanticsFinder] for searching +/// Flutter's element and semantics trees respectively. +/// +/// If the search can be represented as a predicate, then consider using +/// [MatchFinderMixin] along with the [Finder] or [SemanticsFinder] base class. +/// +/// If the search further filters the results from another finder, consider using +/// [ChainedFinderMixin] along with the [Finder] or [SemanticsFinder] base class. +abstract class FinderBase<CandidateType> { + bool _cached = false; + + /// The results of the latest [evaluate] or [tryEvaluate] call. + /// + /// Unlike [evaluate] and [tryEvaluate], [found] will not re-execute the + /// search for this finder. Either [evaluate] or [tryEvaluate] must be called + /// before accessing [found]. + FinderResult<CandidateType> get found { + assert( + _found != null, + 'No results have been found yet. ' + 'Either `evaluate` or `tryEvaluate` must be called before accessing `found`', + ); + return _found!; + } + FinderResult<CandidateType>? _found; + + /// Whether or not this finder has any results in [found]. + bool get hasFound => _found != null; + + /// Describes zero, one, or more candidates that match the requirements of a + /// finder. + /// + /// {@template flutter_test.finders.FinderBase.describeMatch} + /// The description returned should be a brief English phrase describing a + /// matching candidate with the proper plural form. As an example for a string + /// finder that is looking for strings starting with "hello": + /// + /// ```dart + /// String describeMatch(Plurality plurality) { + /// return switch (plurality) { + /// Plurality.zero || Plurality.many => 'strings starting with "hello"', + /// Plurality.one => 'string starting with "hello"', + /// }; + /// } + /// ``` + /// {@endtemplate} /// - /// This is the internal API for the [Finder]. To obtain the elements from - /// a [Finder] in a test, consider [WidgetTester.elementList]. + /// This will be used both to describe a finder and the results of searching + /// with that finder. /// - /// See [collectAllElementsFrom]. + /// See also: + /// + /// * [FinderBase.toString] where this is used to fully describe the finder + /// * [FinderResult.toString] where this is used to provide context to the + /// results of a search + String describeMatch(Plurality plurality); + + /// Returns all of the items that will be considered by this finder. @protected - Iterable<Element> get allCandidates { - return collectAllElementsFrom( - WidgetsBinding.instance.rootElement!, - skipOffstage: skipOffstage, + Iterable<CandidateType> get allCandidates; + + /// Returns a variant of this finder that only matches the first item + /// found by this finder. + FinderBase<CandidateType> get first => _FirstFinder<CandidateType>(this); + + /// Returns a variant of this finder that only matches the last item + /// found by this finder. + FinderBase<CandidateType> get last => _LastFinder<CandidateType>(this); + + /// Returns a variant of this finder that only matches the item at the + /// given index found by this finder. + FinderBase<CandidateType> at(int index) => _IndexFinder<CandidateType>(this, index); + + /// Returns all the items in the given list that match this + /// finder's requirements. + /// + /// This is overridden to define the requirements of the finder when + /// implementing finders that directly extend [FinderBase]. If a finder can + /// be efficiently described just in terms of a predicate function, consider + /// mixing in [MatchFinderMixin] and implementing [MatchFinderMixin.matches] + /// instead. + @protected + Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates); + + /// Searches a set of candidates for those that meet the requirements set by + /// this finder and returns the result of that search. + /// + /// See also: + /// + /// * [found] which will return the latest results without re-executing the + /// search. + /// * [tryEvaluate] which will indicate whether any results were found rather + /// than directly returning results. + FinderResult<CandidateType> evaluate() { + if (!_cached || _found == null) { + _found = FinderResult<CandidateType>(describeMatch, findInCandidates(allCandidates)); + } + return found; + } + + /// Searches a set of candidates for those that meet the requirements set by + /// this finder and returns whether the search found any matching candidates. + /// + /// This is useful in cases where an action needs to be repeated while or + /// until a finder has results. The results from the search can be accessed + /// using the [found] property without re-executing the search. + /// + /// ## Sample code + /// + /// ```dart + /// testWidgets('Top text loads first', (WidgetTester tester) async { + /// // Assume a widget is pumped with a top and bottom loading area, with + /// // the texts "Top loaded" and "Bottom loaded" when loading is complete. + /// // await tester.pumpWidget(...) + /// + /// // Wait until at least one loaded widget is available + /// Finder loadedFinder = find.textContaining('loaded'); + /// while (!loadedFinder.tryEvaluate()) { + /// await tester.pump(const Duration(milliseconds: 100)); + /// } + /// + /// expect(loadedFinder.found, hasLength(1)); + /// expect(tester.widget<Text>(loadedFinder).data, contains('Top')); + /// }); + /// ``` + bool tryEvaluate() { + evaluate(); + return found.isNotEmpty; + } + + /// Runs the given callback using cached results. + /// + /// While in this callback, this [FinderBase] will cache the results from the + /// next call to [evaluate] or [tryEvaluate] and then no longer evaluate new results + /// until the callback completes. After the first call, all calls to [evaluate], + /// [tryEvaluate] or [found] will return the same results without evaluating. + void runCached(VoidCallback run) { + reset(); + _cached = true; + try { + run(); + } finally { + reset(); + _cached = false; + } + } + + /// Resets all state of this [FinderBase]. + /// + /// Generally used between tests to reset the state of [found] if a finder is + /// used across multiple tests. + void reset() { + _found = null; + } + + /// A string representation of this finder or its results. + /// + /// By default, this describes the results of the search in order to play + /// nicely with [expect] and its output when a failure occurs. If you wish + /// to get a string representation of the finder itself, pass [describeSelf] + /// as `true`. + @override + String toString({bool describeSelf = false}) { + if (describeSelf) { + return 'A finder that searches for ${describeMatch(Plurality.many)}.'; + } else { + if (!hasFound) { + evaluate(); + } + return found.toString(); + } + } +} + +/// The results of searching with a [FinderBase]. +class FinderResult<CandidateType> extends Iterable<CandidateType> { + /// Creates a new [FinderResult] that describes the `values` using the given + /// `describeMatch` callback. + /// + /// {@macro flutter_test.finders.FinderBase.describeMatch} + FinderResult(DescribeMatchCallback describeMatch, Iterable<CandidateType> values) + : _describeMatch = describeMatch, _values = values; + + final DescribeMatchCallback _describeMatch; + final Iterable<CandidateType> _values; + + @override + Iterator<CandidateType> get iterator => _values.iterator; + + @override + String toString() { + final List<CandidateType> valuesList = _values.toList(); + // This will put each value on its own line with a comma and indentation + final String valuesString = valuesList.fold( + '', + (String current, CandidateType candidate) => '$current\n $candidate,', ); + return 'Found ${valuesList.length} ${_describeMatch(Plurality._fromNum(valuesList.length))}: [' + '${valuesString.isNotEmpty ? '$valuesString\n' : ''}' + ']'; } +} - Iterable<Element>? _cachedResult; +/// Provides backwards compatibility with the original [Finder] API. +mixin _LegacyFinderMixin on FinderBase<Element> { + Iterable<Element>? _precacheResults; - /// Returns the current result. If [precache] was called and returned true, this will - /// cheaply return the result that was computed then. Otherwise, it creates a new - /// iterable to compute the answer. + /// Describes what the finder is looking for. The description should be + /// a brief English noun phrase describing the finder's requirements. + @Deprecated( + 'Use FinderBase.describeMatch instead. ' + 'FinderBase.describeMatch allows for more readable descriptions and removes ambiguity about pluralization. ' + 'This feature was deprecated after v3.13.0-0.2.pre.' + ) + String get description; + + /// Returns all the elements in the given list that match this + /// finder's pattern. /// - /// Calling this clears the cache from [precache]. - Iterable<Element> evaluate() { - final Iterable<Element> result = _cachedResult ?? apply(allCandidates); - _cachedResult = null; - return result; + /// When implementing Finders that inherit directly from + /// [Finder], [findInCandidates] is the main method to override. This method + /// is maintained for backwards compatibility and will be removed in a future + /// version of Flutter. If the finder can efficiently be described just in + /// terms of a predicate function, consider mixing in [MatchFinderMixin] + /// instead. + @Deprecated( + 'Override FinderBase.findInCandidates instead. ' + 'Using the FinderBase API allows for more consistent caching behavior and cleaner options for interacting with the widget tree. ' + 'This feature was deprecated after v3.13.0-0.2.pre.' + ) + Iterable<Element> apply(Iterable<Element> candidates) { + return findInCandidates(candidates); } /// Attempts to evaluate the finder. Returns whether any elements in the tree @@ -519,131 +922,230 @@ abstract class Finder { /// from [evaluate]. /// /// If this returns true, you must call [evaluate] before you call [precache] again. + @Deprecated( + 'Use FinderBase.tryFind or FinderBase.runCached instead. ' + 'Using the FinderBase API allows for more consistent caching behavior and cleaner options for interacting with the widget tree. ' + 'This feature was deprecated after v3.13.0-0.2.pre.' + ) bool precache() { - assert(_cachedResult == null); - final Iterable<Element> result = apply(allCandidates); - if (result.isNotEmpty) { - _cachedResult = result; + assert(_precacheResults == null); + if (tryEvaluate()) { return true; } - _cachedResult = null; + _precacheResults = null; return false; } - /// Returns a variant of this finder that only matches the first element - /// matched by this finder. - Finder get first => _FirstFinder(this); + @override + Iterable<Element> findInCandidates(Iterable<Element> candidates) { + return apply(candidates); + } +} + +/// A base class for creating finders that search the [Element] tree for +/// [Widget]s. +/// +/// The [findInCandidates] method must be overriden and will be enforced at +/// compilation after [apply] is removed. +abstract class Finder extends FinderBase<Element> with _LegacyFinderMixin { + /// Creates a new [Finder] with the given `skipOffstage` value. + Finder({this.skipOffstage = true}); + + /// Whether this finder skips nodes that are offstage. + /// + /// If this is true, then the elements are walked using + /// [Element.debugVisitOnstageChildren]. This skips offstage children of + /// [Offstage] widgets, as well as children of inactive [Route]s. + final bool skipOffstage; - /// Returns a variant of this finder that only matches the last element - /// matched by this finder. - Finder get last => _LastFinder(this); + @override + Finder get first => _FirstWidgetFinder(this); - /// Returns a variant of this finder that only matches the element at the - /// given index matched by this finder. - Finder at(int index) => _IndexFinder(this, index); + @override + Finder get last => _LastWidgetFinder(this); + + @override + Finder at(int index) => _IndexWidgetFinder(this, index); + + @override + Iterable<Element> get allCandidates { + return collectAllElementsFrom( + WidgetsBinding.instance.rootElement!, + skipOffstage: skipOffstage, + ); + } + + @override + String describeMatch(Plurality plurality) { + return switch (plurality) { + Plurality.zero ||Plurality.many => 'widgets with $description', + Plurality.one => 'widget with $description', + }; + } /// Returns a variant of this finder that only matches elements reachable by /// a hit test. /// - /// The [at] parameter specifies the location relative to the size of the + /// The `at` parameter specifies the location relative to the size of the /// target element where the hit test is performed. - Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableFinder(this, at); + Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableWidgetFinder(this, at); +} + +/// A base class for creating finders that search the semantics tree. +abstract class SemanticsFinder extends FinderBase<SemanticsNode> { + /// Creates a new [SemanticsFinder] that will search starting at the given + /// `root`. + SemanticsFinder(this.root); + + /// The root of the semantics tree that this finder will search. + final SemanticsNode root; @override - String toString() { - final String additional = skipOffstage ? ' (ignoring offstage widgets)' : ''; - final List<Element> widgets = evaluate().toList(); - final int count = widgets.length; - if (count == 0) { - return 'zero widgets with $description$additional'; - } - if (count == 1) { - return 'exactly one widget with $description$additional: ${widgets.single}'; - } - if (count < 4) { - return '$count widgets with $description$additional: $widgets'; - } - return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...'; + Iterable<SemanticsNode> get allCandidates { + return collectAllSemanticsNodesFrom(root); } } -/// Applies additional filtering against a [parent] [Finder]. -abstract class ChainedFinder extends Finder { - /// Create a Finder chained against the candidates of another [Finder]. - ChainedFinder(this.parent); +/// A mixin that applies additional filtering to the results of a parent [Finder]. + mixin ChainedFinderMixin<CandidateType> on FinderBase<CandidateType> { - /// Another [Finder] that will run first. - final Finder parent; + /// Another finder whose results will be further filtered. + FinderBase<CandidateType> get parent; /// Return another [Iterable] when given an [Iterable] of candidates from a - /// parent [Finder]. + /// parent [FinderBase]. /// - /// This is the method to implement when subclassing [ChainedFinder]. - Iterable<Element> filter(Iterable<Element> parentCandidates); + /// This is the main method to implement when mixing in [ChainedFinderMixin]. + Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates); @override - Iterable<Element> apply(Iterable<Element> candidates) { - return filter(parent.apply(candidates)); + Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) { + return filter(parent.findInCandidates(candidates)); } @override - Iterable<Element> get allCandidates => parent.allCandidates; + Iterable<CandidateType> get allCandidates => parent.allCandidates; } -class _FirstFinder extends ChainedFinder { - _FirstFinder(super.parent); +/// Applies additional filtering against a [parent] widget finder. +abstract class ChainedFinder extends Finder with ChainedFinderMixin<Element> { + /// Create a Finder chained against the candidates of another `parent` [Finder]. + ChainedFinder(this.parent); @override - String get description => '${parent.description} (ignoring all but first)'; + final FinderBase<Element> parent; +} +mixin _FirstFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType>{ @override - Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { + String describeMatch(Plurality plurality) { + return '${parent.describeMatch(plurality)} (ignoring all but first)'; + } + + @override + Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* { yield parentCandidates.first; } } -class _LastFinder extends ChainedFinder { - _LastFinder(super.parent); +class _FirstFinder<CandidateType> extends FinderBase<CandidateType> + with ChainedFinderMixin<CandidateType>, _FirstFinderMixin<CandidateType> { + _FirstFinder(this.parent); @override - String get description => '${parent.description} (ignoring all but last)'; + final FinderBase<CandidateType> parent; +} + +class _FirstWidgetFinder extends ChainedFinder with _FirstFinderMixin<Element> { + _FirstWidgetFinder(super.parent); @override - Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { + String get description => describeMatch(Plurality.many); +} + +mixin _LastFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType> { + @override + String describeMatch(Plurality plurality) { + return '${parent.describeMatch(plurality)} (ignoring all but first)'; + } + + @override + Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* { yield parentCandidates.last; } } -class _IndexFinder extends ChainedFinder { - _IndexFinder(super.parent, this.index); +class _LastFinder<CandidateType> extends FinderBase<CandidateType> + with ChainedFinderMixin<CandidateType>, _LastFinderMixin<CandidateType>{ + _LastFinder(this.parent); - final int index; + @override + final FinderBase<CandidateType> parent; +} + +class _LastWidgetFinder extends ChainedFinder with _LastFinderMixin<Element> { + _LastWidgetFinder(super.parent); @override - String get description => '${parent.description} (ignoring all but index $index)'; + String get description => describeMatch(Plurality.many); +} + +mixin _IndexFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType> { + int get index; @override - Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { + String describeMatch(Plurality plurality) { + return '${parent.describeMatch(plurality)} (ignoring all but index $index)'; + } + + @override + Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* { yield parentCandidates.elementAt(index); } } -class _HitTestableFinder extends ChainedFinder { - _HitTestableFinder(super.parent, this.alignment); +class _IndexFinder<CandidateType> extends FinderBase<CandidateType> + with ChainedFinderMixin<CandidateType>, _IndexFinderMixin<CandidateType> { + _IndexFinder(this.parent, this.index); + + @override + final int index; + + @override + final FinderBase<CandidateType> parent; +} + +class _IndexWidgetFinder extends ChainedFinder with _IndexFinderMixin<Element> { + _IndexWidgetFinder(super.parent, this.index); + + @override + final int index; + + @override + String get description => describeMatch(Plurality.many); +} + +class _HitTestableWidgetFinder extends ChainedFinder { + _HitTestableWidgetFinder(super.parent, this.alignment); final Alignment alignment; @override - String get description => '${parent.description} (considering only hit-testable ones)'; + String describeMatch(Plurality plurality) { + return '${parent.describeMatch(plurality)} (considering only hit-testable ones)'; + } + + @override + String get description => describeMatch(Plurality.many); @override Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { for (final Element candidate in parentCandidates) { + final int viewId = candidate.findAncestorWidgetOfExactType<View>()!.view.viewId; final RenderBox box = candidate.renderObject! as RenderBox; final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size)); final HitTestResult hitResult = HitTestResult(); - // TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 - WidgetsBinding.instance.hitTest(hitResult, absoluteOffset); // ignore: deprecated_member_use + WidgetsBinding.instance.hitTestInView(hitResult, absoluteOffset, viewId); for (final HitTestEntry entry in hitResult.path) { if (entry.target == candidate.renderObject) { yield candidate; @@ -654,24 +1156,27 @@ class _HitTestableFinder extends ChainedFinder { } } -/// Searches a widget tree and returns nodes that match a particular -/// pattern. -abstract class MatchFinder extends Finder { - /// Initializes a predicate-based Finder. Used by subclasses to initialize the - /// [skipOffstage] property. - MatchFinder({ super.skipOffstage }); - +/// A mixin for creating finders that search candidates for those that match +/// a given pattern. +mixin MatchFinderMixin<CandidateType> on FinderBase<CandidateType> { /// Returns true if the given element matches the pattern. /// - /// When implementing your own MatchFinder, this is the main method to override. - bool matches(Element candidate); + /// When implementing a MatchFinder, this is the main method to override. + bool matches(CandidateType candidate); @override - Iterable<Element> apply(Iterable<Element> candidates) { + Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) { return candidates.where(matches); } } +/// Searches candidates for any that match a particular pattern. +abstract class MatchFinder extends Finder with MatchFinderMixin<Element> { + /// Initializes a predicate-based Finder. Used by subclasses to initialize the + /// `skipOffstage` property. + MatchFinder({ super.skipOffstage }); +} + abstract class _MatchTextFinder extends MatchFinder { _MatchTextFinder({ this.findRichText = false, @@ -734,8 +1239,8 @@ abstract class _MatchTextFinder extends MatchFinder { } } -class _TextFinder extends _MatchTextFinder { - _TextFinder( +class _TextWidgetFinder extends _MatchTextFinder { + _TextWidgetFinder( this.text, { super.findRichText, super.skipOffstage, @@ -752,8 +1257,8 @@ class _TextFinder extends _MatchTextFinder { } } -class _TextContainingFinder extends _MatchTextFinder { - _TextContainingFinder( +class _TextContainingWidgetFinder extends _MatchTextFinder { + _TextContainingWidgetFinder( this.pattern, { super.findRichText, super.skipOffstage, @@ -770,8 +1275,8 @@ class _TextContainingFinder extends _MatchTextFinder { } } -class _KeyFinder extends MatchFinder { - _KeyFinder(this.key, { super.skipOffstage }); +class _KeyWidgetFinder extends MatchFinder { + _KeyWidgetFinder(this.key, { super.skipOffstage }); final Key key; @@ -784,8 +1289,8 @@ class _KeyFinder extends MatchFinder { } } -class _WidgetSubtypeFinder<T extends Widget> extends MatchFinder { - _WidgetSubtypeFinder({ super.skipOffstage }); +class _SubtypeWidgetFinder<T extends Widget> extends MatchFinder { + _SubtypeWidgetFinder({ super.skipOffstage }); @override String get description => 'is "$T"'; @@ -796,8 +1301,8 @@ class _WidgetSubtypeFinder<T extends Widget> extends MatchFinder { } } -class _WidgetTypeFinder extends MatchFinder { - _WidgetTypeFinder(this.widgetType, { super.skipOffstage }); +class _TypeWidgetFinder extends MatchFinder { + _TypeWidgetFinder(this.widgetType, { super.skipOffstage }); final Type widgetType; @@ -810,8 +1315,8 @@ class _WidgetTypeFinder extends MatchFinder { } } -class _WidgetImageFinder extends MatchFinder { - _WidgetImageFinder(this.image, { super.skipOffstage }); +class _ImageWidgetFinder extends MatchFinder { + _ImageWidgetFinder(this.image, { super.skipOffstage }); final ImageProvider image; @@ -830,8 +1335,8 @@ class _WidgetImageFinder extends MatchFinder { } } -class _WidgetIconFinder extends MatchFinder { - _WidgetIconFinder(this.icon, { super.skipOffstage }); +class _IconWidgetFinder extends MatchFinder { + _IconWidgetFinder(this.icon, { super.skipOffstage }); final IconData icon; @@ -845,8 +1350,8 @@ class _WidgetIconFinder extends MatchFinder { } } -class _ElementTypeFinder extends MatchFinder { - _ElementTypeFinder(this.elementType, { super.skipOffstage }); +class _ElementTypeWidgetFinder extends MatchFinder { + _ElementTypeWidgetFinder(this.elementType, { super.skipOffstage }); final Type elementType; @@ -859,8 +1364,8 @@ class _ElementTypeFinder extends MatchFinder { } } -class _WidgetFinder extends MatchFinder { - _WidgetFinder(this.widget, { super.skipOffstage }); +class _ExactWidgetFinder extends MatchFinder { + _ExactWidgetFinder(this.widget, { super.skipOffstage }); final Widget widget; @@ -873,15 +1378,15 @@ class _WidgetFinder extends MatchFinder { } } -class _WidgetPredicateFinder extends MatchFinder { - _WidgetPredicateFinder(this.predicate, { String? description, super.skipOffstage }) +class _WidgetPredicateWidgetFinder extends MatchFinder { + _WidgetPredicateWidgetFinder(this.predicate, { String? description, super.skipOffstage }) : _description = description; final WidgetPredicate predicate; final String? _description; @override - String get description => _description ?? 'widget matching predicate ($predicate)'; + String get description => _description ?? 'widget matching predicate'; @override bool matches(Element candidate) { @@ -889,15 +1394,15 @@ class _WidgetPredicateFinder extends MatchFinder { } } -class _ElementPredicateFinder extends MatchFinder { - _ElementPredicateFinder(this.predicate, { String? description, super.skipOffstage }) +class _ElementPredicateWidgetFinder extends MatchFinder { + _ElementPredicateWidgetFinder(this.predicate, { String? description, super.skipOffstage }) : _description = description; final ElementPredicate predicate; final String? _description; @override - String get description => _description ?? 'element matching predicate ($predicate)'; + String get description => _description ?? 'element matching predicate'; @override bool matches(Element candidate) { @@ -905,80 +1410,182 @@ class _ElementPredicateFinder extends MatchFinder { } } -class _DescendantFinder extends Finder { - _DescendantFinder( - this.ancestor, - this.descendant, { - this.matchRoot = false, - super.skipOffstage, - }); +class _PredicateSemanticsFinder extends SemanticsFinder + with MatchFinderMixin<SemanticsNode> { + _PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.root) + : _describeMatch = describeMatch; - final Finder ancestor; - final Finder descendant; - final bool matchRoot; + final SemanticsNodePredicate predicate; + final DescribeMatchCallback? _describeMatch; @override - String get description { - if (matchRoot) { - return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}'; - } - return '${descendant.description} that has ancestor(s) with ${ancestor.description}'; + String describeMatch(Plurality plurality) { + return _describeMatch?.call(plurality) ?? + 'matching semantics predicate'; } @override - Iterable<Element> apply(Iterable<Element> candidates) { - final Iterable<Element> descendants = descendant.evaluate(); - return candidates.where((Element element) => descendants.contains(element)); + bool matches(SemanticsNode candidate) { + return predicate(candidate); } +} + +mixin _DescendantFinderMixin<CandidateType> on FinderBase<CandidateType> { + + FinderBase<CandidateType> get ancestor; + FinderBase<CandidateType> get descendant; + bool get matchRoot; @override - Iterable<Element> get allCandidates { - final Iterable<Element> ancestorElements = ancestor.evaluate(); - final List<Element> candidates = ancestorElements.expand<Element>( - (Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage) + String describeMatch(Plurality plurality) { + return '${descendant.describeMatch(plurality)} descending from ' + '${ancestor.describeMatch(plurality)}' + '${matchRoot ? ' inclusive' : ''}'; + } + + @override + Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) { + final Iterable<CandidateType> descendants = descendant.evaluate(); + return candidates.where((CandidateType candidate) => descendants.contains(candidate)); + } + + @override + Iterable<CandidateType> get allCandidates { + final Iterable<CandidateType> ancestors = ancestor.evaluate(); + final List<CandidateType> candidates = ancestors.expand<CandidateType>( + (CandidateType ancestor) => _collectDescendants(ancestor) ).toSet().toList(); if (matchRoot) { - candidates.insertAll(0, ancestorElements); + candidates.insertAll(0, ancestors); } return candidates; } + + Iterable<CandidateType> _collectDescendants(CandidateType root); } -class _AncestorFinder extends Finder { - _AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false); +class _DescendantWidgetFinder extends Finder + with _DescendantFinderMixin<Element> { + _DescendantWidgetFinder( + this.ancestor, + this.descendant, { + this.matchRoot = false, + super.skipOffstage, + }); - final Finder ancestor; - final Finder descendant; + @override + final FinderBase<Element> ancestor; + @override + final FinderBase<Element> descendant; + @override final bool matchRoot; @override - String get description { - if (matchRoot) { - return 'ancestor ${ancestor.description} beginning with ${descendant.description}'; - } - return '${ancestor.description} which is an ancestor of ${descendant.description}'; + String get description => describeMatch(Plurality.many); + + @override + Iterable<Element> _collectDescendants(Element root) { + return collectAllElementsFrom(root, skipOffstage: skipOffstage); } +} + +class _DescendantSemanticsFinder extends FinderBase<SemanticsNode> + with _DescendantFinderMixin<SemanticsNode> { + _DescendantSemanticsFinder(this.ancestor, this.descendant, {this.matchRoot = false}); @override - Iterable<Element> apply(Iterable<Element> candidates) { - final Iterable<Element> ancestors = ancestor.evaluate(); - return candidates.where((Element element) => ancestors.contains(element)); + final FinderBase<SemanticsNode> ancestor; + + @override + final FinderBase<SemanticsNode> descendant; + + @override + final bool matchRoot; + + @override + Iterable<SemanticsNode> _collectDescendants(SemanticsNode root) { + return collectAllSemanticsNodesFrom(root); } +} + +mixin _AncestorFinderMixin<CandidateType> on FinderBase<CandidateType> { + FinderBase<CandidateType> get ancestor; + FinderBase<CandidateType> get descendant; + bool get matchLeaves; @override - Iterable<Element> get allCandidates { - final List<Element> candidates = <Element>[]; - for (final Element root in descendant.evaluate()) { - final List<Element> ancestors = <Element>[]; - if (matchRoot) { - ancestors.add(root); + String describeMatch(Plurality plurality) { + return '${ancestor.describeMatch(plurality)} that are ancestors of ' + '${descendant.describeMatch(plurality)}' + '${matchLeaves ? ' inclusive' : ''}'; + } + + @override + Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) { + final Iterable<CandidateType> ancestors = ancestor.evaluate(); + return candidates.where((CandidateType element) => ancestors.contains(element)); + } + + @override + Iterable<CandidateType> get allCandidates { + final List<CandidateType> candidates = <CandidateType>[]; + for (final CandidateType leaf in descendant.evaluate()) { + if (matchLeaves) { + candidates.add(leaf); } - root.visitAncestorElements((Element element) { - ancestors.add(element); - return true; - }); - candidates.addAll(ancestors); + candidates.addAll(_collectAncestors(leaf)); } return candidates; } + + Iterable<CandidateType> _collectAncestors(CandidateType child); +} + +class _AncestorWidgetFinder extends Finder + with _AncestorFinderMixin<Element> { + _AncestorWidgetFinder(this.descendant, this.ancestor, { this.matchLeaves = false }) : super(skipOffstage: false); + + @override + final FinderBase<Element> ancestor; + @override + final FinderBase<Element> descendant; + @override + final bool matchLeaves; + + @override + String get description => describeMatch(Plurality.many); + + @override + Iterable<Element> _collectAncestors(Element child) { + final List<Element> ancestors = <Element>[]; + child.visitAncestorElements((Element element) { + ancestors.add(element); + return true; + }); + return ancestors; + } +} + +class _AncestorSemanticsFinder extends FinderBase<SemanticsNode> + with _AncestorFinderMixin<SemanticsNode> { + _AncestorSemanticsFinder(this.descendant, this.ancestor, this.matchLeaves); + + @override + final FinderBase<SemanticsNode> ancestor; + + @override + final FinderBase<SemanticsNode> descendant; + + @override + final bool matchLeaves; + + @override + Iterable<SemanticsNode> _collectAncestors(SemanticsNode child) { + final List<SemanticsNode> ancestors = <SemanticsNode>[]; + while (child.parent != null) { + ancestors.add(child.parent!); + child = child.parent!; + } + return ancestors; + } } diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart index 82259c5879eef..edd49d2841b9c 100644 --- a/packages/flutter_test/lib/src/goldens.dart +++ b/packages/flutter_test/lib/src/goldens.dart @@ -331,8 +331,6 @@ class ComparisonResult { }); /// Indicates whether or not a pixel comparison test has failed. - /// - /// This value cannot be null. final bool passed; /// Error message used to describe the cause of the pixel comparison failure. diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 3cc1bb6ce2e99..85087dc5e636b 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -12,15 +12,17 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:matcher/expect.dart'; import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports +import 'package:vector_math/vector_math_64.dart' show Matrix3; import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage; import 'accessibility.dart'; import 'binding.dart'; +import 'controller.dart'; import 'finders.dart'; import 'goldens.dart'; import 'widget_tester.dart' show WidgetTester; -/// Asserts that the [Finder] matches no widgets in the widget tree. +/// Asserts that the [FinderBase] matches nothing in the available candidates. /// /// ## Sample code /// @@ -30,14 +32,16 @@ import 'widget_tester.dart' show WidgetTester; /// /// See also: /// -/// * [findsWidgets], when you want the finder to find one or more widgets. -/// * [findsOneWidget], when you want the finder to find exactly one widget. -/// * [findsNWidgets], when you want the finder to find a specific number of widgets. -/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. -const Matcher findsNothing = _FindsWidgetMatcher(null, 0); +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsOne], when you want the finder to find exactly one candidate. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +const Matcher findsNothing = _FindsCountMatcher(null, 0); /// Asserts that the [Finder] locates at least one widget in the widget tree. /// +/// This is equivalent to the preferred [findsAny] method. +/// /// ## Sample code /// /// ```dart @@ -47,13 +51,31 @@ const Matcher findsNothing = _FindsWidgetMatcher(null, 0); /// See also: /// /// * [findsNothing], when you want the finder to not find anything. -/// * [findsOneWidget], when you want the finder to find exactly one widget. -/// * [findsNWidgets], when you want the finder to find a specific number of widgets. -/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. -const Matcher findsWidgets = _FindsWidgetMatcher(1, null); +/// * [findsOne], when you want the finder to find exactly one candidate. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +const Matcher findsWidgets = _FindsCountMatcher(1, null); + +/// Asserts that the [FinderBase] locates at least one matching candidate. +/// +/// ## Sample code +/// +/// ```dart +/// expect(find.text('Save'), findsAny); +/// ``` +/// +/// See also: +/// +/// * [findsNothing], when you want the finder to not find anything. +/// * [findsOne], when you want the finder to find exactly one candidate. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +const Matcher findsAny = _FindsCountMatcher(1, null); /// Asserts that the [Finder] locates at exactly one widget in the widget tree. /// +/// This is equivalent to the preferred [findsOne] method. +/// /// ## Sample code /// /// ```dart @@ -63,13 +85,31 @@ const Matcher findsWidgets = _FindsWidgetMatcher(1, null); /// See also: /// /// * [findsNothing], when you want the finder to not find anything. -/// * [findsWidgets], when you want the finder to find one or more widgets. -/// * [findsNWidgets], when you want the finder to find a specific number of widgets. -/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. -const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1); +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +const Matcher findsOneWidget = _FindsCountMatcher(1, 1); + +/// Asserts that the [FinderBase] finds exactly one matching candidate. +/// +/// ## Sample code +/// +/// ```dart +/// expect(find.text('Save'), findsOne); +/// ``` +/// +/// See also: +/// +/// * [findsNothing], when you want the finder to not find anything. +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsExactly], when you want the finder to find a specific number candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +const Matcher findsOne = _FindsCountMatcher(1, 1); /// Asserts that the [Finder] locates the specified number of widgets in the widget tree. /// +/// This is equivalent to the preferred [findsExactly] method. +/// /// ## Sample code /// /// ```dart @@ -79,13 +119,31 @@ const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1); /// See also: /// /// * [findsNothing], when you want the finder to not find anything. -/// * [findsWidgets], when you want the finder to find one or more widgets. -/// * [findsOneWidget], when you want the finder to find exactly one widget. -/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. -Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n); +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsOne], when you want the finder to find exactly one candidate. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +Matcher findsNWidgets(int n) => _FindsCountMatcher(n, n); + +/// Asserts that the [FinderBase] locates the specified number of candidates. +/// +/// ## Sample code +/// +/// ```dart +/// expect(find.text('Save'), findsExactly(2)); +/// ``` +/// +/// See also: +/// +/// * [findsNothing], when you want the finder to not find anything. +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsOne], when you want the finder to find exactly one candidates. +/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. +Matcher findsExactly(int n) => _FindsCountMatcher(n, n); /// Asserts that the [Finder] locates at least a number of widgets in the widget tree. /// +/// This is equivalent to the preferred [findsAtLeast] method. +/// /// ## Sample code /// /// ```dart @@ -95,10 +153,26 @@ Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n); /// See also: /// /// * [findsNothing], when you want the finder to not find anything. -/// * [findsWidgets], when you want the finder to find one or more widgets. -/// * [findsOneWidget], when you want the finder to find exactly one widget. -/// * [findsNWidgets], when you want the finder to find a specific number of widgets. -Matcher findsAtLeastNWidgets(int n) => _FindsWidgetMatcher(n, null); +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsOne], when you want the finder to find exactly one candidate. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +Matcher findsAtLeastNWidgets(int n) => _FindsCountMatcher(n, null); + +/// Asserts that the [FinderBase] locates at least the given number of candidates. +/// +/// ## Sample code +/// +/// ```dart +/// expect(find.text('Save'), findsAtLeast(2)); +/// ``` +/// +/// See also: +/// +/// * [findsNothing], when you want the finder to not find anything. +/// * [findsAny], when you want the finder to find one or more candidates. +/// * [findsOne], when you want the finder to find exactly one candidates. +/// * [findsExactly], when you want the finder to find a specific number of candidates. +Matcher findsAtLeast(int n) => _FindsCountMatcher(n, null); /// Asserts that the [Finder] locates a single widget that has at /// least one [Offstage] widget ancestor. @@ -272,10 +346,24 @@ Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolera /// /// * [moreOrLessEquals], which is for [double]s. /// * [offsetMoreOrLessEquals], which is for [Offset]s. +/// * [matrix3MoreOrLessEquals], which is for [Matrix3]s. Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) { return _IsWithinDistance<Matrix4>(_matrixDistance, value, epsilon); } +/// Asserts that two [Matrix3]s are equal, within some tolerated error. +/// +/// {@macro flutter.flutter_test.moreOrLessEquals} +/// +/// See also: +/// +/// * [moreOrLessEquals], which is for [double]s. +/// * [offsetMoreOrLessEquals], which is for [Offset]s. +/// * [matrixMoreOrLessEquals], which is for [Matrix4]s. +Matcher matrix3MoreOrLessEquals(Matrix3 value, { double epsilon = precisionErrorTolerance }) { + return _IsWithinDistance<Matrix3>(_matrix3Distance, value, epsilon); +} + /// Asserts that two [Offset]s are equal, within some tolerated error. /// /// {@macro flutter.flutter_test.moreOrLessEquals} @@ -331,6 +419,13 @@ Matcher isMethodCall(String name, { required dynamic arguments }) { Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int sampleSize = 20 }) => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize); +// Examples can assume: +// late Image image; +// late Future<Image> imageFuture; +// typedef MyWidget = Placeholder; +// late Future<ByteData> someFont; +// late WidgetTester tester; + /// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the /// golden image file identified by [key], with an optional [version] number. /// @@ -404,18 +499,18 @@ Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int s /// {@tool snippet} /// How to load a custom font for golden images. /// ```dart -/// testWidgets('Creating a golden image with a custom font', (tester) async { +/// testWidgets('Creating a golden image with a custom font', (WidgetTester tester) async { /// // Assuming the 'Roboto.ttf' file is declared in the pubspec.yaml file -/// final font = rootBundle.load('path/to/font-file/Roboto.ttf'); +/// final Future<ByteData> font = rootBundle.load('path/to/font-file/Roboto.ttf'); /// -/// final fontLoader = FontLoader('Roboto')..addFont(font); +/// final FontLoader fontLoader = FontLoader('Roboto')..addFont(font); /// await fontLoader.load(); /// -/// await tester.pumpWidget(const SomeWidget()); +/// await tester.pumpWidget(const MyWidget()); /// /// await expectLater( -/// find.byType(SomeWidget), -/// matchesGoldenFile('someWidget.png'), +/// find.byType(MyWidget), +/// matchesGoldenFile('myWidget.png'), /// ); /// }); /// ``` @@ -431,7 +526,7 @@ Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int s /// ```dart /// Future<void> testExecutable(FutureOr<void> Function() testMain) async { /// setUpAll(() async { -/// final fontLoader = FontLoader('SomeFont')..addFont(someFont); +/// final FontLoader fontLoader = FontLoader('SomeFont')..addFont(someFont); /// await fontLoader.load(); /// }); /// @@ -473,18 +568,22 @@ AsyncMatcher matchesGoldenFile(Object key, {int? version}) { /// ## Sample code /// /// ```dart -/// final ui.Paint paint = ui.Paint() -/// ..style = ui.PaintingStyle.stroke -/// ..strokeWidth = 1.0; -/// final ui.PictureRecorder recorder = ui.PictureRecorder(); -/// final ui.Canvas pictureCanvas = ui.Canvas(recorder); -/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint); -/// final ui.Picture picture = recorder.endRecording(); -/// ui.Image referenceImage = picture.toImage(50, 50); -/// -/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage)); -/// await expectLater(image, matchesReferenceImage(referenceImage); -/// await expectLater(imageFuture, matchesReferenceImage(referenceImage)); +/// testWidgets('matchesReferenceImage', (WidgetTester tester) async { +/// final ui.Paint paint = ui.Paint() +/// ..style = ui.PaintingStyle.stroke +/// ..strokeWidth = 1.0; +/// final ui.PictureRecorder recorder = ui.PictureRecorder(); +/// final ui.Canvas pictureCanvas = ui.Canvas(recorder); +/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint); +/// final ui.Picture picture = recorder.endRecording(); +/// addTearDown(picture.dispose); +/// ui.Image referenceImage = await picture.toImage(50, 50); +/// addTearDown(referenceImage.dispose); +/// +/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage)); +/// await expectLater(image, matchesReferenceImage(referenceImage)); +/// await expectLater(imageFuture, matchesReferenceImage(referenceImage)); +/// }); /// ``` /// /// See also: @@ -508,14 +607,17 @@ AsyncMatcher matchesReferenceImage(ui.Image image) { /// ## Sample code /// /// ```dart -/// final SemanticsHandle handle = tester.ensureSemantics(); -/// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello')); -/// handle.dispose(); +/// testWidgets('matchesSemantics', (WidgetTester tester) async { +/// final SemanticsHandle handle = tester.ensureSemantics(); +/// // ... +/// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello')); +/// handle.dispose(); +/// }); /// ``` /// /// See also: /// -/// * [WidgetTester.getSemantics], the tester method which retrieves semantics. +/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. /// * [containsSemantics], a similar matcher without default values for flags or actions. Matcher matchesSemantics({ String? label, @@ -564,6 +666,8 @@ Matcher matchesSemantics({ bool hasToggledState = false, bool isToggled = false, bool hasImplicitScrolling = false, + bool hasExpandedState = false, + bool isExpanded = false, // Actions // bool hasTapAction = false, bool hasLongPressAction = false, @@ -640,6 +744,8 @@ Matcher matchesSemantics({ hasToggledState: hasToggledState, isToggled: isToggled, hasImplicitScrolling: hasImplicitScrolling, + hasExpandedState: hasExpandedState, + isExpanded: isExpanded, // Actions hasTapAction: hasTapAction, hasLongPressAction: hasLongPressAction, @@ -681,14 +787,17 @@ Matcher matchesSemantics({ /// ## Sample code /// /// ```dart -/// final SemanticsHandle handle = tester.ensureSemantics(); -/// expect(tester.getSemantics(find.text('hello')), hasSemantics(label: 'hello')); -/// handle.dispose(); +/// testWidgets('containsSemantics', (WidgetTester tester) async { +/// final SemanticsHandle handle = tester.ensureSemantics(); +/// // ... +/// expect(tester.getSemantics(find.text('hello')), containsSemantics(label: 'hello')); +/// handle.dispose(); +/// }); /// ``` /// /// See also: /// -/// * [WidgetTester.getSemantics], the tester method which retrieves semantics. +/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. /// * [matchesSemantics], a similar matcher with default values for flags and actions. Matcher containsSemantics({ String? label, @@ -737,6 +846,8 @@ Matcher containsSemantics({ bool? hasToggledState, bool? isToggled, bool? hasImplicitScrolling, + bool? hasExpandedState, + bool? isExpanded, // Actions bool? hasTapAction, bool? hasLongPressAction, @@ -813,6 +924,8 @@ Matcher containsSemantics({ hasToggledState: hasToggledState, isToggled: isToggled, hasImplicitScrolling: hasImplicitScrolling, + hasExpandedState: hasExpandedState, + isExpanded: isExpanded, // Actions hasTapAction: hasTapAction, hasLongPressAction: hasLongPressAction, @@ -851,9 +964,12 @@ Matcher containsSemantics({ /// ## Sample code /// /// ```dart -/// final SemanticsHandle handle = tester.ensureSemantics(); -/// await expectLater(tester, meetsGuideline(textContrastGuideline)); -/// handle.dispose(); +/// testWidgets('containsSemantics', (WidgetTester tester) async { +/// final SemanticsHandle handle = tester.ensureSemantics(); +/// // ... +/// await expectLater(tester, meetsGuideline(textContrastGuideline)); +/// handle.dispose(); +/// }); /// ``` /// /// Supported accessibility guidelines: @@ -874,19 +990,19 @@ AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) { return _DoesNotMatchAccessibilityGuideline(guideline); } -class _FindsWidgetMatcher extends Matcher { - const _FindsWidgetMatcher(this.min, this.max); +class _FindsCountMatcher extends Matcher { + const _FindsCountMatcher(this.min, this.max); final int? min; final int? max; @override - bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { + bool matches(covariant FinderBase<dynamic> finder, Map<dynamic, dynamic> matchState) { assert(min != null || max != null); assert(min == null || max == null || min! <= max!); - matchState[Finder] = finder; + matchState[FinderBase] = finder; int count = 0; - final Iterator<Element> iterator = finder.evaluate().iterator; + final Iterator<dynamic> iterator = finder.evaluate().iterator; if (min != null) { while (count < min! && iterator.moveNext()) { count += 1; @@ -911,26 +1027,26 @@ class _FindsWidgetMatcher extends Matcher { assert(min != null || max != null); if (min == max) { if (min == 1) { - return description.add('exactly one matching node in the widget tree'); + return description.add('exactly one matching candidate'); } - return description.add('exactly $min matching nodes in the widget tree'); + return description.add('exactly $min matching candidates'); } if (min == null) { if (max == 0) { - return description.add('no matching nodes in the widget tree'); + return description.add('no matching candidates'); } if (max == 1) { - return description.add('at most one matching node in the widget tree'); + return description.add('at most one matching candidate'); } - return description.add('at most $max matching nodes in the widget tree'); + return description.add('at most $max matching candidates'); } if (max == null) { if (min == 1) { - return description.add('at least one matching node in the widget tree'); + return description.add('at least one matching candidate'); } - return description.add('at least $min matching nodes in the widget tree'); + return description.add('at least $min matching candidates'); } - return description.add('between $min and $max matching nodes in the widget tree (inclusive)'); + return description.add('between $min and $max matching candidates (inclusive)'); } @override @@ -940,8 +1056,8 @@ class _FindsWidgetMatcher extends Matcher { Map<dynamic, dynamic> matchState, bool verbose, ) { - final Finder finder = matchState[Finder] as Finder; - final int count = finder.evaluate().length; + final FinderBase<dynamic> finder = matchState[FinderBase] as FinderBase<dynamic>; + final int count = finder.found.length; if (count == 0) { assert(min != null && min! > 0); if (min == 1 && max == 1) { @@ -1344,6 +1460,14 @@ double _matrixDistance(Matrix4 a, Matrix4 b) { return delta; } +double _matrix3Distance(Matrix3 a, Matrix3 b) { + double delta = 0.0; + for (int i = 0; i < 9; i += 1) { + delta = math.max<double>((a[i] - b[i]).abs(), delta); + } + return delta; +} + double _sizeDistance(Size a, Size b) { // TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843 // ignore: unnecessary_parenthesis @@ -2017,10 +2141,13 @@ class _MatchesReferenceImage extends AsyncMatcher { @override Future<String?> matchAsync(dynamic item) async { Future<ui.Image> imageFuture; + final bool disposeImage; // set to true if the matcher created and owns the image and must therefore dispose it. if (item is Future<ui.Image>) { imageFuture = item; + disposeImage = false; } else if (item is ui.Image) { imageFuture = Future<ui.Image>.value(item); + disposeImage = false; } else { final Finder finder = item as Finder; final Iterable<Element> elements = finder.evaluate(); @@ -2030,30 +2157,37 @@ class _MatchesReferenceImage extends AsyncMatcher { return 'matched too many widgets'; } imageFuture = captureImage(elements.single); + disposeImage = true; } final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; return binding.runAsync<String?>(() async { final ui.Image image = await imageFuture; - final ByteData? bytes = await image.toByteData(); - if (bytes == null) { - return 'could not be encoded.'; - } + try { + final ByteData? bytes = await image.toByteData(); + if (bytes == null) { + return 'could not be encoded.'; + } - final ByteData? referenceBytes = await referenceImage.toByteData(); - if (referenceBytes == null) { - return 'could not have its reference image encoded.'; - } + final ByteData? referenceBytes = await referenceImage.toByteData(); + if (referenceBytes == null) { + return 'could not have its reference image encoded.'; + } - if (referenceImage.height != image.height || referenceImage.width != image.width) { - return 'does not match as width or height do not match. $image != $referenceImage'; - } + if (referenceImage.height != image.height || referenceImage.width != image.width) { + return 'does not match as width or height do not match. $image != $referenceImage'; + } - final int countDifferentPixels = _countDifferentPixels( - Uint8List.view(bytes.buffer), - Uint8List.view(referenceBytes.buffer), - ); - return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels'; + final int countDifferentPixels = _countDifferentPixels( + Uint8List.view(bytes.buffer), + Uint8List.view(referenceBytes.buffer), + ); + return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels'; + } finally { + if (disposeImage) { + image.dispose(); + } + } }); } @@ -2111,6 +2245,8 @@ class _MatchesSemanticsData extends Matcher { required bool? hasToggledState, required bool? isToggled, required bool? hasImplicitScrolling, + required bool? hasExpandedState, + required bool? isExpanded, // Actions required bool? hasTapAction, required bool? hasLongPressAction, @@ -2166,6 +2302,8 @@ class _MatchesSemanticsData extends Matcher { if (isToggled != null) SemanticsFlag.isToggled: isToggled, if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling, if (isSlider != null) SemanticsFlag.isSlider: isSlider, + if (hasExpandedState != null) SemanticsFlag.hasExpandedState: hasExpandedState, + if (isExpanded != null) SemanticsFlag.isExpanded: isExpanded, }, actions = <SemanticsAction, bool>{ if (hasTapAction != null) SemanticsAction.tap: hasTapAction, diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter_test/lib/src/mock_canvas.dart similarity index 82% rename from packages/flutter/test/rendering/mock_canvas.dart rename to packages/flutter_test/lib/src/mock_canvas.dart index 61a0aac9ef43e..48aab5a23fd31 100644 --- a/packages/flutter/test/rendering/mock_canvas.dart +++ b/packages/flutter_test/lib/src/mock_canvas.dart @@ -5,10 +5,16 @@ import 'dart:ui' as ui show Image, Paragraph; import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; +import 'package:matcher/expect.dart'; +import 'finders.dart'; import 'recording_canvas.dart'; +import 'test_async_utils.dart'; + +// Examples can assume: +// late RenderObject myRenderObject; +// late Symbol methodName; /// Matches objects or functions that paint a display list that matches the /// canvas calls described by the pattern. @@ -18,8 +24,8 @@ import 'recording_canvas.dart'; /// following signatures: /// /// ```dart -/// void function(PaintingContext context, Offset offset); -/// void function(Canvas canvas); +/// void exampleOne(PaintingContext context, Offset offset) { } +/// void exampleTwo(Canvas canvas) { } /// ``` /// /// In the case of functions that take a [PaintingContext] and an [Offset], the @@ -48,7 +54,8 @@ Matcher get paintsNothing => _TestRecordingCanvasPaintsNothingMatcher(); /// Matches objects or functions that assert when they try to paint. Matcher get paintsAssertion => _TestRecordingCanvasPaintsAssertionMatcher(); -/// Matches objects or functions that draw `methodName` exactly `count` number of times. +/// Matches objects or functions that draw `methodName` exactly `count` number +/// of times. Matcher paintsExactlyCountTimes(Symbol methodName, int count) { return _TestRecordingCanvasPaintsCountMatcher(methodName, count); } @@ -62,7 +69,9 @@ Matcher paintsExactlyCountTimes(Symbol methodName, int count) { /// literal syntax, for example: /// /// ```dart -/// if (methodName == #drawCircle) { ... } +/// if (methodName == #drawCircle) { +/// // ... +/// } /// ``` typedef PaintPatternPredicate = bool Function(Symbol methodName, List<dynamic> arguments); @@ -324,7 +333,8 @@ abstract class PaintPattern { /// /// Calls are skipped until a call to [Canvas.drawParagraph] is found. Any /// arguments that are passed to this method are compared to the actual - /// [Canvas.drawParagraph] call's argument, and any mismatches result in failure. + /// [Canvas.drawParagraph] call's argument, and any mismatches result in + /// failure. /// /// The `offset` argument can be either an [Offset] or a [Matcher]. If it is /// an [Offset] then the actual value must match the expected offset @@ -333,7 +343,8 @@ abstract class PaintPattern { /// assert that the actual offset is within a given distance from the expected /// offset. /// - /// If no call to [Canvas.drawParagraph] was made, then this results in failure. + /// If no call to [Canvas.drawParagraph] was made, then this results in + /// failure. void paragraph({ ui.Paragraph? paragraph, dynamic offset }); /// Indicates that a shadow is expected next. @@ -398,7 +409,8 @@ abstract class PaintPattern { /// Each method call after the last matched call (if any) will be passed to /// the given predicate, along with the values of its (positional) arguments. /// - /// For each one, the predicate must either return a boolean or throw a [String]. + /// For each one, the predicate must either return a boolean or throw a + /// [String]. /// /// If the predicate returns true, the call is considered a successful match /// and the next step in the pattern is examined. If this was the last step, @@ -421,7 +433,8 @@ abstract class PaintPattern { /// Each method call after the last matched call (if any) will be passed to /// the given predicate, along with the values of its (positional) arguments. /// - /// For each one, the predicate must either return a boolean or throw a [String]. + /// For each one, the predicate must either return a boolean or throw a + /// [String]. /// /// The predicate will be applied to each [Canvas] call until it returns false /// or all of the method calls have been tested. @@ -470,7 +483,8 @@ class _PathMatcher extends Matcher { if (errors.isEmpty) { return true; } - matchState[this] = 'Not all the given points were inside or outside the path as expected:\n ${errors.join("\n ")}'; + matchState[this] = 'Not all the given points were inside or outside the ' + 'path as expected:\n ${errors.join("\n ")}'; return false; } @@ -483,7 +497,8 @@ class _PathMatcher extends Matcher { } return '$count particular points'; } - return description.add('A Path that contains ${points(includes)} but does not contain ${points(excludes)}.'); + return description.add('A Path that contains ${points(includes)} but does ' + 'not contain ${points(excludes)}.'); } @override @@ -497,8 +512,8 @@ class _PathMatcher extends Matcher { } } -class _MismatchedCall { - const _MismatchedCall(this.message, this.callIntroduction, this.call); +class _MismatchedCall extends Error { + _MismatchedCall(this.message, this.callIntroduction, this.call); final String message; final String callIntroduction; final RecordedInvocation call; @@ -537,7 +552,8 @@ abstract class _TestRecordingCanvasMatcher extends Matcher { bool result = false; try { if (!_evaluatePainter(object, canvas, context)) { - matchState[this] = 'was not one of the supported objects for the "paints" matcher.'; + matchState[this] = 'was not one of the supported objects for the ' + '"paints" matcher.'; return false; } result = _evaluatePredicates(canvas.invocations, description); @@ -645,7 +661,8 @@ class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { bool result = false; try { if (!_evaluatePainter(object, canvas, context)) { - matchState[this] = 'was not one of the supported objects for the "paints" matcher.'; + matchState[this] = 'was not one of the supported objects for the ' + '"paints" matcher.'; return false; } prefixMessage = 'did not assert.'; @@ -823,8 +840,9 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp } if (_predicates.isEmpty) { description.writeln( - 'It painted something, but you must now add a pattern to the paints matcher ' - 'in the test to verify that it matches the important parts of the following.', + 'It painted something, but you must now add a pattern to the paints ' + 'matcher in the test to verify that it matches the important parts of ' + 'the following.', ); return false; } @@ -864,11 +882,12 @@ abstract class _PaintPredicate { others += 1; if (!call.moveNext()) { throw _MismatchedCall( - 'It called $others other method${ others == 1 ? "" : "s" } on the canvas, ' - 'the first of which was $firstCall, but did not ' - 'call ${_symbolName(symbol)}() at the time where $this was expected.', - 'The first method that was called when the call to ${_symbolName(symbol)}() ' - 'was expected, $firstCall, was called with the following stack:', + 'It called $others other method${ others == 1 ? "" : "s" } on the ' + 'canvas, the first of which was $firstCall, but did not call ' + '${_symbolName(symbol)}() at the time where $this was expected.', + 'The first method that was called when the call to ' + '${_symbolName(symbol)}() was expected, $firstCall, was called with ' + 'the following stack:', firstCall, ); } @@ -911,7 +930,11 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate { checkMethod(call, symbol); final int actualArgumentCount = call.current.invocation.positionalArguments.length; if (actualArgumentCount != argumentCount) { - throw 'It called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.'; + throw FlutterError( + 'It called $methodName with $actualArgumentCount ' + 'argument${actualArgumentCount == 1 ? "" : "s"}; expected ' + '$argumentCount.' + ); } verifyArguments(call.current.invocation.positionalArguments); call.moveNext(); @@ -922,23 +945,43 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate { void verifyArguments(List<dynamic> arguments) { final Paint paintArgument = arguments[paintArgumentIndex] as Paint; if (color != null && paintArgument.color != color) { - throw 'It called $methodName with a paint whose color, ${paintArgument.color}, was not exactly the expected color ($color).'; + throw FlutterError( + 'It called $methodName with a paint whose color, ' + '${paintArgument.color}, was not exactly the expected color ($color).' + ); } if (strokeWidth != null && paintArgument.strokeWidth != strokeWidth) { - throw 'It called $methodName with a paint whose strokeWidth, ${paintArgument.strokeWidth}, was not exactly the expected strokeWidth ($strokeWidth).'; + throw FlutterError( + 'It called $methodName with a paint whose strokeWidth, ' + '${paintArgument.strokeWidth}, was not exactly the expected ' + 'strokeWidth ($strokeWidth).' + ); } if (hasMaskFilter != null && (paintArgument.maskFilter != null) != hasMaskFilter) { if (hasMaskFilter!) { - throw 'It called $methodName with a paint that did not have a mask filter, despite expecting one.'; + throw FlutterError( + 'It called $methodName with a paint that did not have a mask filter, ' + 'despite expecting one.' + ); } else { - throw 'It called $methodName with a paint that did have a mask filter, despite not expecting one.'; + throw FlutterError( + 'It called $methodName with a paint that had a mask filter, ' + 'despite not expecting one.' + ); } } if (style != null && paintArgument.style != style) { - throw 'It called $methodName with a paint whose style, ${paintArgument.style}, was not exactly the expected style ($style).'; + throw FlutterError( + 'It called $methodName with a paint whose style, ' + '${paintArgument.style}, was not exactly the expected style ($style).' + ); } if (strokeCap != null && paintArgument.strokeCap != strokeCap) { - throw 'It called $methodName with a paint whose strokeCap, ${paintArgument.strokeCap}, was not exactly the expected strokeCap ($strokeCap).'; + throw FlutterError( + 'It called $methodName with a paint whose strokeCap, ' + '${paintArgument.strokeCap}, was not exactly the expected ' + 'strokeCap ($strokeCap).' + ); } } @@ -976,20 +1019,11 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate { Symbol symbol, String name, { required this.expected, - required Color? color, - required double? strokeWidth, - required bool? hasMaskFilter, - required PaintingStyle? style, - }) : super( - symbol, - name, - 2, - 1, - color: color, - strokeWidth: strokeWidth, - hasMaskFilter: hasMaskFilter, - style: style, - ); + required super.color, + required super.strokeWidth, + required super.hasMaskFilter, + required super.style, + }) : super(symbol, name, 2, 1); final T? expected; @@ -998,7 +1032,10 @@ class _OneParameterPaintPredicate<T> extends _DrawCommandPaintPredicate { super.verifyArguments(arguments); final T actual = arguments[0] as T; if (expected != null && actual != expected) { - throw 'It called $methodName with $T, $actual, which was not exactly the expected $T ($expected).'; + throw FlutterError( + 'It called $methodName with $T, $actual, which was not exactly the ' + 'expected $T ($expected).' + ); } } @@ -1021,20 +1058,11 @@ class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate { String name, { required this.expected1, required this.expected2, - required Color? color, - required double? strokeWidth, - required bool? hasMaskFilter, - required PaintingStyle? style, - }) : super( - symbol, - name, - 3, - 2, - color: color, - strokeWidth: strokeWidth, - hasMaskFilter: hasMaskFilter, - style: style, - ); + required super.color, + required super.strokeWidth, + required super.hasMaskFilter, + required super.style, + }) : super(symbol, name, 3, 2); final T1? expected1; @@ -1045,11 +1073,17 @@ class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate { super.verifyArguments(arguments); final T1 actual1 = arguments[0] as T1; if (expected1 != null && actual1 != expected1) { - throw 'It called $methodName with its first argument (a $T1), $actual1, which was not exactly the expected $T1 ($expected1).'; + throw FlutterError( + 'It called $methodName with its first argument (a $T1), $actual1, ' + 'which was not exactly the expected $T1 ($expected1).' + ); } final T2 actual2 = arguments[1] as T2; if (expected2 != null && actual2 != expected2) { - throw 'It called $methodName with its second argument (a $T2), $actual2, which was not exactly the expected $T2 ($expected2).'; + throw FlutterError( + 'It called $methodName with its second argument (a $T2), $actual2, ' + 'which was not exactly the expected $T2 ($expected2).' + ); } } @@ -1074,28 +1108,23 @@ class _TwoParameterPaintPredicate<T1, T2> extends _DrawCommandPaintPredicate { } class _RectPaintPredicate extends _OneParameterPaintPredicate<Rect> { - _RectPaintPredicate({ Rect? rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawRect, - 'a rectangle', - expected: rect, - color: color, - strokeWidth: strokeWidth, - hasMaskFilter: hasMaskFilter, - style: style, - ); + _RectPaintPredicate({ + Rect? rect, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawRect, 'a rectangle', expected: rect); } class _RRectPaintPredicate extends _DrawCommandPaintPredicate { - _RRectPaintPredicate({ this.rrect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawRRect, - 'a rounded rectangle', - 2, - 1, - color: color, - strokeWidth: strokeWidth, - hasMaskFilter: hasMaskFilter, - style: style, - ); + _RRectPaintPredicate({ + this.rrect, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawRRect, 'a rounded rectangle', 2, 1); final RRect? rrect; @@ -1117,7 +1146,10 @@ class _RRectPaintPredicate extends _DrawCommandPaintPredicate { (actual.tlRadiusY - rrect!.tlRadiusY).abs() > eps || (actual.trRadiusX - rrect!.trRadiusX).abs() > eps || (actual.trRadiusY - rrect!.trRadiusY).abs() > eps)) { - throw 'It called $methodName with RRect, $actual, which was not exactly the expected RRect ($rrect).'; + throw FlutterError( + 'It called $methodName with RRect, $actual, which was not exactly the ' + 'expected RRect ($rrect).' + ); } } @@ -1131,22 +1163,26 @@ class _RRectPaintPredicate extends _DrawCommandPaintPredicate { } class _DRRectPaintPredicate extends _TwoParameterPaintPredicate<RRect, RRect> { - _DRRectPaintPredicate({ RRect? inner, RRect? outer, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawDRRect, - 'a rounded rectangle outline', - expected1: outer, - expected2: inner, - color: color, - strokeWidth: strokeWidth, - hasMaskFilter: hasMaskFilter, - style: style, - ); + _DRRectPaintPredicate({ + RRect? inner, + RRect? outer, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawDRRect, 'a rounded rectangle outline', expected1: outer, expected2: inner); } class _CirclePaintPredicate extends _DrawCommandPaintPredicate { - _CirclePaintPredicate({ this.x, this.y, this.radius, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawCircle, 'a circle', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, - ); + _CirclePaintPredicate({ + this.x, + this.y, + this.radius, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawCircle, 'a circle', 3, 2); final double? x; final double? y; @@ -1159,19 +1195,34 @@ class _CirclePaintPredicate extends _DrawCommandPaintPredicate { if (x != null && y != null) { final Offset point = Offset(x!, y!); if (point != pointArgument) { - throw 'It called $methodName with a center coordinate, $pointArgument, which was not exactly the expected coordinate ($point).'; + throw FlutterError( + 'It called $methodName with a center coordinate, $pointArgument, ' + 'which was not exactly the expected coordinate ($point).' + ); } } else { if (x != null && pointArgument.dx != x) { - throw 'It called $methodName with a center coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).'; + throw FlutterError( + 'It called $methodName with a center coordinate, $pointArgument, ' + 'whose x-coordinate was not exactly the expected coordinate ' + '(${x!.toStringAsFixed(1)}).' + ); } if (y != null && pointArgument.dy != y) { - throw 'It called $methodName with a center coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).'; + throw FlutterError( + 'It called $methodName with a center coordinate, $pointArgument, ' + 'whose y-coordinate was not exactly the expected coordinate ' + '(${y!.toStringAsFixed(1)}).' + ); } } final double radiusArgument = arguments[1] as double; if (radius != null && radiusArgument != radius) { - throw 'It called $methodName with radius, ${radiusArgument.toStringAsFixed(1)}, which was not exactly the expected radius (${radius!.toStringAsFixed(1)}).'; + throw FlutterError( + 'It called $methodName with radius, ' + '${radiusArgument.toStringAsFixed(1)}, which was not exactly the ' + 'expected radius (${radius!.toStringAsFixed(1)}).' + ); } } @@ -1195,9 +1246,14 @@ class _CirclePaintPredicate extends _DrawCommandPaintPredicate { } class _PathPaintPredicate extends _DrawCommandPaintPredicate { - _PathPaintPredicate({ this.includes, this.excludes, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawPath, 'a path', 2, 1, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, - ); + _PathPaintPredicate({ + this.includes, + this.excludes, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawPath, 'a path', 2, 1); final Iterable<Offset>? includes; final Iterable<Offset>? excludes; @@ -1209,14 +1265,20 @@ class _PathPaintPredicate extends _DrawCommandPaintPredicate { if (includes != null) { for (final Offset offset in includes!) { if (!pathArgument.contains(offset)) { - throw 'It called $methodName with a path that unexpectedly did not contain $offset.'; + throw FlutterError( + 'It called $methodName with a path that unexpectedly did not ' + 'contain $offset.' + ); } } } if (excludes != null) { for (final Offset offset in excludes!) { if (pathArgument.contains(offset)) { - throw 'It called $methodName with a path that unexpectedly contained $offset.'; + throw FlutterError( + 'It called $methodName with a path that unexpectedly contained ' + '$offset.' + ); } } } @@ -1237,9 +1299,14 @@ class _PathPaintPredicate extends _DrawCommandPaintPredicate { // TODO(ianh): add arguments to test the length, angle, that kind of thing class _LinePaintPredicate extends _DrawCommandPaintPredicate { - _LinePaintPredicate({ this.p1, this.p2, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawLine, 'a line', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, - ); + _LinePaintPredicate({ + this.p1, + this.p2, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawLine, 'a line', 3, 2); final Offset? p1; final Offset? p2; @@ -1248,15 +1315,23 @@ class _LinePaintPredicate extends _DrawCommandPaintPredicate { void verifyArguments(List<dynamic> arguments) { super.verifyArguments(arguments); // Checks the 3rd argument, a Paint if (arguments.length != 3) { - throw 'It called $methodName with ${arguments.length} arguments; expected 3.'; + throw FlutterError( + 'It called $methodName with ${arguments.length} arguments; expected 3.' + ); } final Offset p1Argument = arguments[0] as Offset; final Offset p2Argument = arguments[1] as Offset; if (p1 != null && p1Argument != p1) { - throw 'It called $methodName with p1 endpoint, $p1Argument, which was not exactly the expected endpoint ($p1).'; + throw FlutterError( + 'It called $methodName with p1 endpoint, $p1Argument, which was not ' + 'exactly the expected endpoint ($p1).' + ); } if (p2 != null && p2Argument != p2) { - throw 'It called $methodName with p2 endpoint, $p2Argument, which was not exactly the expected endpoint ($p2).'; + throw FlutterError( + 'It called $methodName with p2 endpoint, $p2Argument, which was not ' + 'exactly the expected endpoint ($p2).' + ); } } @@ -1273,9 +1348,14 @@ class _LinePaintPredicate extends _DrawCommandPaintPredicate { } class _ArcPaintPredicate extends _DrawCommandPaintPredicate { - _ArcPaintPredicate({ this.rect, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style, StrokeCap? strokeCap }) : super( - #drawArc, 'an arc', 5, 4, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, strokeCap: strokeCap, - ); + _ArcPaintPredicate({ + this.rect, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + super.strokeCap, + }) : super(#drawArc, 'an arc', 5, 4); final Rect? rect; @@ -1284,7 +1364,10 @@ class _ArcPaintPredicate extends _DrawCommandPaintPredicate { super.verifyArguments(arguments); final Rect rectArgument = arguments[0] as Rect; if (rect != null && rectArgument != rect) { - throw 'It called $methodName with a paint whose rect, $rectArgument, was not exactly the expected rect ($rect).'; + throw FlutterError( + 'It called $methodName with a paint whose rect, $rectArgument, was not ' + 'exactly the expected rect ($rect).' + ); } } @@ -1312,34 +1395,52 @@ class _ShadowPredicate extends _PaintPredicate { @protected void verifyArguments(List<dynamic> arguments) { if (arguments.length != 4) { - throw 'It called $methodName with ${arguments.length} arguments; expected 4.'; + throw FlutterError( + 'It called $methodName with ${arguments.length} arguments; expected 4.' + ); } final Path pathArgument = arguments[0] as Path; if (includes != null) { for (final Offset offset in includes!) { if (!pathArgument.contains(offset)) { - throw 'It called $methodName with a path that unexpectedly did not contain $offset.'; + throw FlutterError( + 'It called $methodName with a path that unexpectedly did not ' + 'contain $offset.' + ); } } } if (excludes != null) { for (final Offset offset in excludes!) { if (pathArgument.contains(offset)) { - throw 'It called $methodName with a path that unexpectedly contained $offset.'; + throw FlutterError( + 'It called $methodName with a path that unexpectedly contained ' + '$offset.' + ); } } } final Color actualColor = arguments[1] as Color; if (color != null && actualColor != color) { - throw 'It called $methodName with a color, $actualColor, which was not exactly the expected color ($color).'; + throw FlutterError( + 'It called $methodName with a color, $actualColor, which was not ' + 'exactly the expected color ($color).' + ); } final double actualElevation = arguments[2] as double; if (elevation != null && actualElevation != elevation) { - throw 'It called $methodName with an elevation, $actualElevation, which was not exactly the expected value ($elevation).'; + throw FlutterError( + 'It called $methodName with an elevation, $actualElevation, which was ' + 'not exactly the expected value ($elevation).' + ); } final bool actualTransparentOccluder = arguments[3] as bool; if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder) { - throw 'It called $methodName with a transparentOccluder value, $actualTransparentOccluder, which was not exactly the expected value ($transparentOccluder).'; + throw FlutterError( + 'It called $methodName with a transparentOccluder value, ' + '$actualTransparentOccluder, which was not exactly the expected value ' + '($transparentOccluder).' + ); } } @@ -1383,9 +1484,15 @@ class _ShadowPredicate extends _PaintPredicate { } class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate { - _DrawImagePaintPredicate({ this.image, this.x, this.y, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawImage, 'an image', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, - ); + _DrawImagePaintPredicate({ + this.image, + this.x, + this.y, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawImage, 'an image', 3, 2); final ui.Image? image; final double? x; @@ -1396,20 +1503,34 @@ class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate { super.verifyArguments(arguments); final ui.Image imageArgument = arguments[0] as ui.Image; if (image != null && !image!.isCloneOf(imageArgument)) { - throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).'; + throw FlutterError( + 'It called $methodName with an image, $imageArgument, which was not ' + 'exactly the expected image ($image).' + ); } final Offset pointArgument = arguments[0] as Offset; if (x != null && y != null) { final Offset point = Offset(x!, y!); if (point != pointArgument) { - throw 'It called $methodName with an offset coordinate, $pointArgument, which was not exactly the expected coordinate ($point).'; + throw FlutterError( + 'It called $methodName with an offset coordinate, $pointArgument, ' + 'which was not exactly the expected coordinate ($point).' + ); } } else { if (x != null && pointArgument.dx != x) { - throw 'It called $methodName with an offset coordinate, $pointArgument, whose x-coordinate not exactly the expected coordinate (${x!.toStringAsFixed(1)}).'; + throw FlutterError( + 'It called $methodName with an offset coordinate, $pointArgument, ' + 'whose x-coordinate was not exactly the expected coordinate ' + '(${x!.toStringAsFixed(1)}).' + ); } if (y != null && pointArgument.dy != y) { - throw 'It called $methodName with an offset coordinate, $pointArgument, whose y-coordinate not exactly the expected coordinate (${y!.toStringAsFixed(1)}).'; + throw FlutterError( + 'It called $methodName with an offset coordinate, $pointArgument, ' + 'whose y-coordinate was not exactly the expected coordinate ' + '(${y!.toStringAsFixed(1)}).' + ); } } } @@ -1434,9 +1555,15 @@ class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate { } class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate { - _DrawImageRectPaintPredicate({ this.image, this.source, this.destination, Color? color, double? strokeWidth, bool? hasMaskFilter, PaintingStyle? style }) : super( - #drawImageRect, 'an image', 4, 3, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style, - ); + _DrawImageRectPaintPredicate({ + this.image, + this.source, + this.destination, + super.color, + super.strokeWidth, + super.hasMaskFilter, + super.style, + }) : super(#drawImageRect, 'an image', 4, 3); final ui.Image? image; final Rect? source; @@ -1447,15 +1574,25 @@ class _DrawImageRectPaintPredicate extends _DrawCommandPaintPredicate { super.verifyArguments(arguments); final ui.Image imageArgument = arguments[0] as ui.Image; if (image != null && !image!.isCloneOf(imageArgument)) { - throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).'; + throw FlutterError( + 'It called $methodName with an image, $imageArgument, which was not ' + 'exactly the expected image ($image).' + ); } final Rect sourceArgument = arguments[1] as Rect; if (source != null && sourceArgument != source) { - throw 'It called $methodName with a source rectangle, $sourceArgument, which was not exactly the expected rectangle ($source).'; + throw FlutterError( + 'It called $methodName with a source rectangle, $sourceArgument, which ' + 'was not exactly the expected rectangle ($source).' + ); } final Rect destinationArgument = arguments[2] as Rect; if (destination != null && destinationArgument != destination) { - throw 'It called $methodName with a destination rectangle, $destinationArgument, which was not exactly the expected rectangle ($destination).'; + throw FlutterError( + 'It called $methodName with a destination rectangle, ' + '$destinationArgument, which was not exactly the expected rectangle ' + '($destination).' + ); } } @@ -1485,12 +1622,17 @@ class _SomethingPaintPredicate extends _PaintPredicate { bool testedAllCalls = false; do { if (testedAllCalls) { - throw 'It painted methods that the predicate passed to a "something" step, ' - 'in the paint pattern, none of which were considered correct.'; + throw FlutterError( + 'It painted methods that the predicate passed to a "something" step, ' + 'in the paint pattern, none of which were considered correct.' + ); } currentCall = call.current; if (!currentCall.invocation.isMethod) { - throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call'; + throw FlutterError( + 'It called $currentCall, which was not a method, when the paint ' + 'pattern expected a method call' + ); } testedAllCalls = !call.moveNext(); } while (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments)); @@ -1500,8 +1642,10 @@ class _SomethingPaintPredicate extends _PaintPredicate { try { return predicate(methodName, arguments); } on String catch (s) { - throw 'It painted something that the predicate passed to a "something" step ' - 'in the paint pattern considered incorrect:\n $s\n '; + throw FlutterError( + 'It painted something that the predicate passed to a "something" step ' + 'in the paint pattern considered incorrect:\n $s\n ' + ); } } @@ -1519,11 +1663,16 @@ class _EverythingPaintPredicate extends _PaintPredicate { do { final RecordedInvocation currentCall = call.current; if (!currentCall.invocation.isMethod) { - throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call'; + throw FlutterError( + 'It called $currentCall, which was not a method, when the paint ' + 'pattern expected a method call' + ); } if (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments)) { - throw 'It painted something that the predicate passed to an "everything" step ' - 'in the paint pattern considered incorrect.\n'; + throw FlutterError( + 'It painted something that the predicate passed to an "everything" ' + 'step in the paint pattern considered incorrect.\n' + ); } } while (call.moveNext()); } @@ -1532,8 +1681,10 @@ class _EverythingPaintPredicate extends _PaintPredicate { try { return predicate(methodName, arguments); } on String catch (s) { - throw 'It painted something that the predicate passed to an "everything" step ' - 'in the paint pattern considered incorrect:\n $s\n '; + throw FlutterError( + 'It painted something that the predicate passed to an "everything" step ' + 'in the paint pattern considered incorrect:\n $s\n ' + ); } } @@ -1552,7 +1703,11 @@ class _FunctionPaintPredicate extends _PaintPredicate { void match(Iterator<RecordedInvocation> call) { checkMethod(call, symbol); if (call.current.invocation.positionalArguments.length != arguments.length) { - throw 'It called ${_symbolName(symbol)} with ${call.current.invocation.positionalArguments.length} arguments; expected ${arguments.length}.'; + throw FlutterError( + 'It called ${_symbolName(symbol)} with ' + '${call.current.invocation.positionalArguments.length} arguments; ' + 'expected ${arguments.length}.' + ); } for (int index = 0; index < arguments.length; index += 1) { final dynamic actualArgument = call.current.invocation.positionalArguments[index]; @@ -1561,7 +1716,11 @@ class _FunctionPaintPredicate extends _PaintPredicate { if (desiredArgument is Matcher) { expect(actualArgument, desiredArgument); } else if (desiredArgument != null && desiredArgument != actualArgument) { - throw 'It called ${_symbolName(symbol)} with argument $index having value ${_valueName(actualArgument)} when ${_valueName(desiredArgument)} was expected.'; + throw FlutterError( + 'It called ${_symbolName(symbol)} with argument $index having value ' + '${_valueName(actualArgument)} when ${_valueName(desiredArgument)} ' + 'was expected.' + ); } } call.moveNext(); @@ -1584,7 +1743,10 @@ class _SaveRestorePairPaintPredicate extends _PaintPredicate { int depth = 1; while (depth > 0) { if (!call.moveNext()) { - throw 'It did not have a matching restore() for the save() that was found where $this was expected.'; + throw FlutterError( + 'It did not have a matching restore() for the save() that was found ' + 'where $this was expected.' + ); } if (call.current.invocation.isMethod) { if (call.current.invocation.memberName == #save) { diff --git a/packages/flutter_test/lib/src/nonconst.dart b/packages/flutter_test/lib/src/nonconst.dart index c3b9d34660fd2..561f933f5fb91 100644 --- a/packages/flutter_test/lib/src/nonconst.dart +++ b/packages/flutter_test/lib/src/nonconst.dart @@ -8,15 +8,15 @@ /// ```dart /// class A { /// const A(this.i); -/// int i; +/// final int? i; /// } /// -/// main () { +/// void main () { /// // prevent prefer_const_constructors lint /// A(nonconst(null)); /// /// // prevent prefer_const_declarations lint -/// final int $null = nonconst(null); +/// final int? $null = nonconst(null); /// final A a = nonconst(const A(null)); /// } /// ``` diff --git a/packages/flutter/test/rendering/recording_canvas.dart b/packages/flutter_test/lib/src/recording_canvas.dart similarity index 93% rename from packages/flutter/test/rendering/recording_canvas.dart rename to packages/flutter_test/lib/src/recording_canvas.dart index 62031fd07fbe5..c958006a499bf 100644 --- a/packages/flutter/test/rendering/recording_canvas.dart +++ b/packages/flutter_test/lib/src/recording_canvas.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; /// An [Invocation] and the [stack] trace that led to it. @@ -26,7 +27,8 @@ class RecordedInvocation { @override String toString() => _describeInvocation(invocation); - /// Converts [stack] to a string using the [FlutterError.defaultStackFilter] logic. + /// Converts [stack] to a string using the [FlutterError.defaultStackFilter] + /// logic. String stackToString({ String indent = '' }) { return indent + FlutterError.defaultStackFilter( stack.toString().trimRight().split('\n'), @@ -34,6 +36,9 @@ class RecordedInvocation { } } +// Examples can assume: +// late WidgetTester tester; + /// A [Canvas] for tests that records its method calls. /// /// This class can be used in conjunction with [TestRecordingPaintingContext] @@ -92,6 +97,8 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex /// Creates a [PaintingContext] for tests that use [TestRecordingCanvas]. TestRecordingPaintingContext(this.canvas); + final List<OpacityLayer> _createdLayers = <OpacityLayer>[]; + @override final Canvas canvas; @@ -166,7 +173,18 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex canvas.saveLayer(null, Paint()); // TODO(ianh): Expose the alpha somewhere. painter(this, offset); canvas.restore(); - return OpacityLayer(); + final OpacityLayer layer = OpacityLayer(); + _createdLayers.add(layer); + return layer; + } + + /// Releases allocated resources. + @mustCallSuper + void dispose() { + for (final OpacityLayer layer in _createdLayers) { + layer.dispose(); + } + _createdLayers.clear(); } @override diff --git a/packages/flutter_test/lib/src/test_async_utils.dart b/packages/flutter_test/lib/src/test_async_utils.dart index 9bdeebe110e01..3d915af565af2 100644 --- a/packages/flutter_test/lib/src/test_async_utils.dart +++ b/packages/flutter_test/lib/src/test_async_utils.dart @@ -12,6 +12,9 @@ class _AsyncScope { final Zone zone; } +// Examples can assume: +// late WidgetTester tester; + /// Utility class for all the async APIs in the `flutter_test` library. /// /// This class provides checking for asynchronous APIs, allowing the library to diff --git a/packages/flutter_test/lib/src/test_default_binary_messenger.dart b/packages/flutter_test/lib/src/test_default_binary_messenger.dart index f5c4e2f529369..25647bc9d90f3 100644 --- a/packages/flutter_test/lib/src/test_default_binary_messenger.dart +++ b/packages/flutter_test/lib/src/test_default_binary_messenger.dart @@ -47,8 +47,6 @@ typedef AllMessagesHandler = Future<ByteData?>? Function( /// Listeners for these messages are configured using [setMessageHandler]. class TestDefaultBinaryMessenger extends BinaryMessenger { /// Creates a [TestDefaultBinaryMessenger] instance. - /// - /// The [delegate] instance must not be null. TestDefaultBinaryMessenger( this.delegate, { Map<String, MessageHandler> outboundHandlers = const <String, MessageHandler>{}, diff --git a/packages/flutter_test/lib/src/test_text_input_key_handler.dart b/packages/flutter_test/lib/src/test_text_input_key_handler.dart index 410f9d16616a3..7dba935d65794 100644 --- a/packages/flutter_test/lib/src/test_text_input_key_handler.dart +++ b/packages/flutter_test/lib/src/test_text_input_key_handler.dart @@ -50,6 +50,8 @@ class MacOSTestTextInputKeyHandler extends TestTextInputKeyHandler { alt: true, shift: pressShift): <String>['deleteWordBackward:'], SingleActivator(LogicalKeyboardKey.backspace, meta: true, shift: pressShift): <String>['deleteToBeginningOfLine:'], + SingleActivator(LogicalKeyboardKey.backspace, control: true, shift: pressShift): + <String>['deleteBackwardByDecomposingPreviousCharacter:'], SingleActivator(LogicalKeyboardKey.delete, shift: pressShift): <String>[ 'deleteForward:' ], diff --git a/packages/flutter_test/lib/src/tree_traversal.dart b/packages/flutter_test/lib/src/tree_traversal.dart new file mode 100644 index 0000000000000..5ae34e797c713 --- /dev/null +++ b/packages/flutter_test/lib/src/tree_traversal.dart @@ -0,0 +1,156 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/widgets.dart'; + +/// Provides an iterable that efficiently returns all the [Element]s +/// rooted at the given [Element]. See [CachingIterable] for details. +/// +/// This function must be called again if the tree changes. You cannot +/// call this function once, then reuse the iterable after having +/// changed the state of the tree, because the iterable returned by +/// this function caches the results and only walks the tree once. +/// +/// The same applies to any iterable obtained indirectly through this +/// one, for example the results of calling `where` on this iterable +/// are also cached. +Iterable<Element> collectAllElementsFrom( + Element rootElement, { + required bool skipOffstage, +}) { + return CachingIterable<Element>(_DepthFirstElementTreeIterator(rootElement, !skipOffstage)); +} + +/// Provides an iterable that efficiently returns all the [SemanticsNode]s +/// rooted at the given [SemanticsNode]. See [CachingIterable] for details. +/// +/// By default, this will traverse the semantics tree in semantic traversal +/// order, but the traversal order can be changed by passing in a different +/// value to `order`. +/// +/// This function must be called again if the semantics change. You cannot call +/// this function once, then reuse the iterable after having changed the state +/// of the tree, because the iterable returned by this function caches the +/// results and only walks the tree once. +/// +/// The same applies to any iterable obtained indirectly through this +/// one, for example the results of calling `where` on this iterable +/// are also cached. +Iterable<SemanticsNode> collectAllSemanticsNodesFrom( + SemanticsNode root, { + DebugSemanticsDumpOrder order = DebugSemanticsDumpOrder.traversalOrder, + }) { + return CachingIterable<SemanticsNode>(_DepthFirstSemanticsTreeIterator(root, order)); +} + +/// Provides a recursive, efficient, depth first search of a tree. +/// +/// This iterator executes a depth first search as an iterable, and iterates in +/// a left to right order: +/// +/// 1 +/// / \ +/// 2 3 +/// / \ / \ +/// 4 5 6 7 +/// +/// Will iterate in order 2, 4, 5, 3, 6, 7. The given root element is not +/// included in the traversal. +abstract class _DepthFirstTreeIterator<ItemType> implements Iterator<ItemType> { + _DepthFirstTreeIterator(ItemType root) { + _fillStack(_collectChildren(root)); + } + + @override + ItemType get current => _current!; + late ItemType _current; + + final List<ItemType> _stack = <ItemType>[]; + + @override + bool moveNext() { + if (_stack.isEmpty) { + return false; + } + + _current = _stack.removeLast(); + _fillStack(_collectChildren(_current)); + return true; + } + + /// Fills the stack in such a way that the next element of a depth first + /// traversal is easily and efficiently accessible when calling `moveNext`. + void _fillStack(List<ItemType> children) { + // We reverse the list of children so we don't have to do use expensive + // `insert` or `remove` operations, and so the order of the traversal + // is depth first when built lazily through the iterator. + // + // This is faster than `_stack.addAll(children.reversed)`, presumably since + // we don't actually care about maintaining an iteration pointer. + while (children.isNotEmpty) { + _stack.add(children.removeLast()); + } + } + + /// Collect the children from [root] in the order they are expected to traverse. + List<ItemType> _collectChildren(ItemType root); +} + +/// [Element.visitChildren] does not guarantee order, but does guarantee stable +/// order. This iterator also guarantees stable order, and iterates in a left +/// to right order: +/// +/// 1 +/// / \ +/// 2 3 +/// / \ / \ +/// 4 5 6 7 +/// +/// Will iterate in order 2, 4, 5, 3, 6, 7. +/// +/// Performance is important here because this class is on the critical path +/// for flutter_driver and package:integration_test performance tests. +/// Performance is measured in the all_elements_bench microbenchmark. +/// Any changes to this implementation should check the before and after numbers +/// on that benchmark to avoid regressions in general performance test overhead. +/// +/// If we could use RTL order, we could save on performance, but numerous tests +/// have been written (and developers clearly expect) that LTR order will be +/// respected. +class _DepthFirstElementTreeIterator extends _DepthFirstTreeIterator<Element> { + _DepthFirstElementTreeIterator(super.root, this.includeOffstage); + + final bool includeOffstage; + + @override + List<Element> _collectChildren(Element root) { + final List<Element> children = <Element>[]; + if (includeOffstage) { + root.visitChildren(children.add); + } else { + root.debugVisitOnstageChildren(children.add); + } + + return children; + } +} + +/// Iterates the semantics tree starting at the given `root`. +/// +/// This will iterate in the same order expected from accessibility services, +/// so the results can be used to simulate the same traversal the engine will +/// make. The results are not filtered based on flags or visibility, so they +/// will need to be further filtered to fully simulate an accessiblity service. +class _DepthFirstSemanticsTreeIterator extends _DepthFirstTreeIterator<SemanticsNode> { + _DepthFirstSemanticsTreeIterator(super.root, this.order); + + final DebugSemanticsDumpOrder order; + + @override + List<SemanticsNode> _collectChildren(SemanticsNode root) { + return root.debugListChildrenInOrder(order); + } +} diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 8cd6d50cb0234..2a8c36dcc7830 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -13,7 +13,6 @@ import 'package:matcher/expect.dart' as matcher_expect; import 'package:meta/meta.dart'; import 'package:test_api/scaffolding.dart' as test_package; -import 'all_elements.dart'; import 'binding.dart'; import 'controller.dart'; import 'finders.dart'; @@ -23,6 +22,7 @@ import 'test_async_utils.dart'; import 'test_compat.dart'; import 'test_pointer.dart'; import 'test_text_input.dart'; +import 'tree_traversal.dart'; // Keep users from needing multiple imports to test semantics. export 'package:flutter/rendering.dart' show SemanticsHandle; @@ -81,6 +81,9 @@ E? _lastWhereOrNull<E>(Iterable<E> list, bool Function(E) test) { return null; } +// Examples can assume: +// typedef MyWidget = Placeholder; + /// Runs the [callback] inside the Flutter test environment. /// /// Use this function for testing custom [StatelessWidget]s and @@ -117,7 +120,7 @@ E? _lastWhereOrNull<E>(Iterable<E> list, bool Function(E) test) { /// /// ```dart /// testWidgets('MyWidget', (WidgetTester tester) async { -/// await tester.pumpWidget(MyWidget()); +/// await tester.pumpWidget(const MyWidget()); /// await tester.tap(find.text('Save')); /// expect(find.text('Success'), findsOneWidget); /// }); @@ -319,12 +322,13 @@ class TargetPlatformVariant extends TestVariant<TargetPlatform> { /// } /// /// final ValueVariant<TestScenario> variants = ValueVariant<TestScenario>( -/// <TestScenario>{value1, value2}, +/// <TestScenario>{TestScenario.value1, TestScenario.value2}, /// ); -/// -/// testWidgets('Test handling of TestScenario', (WidgetTester tester) { -/// expect(variants.currentValue, equals(value1)); -/// }, variant: variants); +/// void main() { +/// testWidgets('Test handling of TestScenario', (WidgetTester tester) async { +/// expect(variants.currentValue, equals(TestScenario.value1)); +/// }, variant: variants); +/// } /// ``` /// {@end-tool} class ValueVariant<T> extends TestVariant<T> { @@ -507,7 +511,7 @@ Future<void> expectLater( /// /// ```dart /// testWidgets('MyWidget', (WidgetTester tester) async { -/// await tester.pumpWidget(MyWidget()); +/// await tester.pumpWidget(const MyWidget()); /// await tester.tap(find.text('Save')); /// await tester.pump(); // allow the application to handle /// await tester.pump(const Duration(seconds: 1)); // skip past the animation @@ -555,7 +559,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// {@tool snippet} /// ```dart /// testWidgets('MyWidget asserts invalid bounds', (WidgetTester tester) async { - /// await tester.pumpWidget(MyWidget(-1)); + /// await tester.pumpWidget(const MyWidget()); /// expect(tester.takeException(), isAssertionError); // or isNull, as appropriate. /// }); /// ``` @@ -569,24 +573,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker EnginePhase phase = EnginePhase.sendSemanticsUpdate, ]) { return TestAsyncUtils.guard<void>(() { - return _pumpWidget( - binding.wrapWithDefaultView(widget), - duration, - phase, - ); + binding.attachRootWidget(binding.wrapWithDefaultView(widget)); + binding.scheduleFrame(); + return binding.pump(duration, phase); }); } - Future<void> _pumpWidget( - Widget widget, [ - Duration? duration, - EnginePhase phase = EnginePhase.sendSemanticsUpdate, - ]) { - binding.attachRootWidget(widget); - binding.scheduleFrame(); - return binding.pump(duration, phase); - } - @override Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) { assert(records.isNotEmpty); @@ -745,12 +737,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker 'your widget tree in a RootRestorationScope?', ); return TestAsyncUtils.guard<void>(() async { - final Widget widget = ((binding.rootElement! as RenderObjectToWidgetElement<RenderObject>).widget as RenderObjectToWidgetAdapter<RenderObject>).child!; + final RootWidget widget = binding.rootElement!.widget as RootWidget; final TestRestorationData restorationData = binding.restorationManager.restorationData; runApp(Container(key: UniqueKey())); await pump(); binding.restorationManager.restoreFrom(restorationData); - return _pumpWidget(widget); + binding.attachToBuildOwner(widget); + binding.scheduleFrame(); + return binding.pump(); }); } @@ -837,9 +831,11 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker bool get hasRunningAnimations => binding.transientCallbackCount > 0; @override - HitTestResult hitTestOnBinding(Offset location) { - location = binding.localToGlobal(location, binding.renderView); - return super.hitTestOnBinding(location); + HitTestResult hitTestOnBinding(Offset location, {int? viewId}) { + viewId ??= view.viewId; + final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView.viewId == viewId); + location = binding.localToGlobal(location, renderView); + return super.hitTestOnBinding(location, viewId: viewId); } @override @@ -861,10 +857,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker .map((HitTestEntry candidate) => candidate.target) .whereType<RenderObject>() .first; - final Element? innerTargetElement = _lastWhereOrNull( - collectAllElementsFrom(binding.rootElement!, skipOffstage: true), - (Element element) => element.renderObject == innerTarget, - ); + final Element? innerTargetElement = binding.renderViews.contains(innerTarget) + ? null + : _lastWhereOrNull( + collectAllElementsFrom(binding.rootElement!, skipOffstage: true), + (Element element) => element.renderObject == innerTarget, + ); if (innerTargetElement == null) { printToConsole('No widgets found at ${event.position}.'); return; @@ -1060,6 +1058,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker int? _lastRecordedSemanticsHandles; + // TODO(goderbauer): Only use binding.debugOutstandingSemanticsHandles when deprecated binding.pipelineOwner is removed. + // ignore: deprecated_member_use int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles; void _recordNumberOfSemanticsHandles() { @@ -1089,12 +1089,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// /// Tests that just need to add text to widgets like [TextField] /// or [TextFormField] only need to call [enterText]. - Future<void> showKeyboard(Finder finder) async { + Future<void> showKeyboard(FinderBase<Element> finder) async { + bool skipOffstage = true; + if (finder is Finder) { + skipOffstage = finder.skipOffstage; + } return TestAsyncUtils.guard<void>(() async { final EditableTextState editable = state<EditableTextState>( find.descendant( of: finder, - matching: find.byType(EditableText, skipOffstage: finder.skipOffstage), + matching: find.byType(EditableText, skipOffstage: skipOffstage), matchRoot: true, ), ); @@ -1124,7 +1128,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// that widget has an open connection (e.g. by using [tap] to focus it), /// then call `testTextInput.enterText` directly (see /// [TestTextInput.enterText]). - Future<void> enterText(Finder finder, String text) async { + Future<void> enterText(FinderBase<Element> finder, String text) async { return TestAsyncUtils.guard<void>(() async { await showKeyboard(finder); testTextInput.enterText(text); diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index 9c379003e7d9f..c292b796e2e22 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -442,21 +442,6 @@ class TestPlatformDispatcher implements PlatformDispatcher { _platformDispatcher.sendPlatformMessage(name, data, callback); } - @Deprecated( - 'Instead of calling this callback, use ServicesBinding.instance.channelBuffers.push. ' - 'This feature was deprecated after v2.1.0-10.0.pre.' - ) - @override - PlatformMessageCallback? get onPlatformMessage => _platformDispatcher.onPlatformMessage; - @Deprecated( - 'Instead of setting this callback, use ServicesBinding.instance.defaultBinaryMessenger.setMessageHandler. ' - 'This feature was deprecated after v2.1.0-10.0.pre.' - ) - @override - set onPlatformMessage(PlatformMessageCallback? callback) { - _platformDispatcher.onPlatformMessage = callback; - } - /// Delete any test value properties that have been set on this [TestPlatformDispatcher] /// and return to reporting the real [PlatformDispatcher] values for all /// [PlatformDispatcher] properties. @@ -1173,7 +1158,7 @@ class _UnsupportedDisplay implements TestDisplay { /// // Fake the desired properties of the TestWindow. All code running /// // within this test will perceive the following fake text scale /// // factor as the real text scale factor of the window. -/// testBinding.window.textScaleFactorFakeValue = 2.5; +/// testBinding.platformDispatcher.textScaleFactorTestValue = 2.5; /// /// // Test code that depends on text scale factor here. /// }); @@ -1186,7 +1171,7 @@ class _UnsupportedDisplay implements TestDisplay { /// If a test needs to override a real [SingletonFlutterWindow] property and /// then later return to using the real [SingletonFlutterWindow] property, /// [TestWindow] provides methods to clear each individual test value, e.g., -/// [clearLocaleTestValue]. +/// [clearDevicePixelRatioTestValue]. /// /// To clear all fake test values in a [TestWindow], consider using /// [clearAllTestValues]. @@ -1507,22 +1492,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override Locale get locale => platformDispatcher.locale; - /// Hides the real locale and reports the given [localeTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.localeTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set localeTestValue(Locale localeTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.localeTestValue = localeTestValue; - } - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearLocaleTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - /// Deletes any existing test locale and returns to using the real locale. - void clearLocaleTestValue() { - platformDispatcher.clearLocaleTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.locales instead. ' @@ -1531,22 +1500,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override List<Locale> get locales => platformDispatcher.locales; - /// Hides the real locales and reports the given [localesTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.localesTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set localesTestValue(List<Locale> localesTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.localesTestValue = localesTestValue; - } - /// Deletes any existing test locales and returns to using the real locales. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearLocalesTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearLocalesTestValue() { - platformDispatcher.clearLocalesTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.onLocaleChanged instead. ' @@ -1572,14 +1525,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override String get initialLifecycleState => platformDispatcher.initialLifecycleState; - /// Sets a faked initialLifecycleState for testing. - @Deprecated( - 'Use WidgetTester.platformDispatcher.initialLifecycleStateTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set initialLifecycleStateTestValue(String state) { // ignore: avoid_setters_without_getters - platformDispatcher.initialLifecycleStateTestValue = state; - } @Deprecated( 'Use WidgetTester.platformDispatcher.textScaleFactor instead. ' @@ -1588,24 +1533,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override double get textScaleFactor => platformDispatcher.textScaleFactor; - /// Hides the real text scale factor and reports the given - /// [textScaleFactorTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.textScaleFactorTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set textScaleFactorTestValue(double textScaleFactorTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.textScaleFactorTestValue = textScaleFactorTestValue; - } - /// Deletes any existing test text scale factor and returns to using the real - /// text scale factor. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearTextScaleFactorTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearTextScaleFactorTestValue() { - platformDispatcher.clearTextScaleFactorTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.platformBrightness instead. ' @@ -1630,24 +1557,6 @@ class TestWindow implements SingletonFlutterWindow { set onPlatformBrightnessChanged(VoidCallback? callback) { platformDispatcher.onPlatformBrightnessChanged = callback; } - /// Hides the real text scale factor and reports the given - /// [platformBrightnessTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.platformBrightnessTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set platformBrightnessTestValue(Brightness platformBrightnessTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.platformBrightnessTestValue = platformBrightnessTestValue; - } - /// Deletes any existing test platform brightness and returns to using the - /// real platform brightness. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearPlatformBrightnessTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearPlatformBrightnessTestValue() { - platformDispatcher.clearPlatformBrightnessTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.alwaysUse24HourFormat instead. ' @@ -1656,24 +1565,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat; - /// Hides the real clock format and reports the given - /// [alwaysUse24HourFormatTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.alwaysUse24HourFormatTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set alwaysUse24HourFormatTestValue(bool alwaysUse24HourFormatTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.alwaysUse24HourFormatTestValue = alwaysUse24HourFormatTestValue; - } - /// Deletes any existing test clock format and returns to using the real clock - /// format. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearAlwaysUse24HourTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearAlwaysUse24HourTestValue() { - platformDispatcher.clearAlwaysUse24HourTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.onTextScaleFactorChanged instead. ' @@ -1715,15 +1606,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword; - /// Hides the real [brieflyShowPassword] and reports the given - /// `brieflyShowPasswordTestValue` instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.brieflyShowPasswordTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set brieflyShowPasswordTestValue(bool brieflyShowPasswordTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.brieflyShowPasswordTestValue = brieflyShowPasswordTestValue; - } @Deprecated( 'Use WidgetTester.platformDispatcher.onBeginFrame instead. ' @@ -1800,24 +1682,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override String get defaultRouteName => platformDispatcher.defaultRouteName; - /// Hides the real default route name and reports the given - /// [defaultRouteNameTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.defaultRouteNameTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set defaultRouteNameTestValue(String defaultRouteNameTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.defaultRouteNameTestValue = defaultRouteNameTestValue; - } - /// Deletes any existing test default route name and returns to using the real - /// default route name. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearDefaultRouteNameTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearDefaultRouteNameTestValue() { - platformDispatcher.clearDefaultRouteNameTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.scheduleFrame() instead. ' @@ -1846,24 +1710,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override bool get semanticsEnabled => platformDispatcher.semanticsEnabled; - /// Hides the real semantics enabled and reports the given - /// [semanticsEnabledTestValue] instead. - @Deprecated( - 'Use WidgetTester.platformDispatcher.semanticsEnabledTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set semanticsEnabledTestValue(bool semanticsEnabledTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.semanticsEnabledTestValue = semanticsEnabledTestValue; - } - /// Deletes any existing test semantics enabled and returns to using the real - /// semantics enabled. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearSemanticsEnabledTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearSemanticsEnabledTestValue() { - platformDispatcher.clearSemanticsEnabledTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.onSemanticsEnabledChanged instead. ' @@ -1889,27 +1735,6 @@ class TestWindow implements SingletonFlutterWindow { ) @override AccessibilityFeatures get accessibilityFeatures => platformDispatcher.accessibilityFeatures; - /// Hides the real accessibility features and reports the given - /// [accessibilityFeaturesTestValue] instead. - /// - /// Consider using [FakeAccessibilityFeatures] to provide specific - /// values for the various accessibility features under test. - @Deprecated( - 'Use WidgetTester.platformDispatcher.accessibilityFeaturesTestValue instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - set accessibilityFeaturesTestValue(AccessibilityFeatures accessibilityFeaturesTestValue) { // ignore: avoid_setters_without_getters - platformDispatcher.accessibilityFeaturesTestValue = accessibilityFeaturesTestValue; - } - /// Deletes any existing test accessibility features and returns to using the - /// real accessibility features. - @Deprecated( - 'Use WidgetTester.platformDispatcher.clearAccessibilityFeaturesTestValue() instead. ' - 'This feature was deprecated after v2.11.0-0.0.pre.' - ) - void clearAccessibilityFeaturesTestValue() { - platformDispatcher.clearAccessibilityFeaturesTestValue(); - } @Deprecated( 'Use WidgetTester.platformDispatcher.onAccessibilityFeaturesChanged instead. ' @@ -1962,21 +1787,6 @@ class TestWindow implements SingletonFlutterWindow { platformDispatcher.sendPlatformMessage(name, data, callback); } - @Deprecated( - 'Instead of calling this callback, use ServicesBinding.instance.channelBuffers.push. ' - 'This feature was deprecated after v2.1.0-10.0.pre.' - ) - @override - PlatformMessageCallback? get onPlatformMessage => platformDispatcher.onPlatformMessage; - @Deprecated( - 'Instead of setting this callback, use ServicesBinding.instance.defaultBinaryMessenger.setMessageHandler. ' - 'This feature was deprecated after v2.1.0-10.0.pre.' - ) - @override - set onPlatformMessage(PlatformMessageCallback? callback) { - platformDispatcher.onPlatformMessage = callback; - } - /// Delete any test value properties that have been set on this [TestWindow] /// as well as its [platformDispatcher]. /// @@ -1984,7 +1794,7 @@ class TestWindow implements SingletonFlutterWindow { /// [PlatformDispatcher] values are reported again. /// /// If desired, clearing of properties can be done on an individual basis, - /// e.g., [clearLocaleTestValue]. + /// e.g., [clearDevicePixelRatioTestValue]. @Deprecated( 'Use WidgetTester.platformDispatcher.clearAllTestValues() and WidgetTester.view.reset() instead. ' 'Deprecated to prepare for the upcoming multi-window support. ' diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index fe95841298336..c2e39420f6e2b 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -12,7 +12,7 @@ dependencies: # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading # this, make sure the tests are still running correctly. - test_api: 0.6.0 + test_api: 0.6.1 matcher: 0.12.16 # Used by golden file comparator @@ -25,7 +25,7 @@ dependencies: # We import stack_trace because the test packages uses it and we # need to be able to unmangle the stack traces that it passed to # stack_trace. See https://github.com/dart-lang/test/issues/590 - stack_trace: 1.11.0 + stack_trace: 1.11.1 # Used by globalToLocal et al. vector_math: 2.1.4 @@ -33,16 +33,54 @@ dependencies: async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: file: 6.1.4 + # Used to detect memory leaks. + leak_tracker_flutter_testing: 1.0.5 -# PUBSPEC CHECKSUM: 7736 + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_multi_server: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + intl: 0.18.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker: 9.0.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker_testing: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + mime: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test: 1.24.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +# PUBSPEC CHECKSUM: a2c2 diff --git a/packages/flutter_test/test/bindings_reset_test.dart b/packages/flutter_test/test/bindings_reset_test.dart new file mode 100644 index 0000000000000..57dee3220438e --- /dev/null +++ b/packages/flutter_test/test/bindings_reset_test.dart @@ -0,0 +1,20 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Disposes restoration manager on reset.', () { + final AutomatedTestWidgetsFlutterBinding binding = AutomatedTestWidgetsFlutterBinding(); + int oldCounter = 0; + final TestRestorationManager oldRestorationManager = binding.restorationManager; + oldRestorationManager.addListener(() => oldCounter++); + + oldRestorationManager.notifyListeners(); + expect(oldCounter, 1); + + binding.reset(); + expect(oldRestorationManager.notifyListeners, throwsA((Object e) => e.toString().contains('disposed'))); + }); +} diff --git a/packages/flutter_test/test/bindings_test.dart b/packages/flutter_test/test/bindings_test.dart index ef4a1658c2deb..f2ed74c6e3762 100644 --- a/packages/flutter_test/test/bindings_test.dart +++ b/packages/flutter_test/test/bindings_test.dart @@ -12,8 +12,8 @@ library; import 'dart:async'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -57,6 +57,18 @@ void main() { order += 1; }); + testWidgets('timeStamp should be accurate to microsecond precision', (WidgetTester tester) async { + final WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + + await tester.pumpWidget(const CircularProgressIndicator()); + + final Duration timeStampBefore = widgetsBinding.currentSystemFrameTimeStamp; + await tester.pump(const Duration(microseconds: 12345)); + final Duration timeStampAfter = widgetsBinding.currentSystemFrameTimeStamp; + + expect(timeStampAfter - timeStampBefore, const Duration(microseconds: 12345)); + }); + group('elapseBlocking', () { testWidgets('timer is not called', (WidgetTester tester) async { bool timerCalled = false; diff --git a/packages/flutter_test/test/controller_test.dart b/packages/flutter_test/test/controller_test.dart index 0337264f36521..c2ca9c93242eb 100644 --- a/packages/flutter_test/test/controller_test.dart +++ b/packages/flutter_test/test/controller_test.dart @@ -982,6 +982,38 @@ void main() { tester.semantics.simulatedAccessibilityTraversal(), containsAllInOrder(expectedMatchers)); }); + + testWidgets('merging node should not be visited', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MergeSemantics( + child: Column( + children: <Widget>[ + Semantics( + container: true, + child: const Text('1'), + ), + Semantics( + container: true, + child: const Text('2'), + ), + Semantics( + container: true, + child: const Text('3'), + ), + ], + ), + ), + ), + ); + + expect( + tester.semantics.simulatedAccessibilityTraversal(), + orderedEquals( + <Matcher>[containsSemantics(label: '1\n2\n3')], + ), + ); + }); }); }); } diff --git a/packages/flutter_test/test/finders_test.dart b/packages/flutter_test/test/finders_test.dart index 09a482bc29e3e..68ac80a0184e2 100644 --- a/packages/flutter_test/test/finders_test.dart +++ b/packages/flutter_test/test/finders_test.dart @@ -8,6 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +const List<Widget> fooBarTexts = <Text>[ + Text('foo', textDirection: TextDirection.ltr), + Text('bar', textDirection: TextDirection.ltr), +]; + void main() { group('image', () { testWidgets('finds Image widgets', (WidgetTester tester) async { @@ -390,6 +395,764 @@ void main() { find.byWidgetPredicate((_) => true).evaluate().length; expect(find.bySubtype<Widget>(), findsNWidgets(totalWidgetCount)); }); + + group('find.byElementPredicate', () { + testWidgets('fails with a custom description in the message', (WidgetTester tester) async { + await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); + + const String customDescription = 'custom description'; + late TestFailure failure; + try { + expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + expect(failure.message, contains('Actual: _ElementPredicateWidgetFinder:<Found 0 widgets with $customDescription')); + }); + }); + + group('find.byWidgetPredicate', () { + testWidgets('fails with a custom description in the message', (WidgetTester tester) async { + await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); + + const String customDescription = 'custom description'; + late TestFailure failure; + try { + expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + expect(failure.message, contains('Actual: _WidgetPredicateWidgetFinder:<Found 0 widgets with $customDescription')); + }); + }); + + group('find.descendant', () { + testWidgets('finds one descendant', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: fooBarTexts), + ], + )); + + expect(find.descendant( + of: find.widgetWithText(Row, 'foo'), + matching: find.text('bar'), + ), findsOneWidget); + }); + + testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: fooBarTexts), + Column(children: fooBarTexts), + ], + )); + + expect(find.descendant( + of: find.widgetWithText(Column, 'foo'), + matching: find.text('bar'), + ), findsNWidgets(2)); + }); + + testWidgets('fails with a descriptive message', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), + Text('bar', textDirection: TextDirection.ltr), + ], + )); + + late TestFailure failure; + try { + expect(find.descendant( + of: find.widgetWithText(Column, 'foo'), + matching: find.text('bar'), + ), findsOneWidget); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + expect( + failure.message, + contains( + 'Actual: _DescendantWidgetFinder:<Found 0 widgets with text "bar" descending from widgets with type "Column" that are ancestors of widgets with text "foo"', + ), + ); + }); + }); + + group('find.ancestor', () { + testWidgets('finds one ancestor', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: fooBarTexts), + ], + )); + + expect(find.ancestor( + of: find.text('bar'), + matching: find.widgetWithText(Row, 'foo'), + ), findsOneWidget); + }); + + testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: <Widget>[ + Row(children: fooBarTexts), + ], + ), + ), + ); + + expect(find.ancestor( + of: find.text('bar'), + matching: find.byType(Row), + ), findsNWidgets(2)); + }); + + testWidgets('fails with a descriptive message', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), + Text('bar', textDirection: TextDirection.ltr), + ], + )); + + late TestFailure failure; + try { + expect(find.ancestor( + of: find.text('bar'), + matching: find.widgetWithText(Column, 'foo'), + ), findsOneWidget); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + expect( + failure.message, + contains( + 'Actual: _AncestorWidgetFinder:<Found 0 widgets with type "Column" that are ancestors of widgets with text "foo" that are ancestors of widgets with text "bar"', + ), + ); + }); + + testWidgets('Root not matched by default', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: fooBarTexts), + ], + )); + + expect(find.ancestor( + of: find.byType(Column), + matching: find.widgetWithText(Column, 'foo'), + ), findsNothing); + }); + + testWidgets('Match the root', (WidgetTester tester) async { + await tester.pumpWidget(const Row( + textDirection: TextDirection.ltr, + children: <Widget>[ + Column(children: fooBarTexts), + ], + )); + + expect(find.descendant( + of: find.byType(Column), + matching: find.widgetWithText(Column, 'foo'), + matchRoot: true, + ), findsOneWidget); + }); + + testWidgets('is fast in deep tree', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: _deepWidgetTree( + depth: 1000, + child: Row( + children: <Widget>[ + _deepWidgetTree( + depth: 1000, + child: const Column(children: fooBarTexts), + ), + ], + ), + ), + ), + ); + + expect(find.ancestor( + of: find.text('bar'), + matching: find.byType(Row), + ), findsOneWidget); + }); + }); + + group('CommonSemanticsFinders', () { + final Widget semanticsTree = _boilerplate( + Semantics( + container: true, + header: true, + readOnly: true, + onCopy: () {}, + onLongPress: () {}, + value: 'value1', + hint: 'hint1', + label: 'label1', + child: Semantics( + container: true, + textField: true, + onSetText: (_) { }, + onPaste: () { }, + onLongPress: () { }, + value: 'value2', + hint: 'hint2', + label: 'label2', + child: Semantics( + container: true, + readOnly: true, + onCopy: () {}, + value: 'value3', + hint: 'hint3', + label: 'label3', + child: Semantics( + container: true, + readOnly: true, + onLongPress: () { }, + value: 'value4', + hint: 'hint4', + label: 'label4', + child: Semantics( + container: true, + onLongPress: () { }, + onCopy: () {}, + value: 'value5', + hint: 'hint5', + label: 'label5' + ), + ), + ) + ), + ), + ); + + group('ancestor', () { + testWidgets('finds matching ancestor nodes', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final FinderBase<SemanticsNode> finder = find.semantics.ancestor( + of: find.semantics.byLabel('label4'), + matching: find.semantics.byAction(SemanticsAction.copy), + ); + + expect(finder, findsExactly(2)); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final FinderBase<SemanticsNode> finder = find.semantics.ancestor( + of: find.semantics.byLabel('label4'), + matching: find.semantics.byAction(SemanticsAction.copy), + ); + + try { + expect(finder, findsExactly(3)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _AncestorSemanticsFinder:<Found 2 SemanticsNodes with action "SemanticsAction.copy" that are ancestors of SemanticsNodes with label "label4"')); + }); + }); + + group('descendant', () { + testWidgets('finds matching descendant nodes', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final FinderBase<SemanticsNode> finder = find.semantics.descendant( + of: find.semantics.byLabel('label4'), + matching: find.semantics.byAction(SemanticsAction.copy), + ); + + expect(finder, findsOne); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final FinderBase<SemanticsNode> finder = find.semantics.descendant( + of: find.semantics.byLabel('label4'), + matching: find.semantics.byAction(SemanticsAction.copy), + ); + + try { + expect(finder, findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _DescendantSemanticsFinder:<Found 1 SemanticsNode with action "SemanticsAction.copy" descending from SemanticsNode with label "label4"')); + }); + }); + + group('byPredicate', () { + testWidgets('finds nodes matching given predicate', (WidgetTester tester) async { + final RegExp replaceRegExp = RegExp(r'^[^\d]+'); + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byPredicate( + (SemanticsNode node) { + final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; + return labelNum > 1; + }, + ); + + expect(finder, findsExactly(4)); + }); + + testWidgets('fails with default message', (WidgetTester tester) async { + late TestFailure failure; + final RegExp replaceRegExp = RegExp(r'^[^\d]+'); + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byPredicate( + (SemanticsNode node) { + final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; + return labelNum > 1; + }, + ); + try { + expect(finder, findsExactly(5)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 matching semantics predicate')); + }); + + testWidgets('fails with given message', (WidgetTester tester) async { + late TestFailure failure; + const String expected = 'custom error message'; + final RegExp replaceRegExp = RegExp(r'^[^\d]+'); + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byPredicate( + (SemanticsNode node) { + final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; + return labelNum > 1; + }, + describeMatch: (_) => expected, + ); + try { + expect(finder, findsExactly(5)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains(expected)); + }); + }); + + group('byLabel', () { + testWidgets('finds nodes with matching label using String', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byLabel('label3'); + + expect(finder, findsOne); + expect(finder.found.first.label, 'label3'); + }); + + testWidgets('finds nodes with matching label using RegEx', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byLabel(RegExp('^label.*')); + + expect(finder, findsExactly(5)); + expect(finder.found.every((SemanticsNode node) => node.label.startsWith('label')), isTrue); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byLabel('label3'); + + try { + expect(finder, findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with label "label3"')); + }); + }); + + group('byValue', () { + testWidgets('finds nodes with matching value using String', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byValue('value3'); + + expect(finder, findsOne); + expect(finder.found.first.value, 'value3'); + }); + + testWidgets('finds nodes with matching value using RegEx', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byValue(RegExp('^value.*')); + + expect(finder, findsExactly(5)); + expect(finder.found.every((SemanticsNode node) => node.value.startsWith('value')), isTrue); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byValue('value3'); + + try { + expect(finder, findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with value "value3"')); + }); + }); + + group('byHint', () { + testWidgets('finds nodes with matching hint using String', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byHint('hint3'); + + expect(finder, findsOne); + expect(finder.found.first.hint, 'hint3'); + }); + + testWidgets('finds nodes with matching hint using RegEx', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byHint(RegExp('^hint.*')); + + expect(finder, findsExactly(5)); + expect(finder.found.every((SemanticsNode node) => node.hint.startsWith('hint')), isTrue); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byHint('hint3'); + + try { + expect(finder, findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with hint "hint3"')); + }); + }); + + group('byAction', () { + testWidgets('finds nodes with matching action', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); + + expect(finder, findsExactly(3)); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); + + try { + expect(finder, findsExactly(4)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 3 SemanticsNodes with action "SemanticsAction.copy"')); + }); + }); + + group('byAnyAction', () { + testWidgets('finds nodes with any matching actions', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ + SemanticsAction.paste, + SemanticsAction.longPress, + ]); + + expect(finder, findsExactly(4)); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ + SemanticsAction.paste, + SemanticsAction.longPress, + ]); + + try { + expect(finder, findsExactly(5)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 SemanticsNodes with any of the following actions: [SemanticsAction.paste, SemanticsAction.longPress]:')); + }); + }); + + group('byFlag', () { + testWidgets('finds nodes with matching flag', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); + + expect(finder, findsExactly(3)); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); + + try { + expect(finder, findsExactly(4)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('_PredicateSemanticsFinder:<Found 3 SemanticsNodes with flag "SemanticsFlag.isReadOnly":')); + }); + }); + + group('byAnyFlag', () { + testWidgets('finds nodes with any matching flag', (WidgetTester tester) async { + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ + SemanticsFlag.isHeader, + SemanticsFlag.isTextField, + ]); + + expect(finder, findsExactly(2)); + }); + + testWidgets('fails with descriptive message', (WidgetTester tester) async { + late TestFailure failure; + await tester.pumpWidget(semanticsTree); + + final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ + SemanticsFlag.isHeader, + SemanticsFlag.isTextField, + ]); + + try { + expect(finder, findsExactly(3)); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 2 SemanticsNodes with any of the following flags: [SemanticsFlag.isHeader, SemanticsFlag.isTextField]:')); + }); + }); + }); + + group('FinderBase', () { + group('describeMatch', () { + test('is used for Finder and results', () { + const String expected = 'Fake finder describe match'; + final _FakeFinder finder = _FakeFinder(describeMatchCallback: (_) { + return expected; + }); + + expect(finder.evaluate().toString(), contains(expected)); + expect(finder.toString(describeSelf: true), contains(expected)); + }); + + for (int i = 0; i < 4; i++) { + test('gets expected plurality for $i when reporting results from find', () { + final Plurality expected = switch (i) { + 0 => Plurality.zero, + 1 => Plurality.one, + _ => Plurality.many, + }; + late final Plurality actual; + final _FakeFinder finder = _FakeFinder( + describeMatchCallback: (Plurality plurality) { + actual = plurality; + return 'Fake description'; + }, + findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), + ); + finder.evaluate().toString(); + + expect(actual, expected); + }); + + test('gets expected plurality for $i when reporting results from toString', () { + final Plurality expected = switch (i) { + 0 => Plurality.zero, + 1 => Plurality.one, + _ => Plurality.many, + }; + late final Plurality actual; + final _FakeFinder finder = _FakeFinder( + describeMatchCallback: (Plurality plurality) { + actual = plurality; + return 'Fake description'; + }, + findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), + ); + finder.toString(); + + expect(actual, expected); + }); + + test('always gets many when describing finder', () { + const Plurality expected = Plurality.many; + late final Plurality actual; + final _FakeFinder finder = _FakeFinder( + describeMatchCallback: (Plurality plurality) { + actual = plurality; + return 'Fake description'; + }, + findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), + ); + finder.toString(describeSelf: true); + + expect(actual, expected); + }); + } + }); + + test('findInCandidates gets allCandidates', () { + final List<String> expected = <String>['Test1', 'Test2', 'Test3', 'Test4']; + late final List<String> actual; + final _FakeFinder finder = _FakeFinder( + allCandidatesCallback: () => expected, + findInCandidatesCallback: (Iterable<String> candidates) { + actual = candidates.toList(); + return candidates; + }, + ); + finder.evaluate(); + + expect(actual, expected); + }); + + test('allCandidates calculated for each find', () { + const int expectedCallCount = 3; + int actualCallCount = 0; + final _FakeFinder finder = _FakeFinder( + allCandidatesCallback: () { + actualCallCount++; + return <String>['test']; + }, + ); + for (int i = 0; i < expectedCallCount; i++) { + finder.evaluate(); + } + + expect(actualCallCount, expectedCallCount); + }); + + test('allCandidates only called once while caching', () { + int actualCallCount = 0; + final _FakeFinder finder = _FakeFinder( + allCandidatesCallback: () { + actualCallCount++; + return <String>['test']; + }, + ); + finder.runCached(() { + for (int i = 0; i < 5; i++) { + finder.evaluate(); + finder.tryEvaluate(); + final FinderResult<String> _ = finder.found; + } + }); + + expect(actualCallCount, 1); + }); + + group('tryFind', () { + test('returns false if no results', () { + final _FakeFinder finder = _FakeFinder( + findInCandidatesCallback: (_) => <String>[], + ); + + expect(finder.tryEvaluate(), false); + }); + + test('returns true if results are available', () { + final _FakeFinder finder = _FakeFinder( + findInCandidatesCallback: (_) => <String>['Results'], + ); + + expect(finder.tryEvaluate(), true); + }); + }); + + group('found', () { + test('throws before any calls to evaluate or tryEvaluate', () { + final _FakeFinder finder = _FakeFinder(); + + expect(finder.hasFound, false); + expect(() => finder.found, throwsAssertionError); + }); + + test('has same results as evaluate after call to evaluate', () { + final _FakeFinder finder = _FakeFinder(); + final FinderResult<String> expected = finder.evaluate(); + + expect(finder.hasFound, true); + expect(finder.found, expected); + }); + + test('has expected results after call to tryFind', () { + final Iterable<String> expected = Iterable<String>.generate(10, (int i) => i.toString()); + final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => expected); + finder.tryEvaluate(); + + + expect(finder.hasFound, true); + expect(finder.found, orderedEquals(expected)); + }); + }); + }); } Widget _boilerplate(Widget child) { @@ -442,3 +1205,45 @@ class SimpleGenericWidget<T> extends StatelessWidget { return _child; } } + +/// Wraps [child] in [depth] layers of [SizedBox] +Widget _deepWidgetTree({required int depth, required Widget child}) { + Widget tree = child; + for (int i = 0; i < depth; i += 1) { + tree = SizedBox(child: tree); + } + return tree; +} + +class _FakeFinder extends FinderBase<String> { + _FakeFinder({ + this.allCandidatesCallback, + this.describeMatchCallback, + this.findInCandidatesCallback, + }); + + final Iterable<String> Function()? allCandidatesCallback; + final DescribeMatchCallback? describeMatchCallback; + final Iterable<String> Function(Iterable<String> candidates)? findInCandidatesCallback; + + + @override + Iterable<String> get allCandidates { + return allCandidatesCallback?.call() ?? <String>[ + 'String 1', 'String 2', 'String 3', + ]; + } + + @override + String describeMatch(Plurality plurality) { + return describeMatchCallback?.call(plurality) ?? switch (plurality) { + Plurality.one => 'String', + Plurality.many || Plurality.zero => 'Strings', + }; + } + + @override + Iterable<String> findInCandidates(Iterable<String> candidates) { + return findInCandidatesCallback?.call(candidates) ?? candidates; + } +} diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index b4be731f6e203..36598dc796c21 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -10,6 +10,7 @@ import 'dart:typed_data'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_math/vector_math_64.dart' show Matrix3; /// Class that makes it easy to mock common toStringDeep behavior. class _MockToStringDeep { @@ -251,6 +252,35 @@ void main() { ); }); + test('matrix3MoreOrLessEquals', () { + expect( + Matrix3.rotationZ(math.pi), + matrix3MoreOrLessEquals(Matrix3.fromList(<double>[ + -1, 0, 0, + 0, -1, 0, + 0, 0, 1, + ])) + ); + + expect( + Matrix3.rotationZ(math.pi), + matrix3MoreOrLessEquals(Matrix3.fromList(<double>[ + -2, 0, 0, + 0, -2, 0, + 0, 0, 1, + ]), epsilon: 2) + ); + + expect( + Matrix3.rotationZ(math.pi), + isNot(matrix3MoreOrLessEquals(Matrix3.fromList(<double>[ + -2, 0, 0, + 0, -2, 0, + 0, 0, 1, + ]))) + ); + }); + test('rectMoreOrLessEquals', () { expect( const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), @@ -440,6 +470,14 @@ void main() { expect(comparator.imageBytes, equals(<int>[1, 2])); expect(comparator.golden, Uri.parse('foo.png')); }); + + testWidgets('future nullable list of integers', + (WidgetTester tester) async { + await expectLater(Future<List<int>?>.value(<int>[1, 2]), matchesGoldenFile('foo.png')); + expect(comparator.invocation, _ComparatorInvocation.compare); + expect(comparator.imageBytes, equals(<int>[1, 2])); + expect(comparator.golden, Uri.parse('foo.png')); + }); }); group('does not match', () { @@ -683,6 +721,8 @@ void main() { hasToggledState: true, isToggled: true, hasImplicitScrolling: true, + hasExpandedState: true, + isExpanded: true, /* Actions */ hasTapAction: true, hasLongPressAction: true, @@ -966,6 +1006,8 @@ void main() { hasToggledState: true, isToggled: true, hasImplicitScrolling: true, + hasExpandedState: true, + isExpanded: true, /* Actions */ hasTapAction: true, hasLongPressAction: true, @@ -1055,6 +1097,8 @@ void main() { hasToggledState: false, isToggled: false, hasImplicitScrolling: false, + hasExpandedState: false, + isExpanded: false, /* Actions */ hasTapAction: false, hasLongPressAction: false, @@ -1324,6 +1368,72 @@ void main() { expect(find.byType(Text), isNot(findsAtLeastNWidgets(3))); }); }); + + group('findsOneWidget', () { + testWidgets('finds exactly one widget', (WidgetTester tester) async { + await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); + expect(find.text('foo'), findsOneWidget); + }); + + testWidgets('fails with a descriptive message', (WidgetTester tester) async { + late TestFailure failure; + try { + expect(find.text('foo', skipOffstage: false), findsOneWidget); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + final String? message = failure.message; + expect(message, contains('Expected: exactly one matching candidate\n')); + expect(message, contains('Actual: _TextWidgetFinder:<Found 0 widgets with text "foo"')); + expect(message, contains('Which: means none were found but one was expected\n')); + }); + }); + + group('findsNothing', () { + testWidgets('finds no widgets', (WidgetTester tester) async { + expect(find.text('foo'), findsNothing); + }); + + testWidgets('fails with a descriptive message', (WidgetTester tester) async { + await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); + + late TestFailure failure; + try { + expect(find.text('foo', skipOffstage: false), findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + final String? message = failure.message; + + expect(message, contains('Expected: no matching candidates\n')); + expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"')); + expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])')); + expect(message, contains('Which: means one was found but none were expected\n')); + }); + + testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async { + await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); + + late TestFailure failure; + try { + expect(find.text('foo'), findsNothing); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + final String? message = failure.message; + + expect(message, contains('Expected: no matching candidates\n')); + expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"')); + expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])')); + expect(message, contains('Which: means one was found but none were expected\n')); + }); + }); } enum _ComparatorBehavior { diff --git a/packages/flutter/test/rendering/mock_canvas_test.dart b/packages/flutter_test/test/mock_canvas_test.dart similarity index 98% rename from packages/flutter/test/rendering/mock_canvas_test.dart rename to packages/flutter_test/test/mock_canvas_test.dart index 82cccc1bde075..5980b9e2fbc74 100644 --- a/packages/flutter/test/rendering/mock_canvas_test.dart +++ b/packages/flutter_test/test/mock_canvas_test.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'mock_canvas.dart'; - class MyPainter extends CustomPainter { const MyPainter({ required this.color, @@ -131,7 +129,7 @@ void main() { return false; } if (method == #drawColor) { - throw 'fail'; + fail('fail'); } return true; }), @@ -225,7 +223,7 @@ void main() { paints..everything((Symbol method, List<dynamic> arguments) { methodsAndArguments.add(MethodAndArguments(method, arguments)); if (method == #drawColor) { - throw 'failed '; + fail('failed '); } return true; }), diff --git a/packages/flutter_test/test/multi_view_accessibility_test.dart b/packages/flutter_test/test/multi_view_accessibility_test.dart new file mode 100644 index 0000000000000..c070ad63107ba --- /dev/null +++ b/packages/flutter_test/test/multi_view_accessibility_test.dart @@ -0,0 +1,135 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Detects tap targets in all views', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await pumpViews( + tester: tester, + viewContents: <Widget>[ + SizedBox( + width: 47.0, + height: 47.0, + child: GestureDetector(onTap: () {}), + ), + SizedBox( + width: 46.0, + height: 46.0, + child: GestureDetector(onTap: () {}), + ), + ], + ); + final Evaluation result = await androidTapTargetGuideline.evaluate(tester); + expect(result.passed, false); + expect( + result.reason, + contains('expected tap target size of at least Size(48.0, 48.0), but found Size(47.0, 47.0)'), + ); + expect( + result.reason, + contains('expected tap target size of at least Size(48.0, 48.0), but found Size(46.0, 46.0)'), + ); + handle.dispose(); + }); + + testWidgets('Detects labeled tap targets in all views', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await pumpViews( + tester: tester, + viewContents: <Widget>[ + SizedBox( + width: 47.0, + height: 47.0, + child: GestureDetector(onTap: () {}), + ), + SizedBox( + width: 46.0, + height: 46.0, + child: GestureDetector(onTap: () {}), + ), + ], + ); + final Evaluation result = await labeledTapTargetGuideline.evaluate(tester); + expect(result.passed, false); + final List<String> lines = const LineSplitter().convert(result.reason!); + expect(lines, hasLength(2)); + expect(lines.first, startsWith('SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 47.0, 47.0)')); + expect(lines.first, endsWith('expected tappable node to have semantic label, but none was found.')); + expect(lines.last, startsWith('SemanticsNode#2(Rect.fromLTRB(0.0, 0.0, 46.0, 46.0)')); + expect(lines.last, endsWith('expected tappable node to have semantic label, but none was found.')); + handle.dispose(); + }); + + testWidgets('Detects contrast problems in all views', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await pumpViews( + tester: tester, + viewContents: <Widget>[ + Container( + width: 200.0, + height: 200.0, + color: Colors.yellow, + child: const Text( + 'this is a test', + style: TextStyle(fontSize: 14.0, color: Colors.yellowAccent), + ), + ), + Container( + width: 200.0, + height: 200.0, + color: Colors.yellow, + child: const Text( + 'this is a test', + style: TextStyle(fontSize: 25.0, color: Colors.yellowAccent), + ), + ), + ], + ); + final Evaluation result = await textContrastGuideline.evaluate(tester); + expect(result.passed, false); + expect(result.reason, contains('Expected contrast ratio of at least 4.5 but found 0.88 for a font size of 14.0.')); + expect(result.reason, contains('Expected contrast ratio of at least 3.0 but found 0.88 for a font size of 25.0.')); + handle.dispose(); + }); +} + +Future<void> pumpViews({required WidgetTester tester, required List<Widget> viewContents}) { + final List<Widget> views = <Widget>[ + for (int i = 0; i < viewContents.length; i++) + View( + view: FakeView(tester.view, viewId: i + 100), + child: Center( + child: viewContents[i], + ), + ), + ]; + + tester.binding.attachRootWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ViewCollection( + views: views, + ), + ), + ); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} + +class FakeView extends TestFlutterView{ + FakeView(FlutterView view, { this.viewId = 100 }) : super( + view: view, + platformDispatcher: view.platformDispatcher as TestPlatformDispatcher, + display: view.display as TestDisplay, + ); + + @override + final int viewId; +} diff --git a/packages/flutter_test/test/multi_view_controller_test.dart b/packages/flutter_test/test/multi_view_controller_test.dart new file mode 100644 index 0000000000000..6d74d924c15fc --- /dev/null +++ b/packages/flutter_test/test/multi_view_controller_test.dart @@ -0,0 +1,232 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('simulatedAccessibilityTraversal - start and end in same view', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + end: find.text('View2Child3'), + ).map((SemanticsNode node) => node.label), + <String>[ + 'View2Child1', + 'View2Child2', + 'View2Child3', + ], + ); + }); + + testWidgets('simulatedAccessibilityTraversal - start not specified', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal( + end: find.text('View2Child3'), + ).map((SemanticsNode node) => node.label), + <String>[ + 'View2Child0', + 'View2Child1', + 'View2Child2', + 'View2Child3', + ], + ); + }); + + testWidgets('simulatedAccessibilityTraversal - end not specified', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + ).map((SemanticsNode node) => node.label), + <String>[ + 'View2Child1', + 'View2Child2', + 'View2Child3', + 'View2Child4', + ], + ); + }); + + testWidgets('simulatedAccessibilityTraversal - nothing specified', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal().map((SemanticsNode node) => node.label), + <String>[ + 'View1Child0', + 'View1Child1', + 'View1Child2', + 'View1Child3', + 'View1Child4', + ], + ); + // Should be traversing over tester.view. + expect(tester.viewOf(find.text('View1Child0')), tester.view); + }); + + testWidgets('simulatedAccessibilityTraversal - only view specified', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal( + view: tester.viewOf(find.text('View2Child1')), + ).map((SemanticsNode node) => node.label), + <String>[ + 'View2Child0', + 'View2Child1', + 'View2Child2', + 'View2Child3', + 'View2Child4', + ], + ); + }); + + testWidgets('simulatedAccessibilityTraversal - everything specified', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + end: find.text('View2Child3'), + view: tester.viewOf(find.text('View2Child1')), + ).map((SemanticsNode node) => node.label), + <String>[ + 'View2Child1', + 'View2Child2', + 'View2Child3', + ], + ); + }); + + testWidgets('simulatedAccessibilityTraversal - start and end not in same view', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + () => tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + end: find.text('View1Child3'), + ), + throwsA(isStateError.having( + (StateError e) => e.message, + 'message', + contains('The start and end node are in different views.'), + )), + ); + }); + + testWidgets('simulatedAccessibilityTraversal - start is not in view', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + () => tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + end: find.text('View1Child3'), + view: tester.viewOf(find.text('View1Child3')), + ), + throwsA(isStateError.having( + (StateError e) => e.message, + 'message', + contains('The start node is not part of the provided view.'), + )), + ); + }); + + testWidgets('simulatedAccessibilityTraversal - end is not in view', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect( + () => tester.semantics.simulatedAccessibilityTraversal( + start: find.text('View2Child1'), + end: find.text('View1Child3'), + view: tester.viewOf(find.text('View2Child1')), + ), + throwsA(isStateError.having( + (StateError e) => e.message, + 'message', + contains('The end node is not part of the provided view.'), + )), + ); + }); + + testWidgets('viewOf', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect(tester.viewOf(find.text('View0Child0')).viewId, 100); + expect(tester.viewOf(find.text('View1Child1')).viewId, tester.view.viewId); + expect(tester.viewOf(find.text('View2Child2')).viewId, 102); + }); + + testWidgets('layers includes layers from all views', (WidgetTester tester) async { + await pumpViews(tester: tester); + const int numberOfViews = 3; + expect(tester.binding.renderViews.length, numberOfViews); // One RenderView for each FlutterView. + + final List<Layer> layers = tester.layers; + // Each RenderView contributes a TransformLayer and a PictureLayer. + expect(layers, hasLength(numberOfViews * 2)); + expect(layers.whereType<TransformLayer>(), hasLength(numberOfViews)); + expect(layers.whereType<PictureLayer>(), hasLength(numberOfViews)); + expect( + layers.whereType<TransformLayer>().map((TransformLayer l ) => l.owner), + containsAll(tester.binding.renderViews), + ); + }); + + testWidgets('hitTestOnBinding', (WidgetTester tester) async { + await pumpViews(tester: tester); + // Not specifying a viewId hit tests on tester.view: + HitTestResult result = tester.hitTestOnBinding(Offset.zero); + expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView, tester.view); + // Specifying a viewId is respected: + result = tester.hitTestOnBinding(Offset.zero, viewId: 100); + expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView.viewId, 100); + result = tester.hitTestOnBinding(Offset.zero, viewId: 102); + expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView.viewId, 102); + }); + + testWidgets('hitTestable works in different Views', (WidgetTester tester) async { + await pumpViews(tester: tester); + expect((find.text('View0Child0').hitTestable().evaluate().single.widget as Text).data, 'View0Child0'); + expect((find.text('View1Child1').hitTestable().evaluate().single.widget as Text).data, 'View1Child1'); + expect((find.text('View2Child2').hitTestable().evaluate().single.widget as Text).data, 'View2Child2'); + }); +} + +Future<void> pumpViews({required WidgetTester tester}) { + final List<Widget> views = <Widget>[ + for (int i = 0; i < 3; i++) + View( + view: i == 1 ? tester.view : FakeView(tester.view, viewId: i + 100), + child: Center( + child: Column( + children: <Widget>[ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('View${i}Child$c')), + ], + ), + ), + ), + ]; + + tester.binding.attachRootWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ViewCollection( + views: views, + ), + ), + ); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} + +class FakeView extends TestFlutterView{ + FakeView(FlutterView view, { this.viewId = 100 }) : super( + view: view, + platformDispatcher: view.platformDispatcher as TestPlatformDispatcher, + display: view.display as TestDisplay, + ); + + @override + final int viewId; +} diff --git a/packages/flutter_test/test/reference_image_test.dart b/packages/flutter_test/test/reference_image_test.dart index add664c4bbd65..8be052337cdc3 100644 --- a/packages/flutter_test/test/reference_image_test.dart +++ b/packages/flutter_test/test/reference_image_test.dart @@ -4,9 +4,12 @@ import 'dart:ui' as ui; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -Future<ui.Image> createTestImage(int width, int height, ui.Color color) { +Future<ui.Image> createTestImage(int width, int height, ui.Color color) async { final ui.Paint paint = ui.Paint() ..style = ui.PaintingStyle.stroke ..strokeWidth = 1.0 @@ -15,7 +18,9 @@ Future<ui.Image> createTestImage(int width, int height, ui.Color color) { final ui.Canvas pictureCanvas = ui.Canvas(recorder); pictureCanvas.drawCircle(Offset.zero, 20.0, paint); final ui.Picture picture = recorder.endRecording(); - return picture.toImage(width, height); + final ui.Image image = await picture.toImage(width, height); + picture.dispose(); + return image; } void main() { @@ -24,50 +29,121 @@ void main() { const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0); group('succeeds', () { - testWidgets('when images have the same content', (WidgetTester tester) async { - await expectLater( - await createTestImage(100, 100, red), - matchesReferenceImage(await createTestImage(100, 100, red)), - ); - await expectLater( - await createTestImage(100, 100, green), - matchesReferenceImage(await createTestImage(100, 100, green)), - ); + testWidgetsWithLeakTracking('when images have the same content', (WidgetTester tester) async { + final ui.Image image1 = await createTestImage(100, 100, red); + addTearDown(image1.dispose); + final ui.Image referenceImage1 = await createTestImage(100, 100, red); + addTearDown(referenceImage1.dispose); - await expectLater( - await createTestImage(100, 100, transparentRed), - matchesReferenceImage(await createTestImage(100, 100, transparentRed)), - ); + await expectLater(image1, matchesReferenceImage(referenceImage1)); + + final ui.Image image2 = await createTestImage(100, 100, green); + addTearDown(image2.dispose); + final ui.Image referenceImage2 = await createTestImage(100, 100, green); + addTearDown(referenceImage2.dispose); + + await expectLater(image2, matchesReferenceImage(referenceImage2)); + + final ui.Image image3 = await createTestImage(100, 100, transparentRed); + addTearDown(image3.dispose); + final ui.Image referenceImage3 = await createTestImage(100, 100, transparentRed); + addTearDown(referenceImage3.dispose); + + await expectLater(image3, matchesReferenceImage(referenceImage3)); }); - testWidgets('when images are identical', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when images are identical', (WidgetTester tester) async { final ui.Image image = await createTestImage(100, 100, red); + addTearDown(image.dispose); await expectLater(image, matchesReferenceImage(image)); }); + + testWidgetsWithLeakTracking('when widget looks the same', (WidgetTester tester) async { + addTearDown(tester.view.reset); + tester.view + ..physicalSize = const Size(10, 10) + ..devicePixelRatio = 1; + + const ValueKey<String> repaintBoundaryKey = ValueKey<String>('boundary'); + + await tester.pumpWidget( + const RepaintBoundary( + key: repaintBoundaryKey, + child: ColoredBox(color: red), + ), + ); + + final ui.Image referenceImage = (tester.renderObject(find.byKey(repaintBoundaryKey)) as RenderRepaintBoundary).toImageSync(); + addTearDown(referenceImage.dispose); + + await expectLater(find.byKey(repaintBoundaryKey), matchesReferenceImage(referenceImage)); + }); }); group('fails', () { - testWidgets('when image sizes do not match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when image sizes do not match', (WidgetTester tester) async { final ui.Image red50 = await createTestImage(50, 50, red); + addTearDown(red50.dispose); final ui.Image red100 = await createTestImage(100, 100, red); + addTearDown(red100.dispose); + expect( await matchesReferenceImage(red50).matchAsync(red100), equals('does not match as width or height do not match. [100×100] != [50×50]'), ); }); - testWidgets('when image pixels do not match', (WidgetTester tester) async { + testWidgetsWithLeakTracking('when image pixels do not match', (WidgetTester tester) async { final ui.Image red100 = await createTestImage(100, 100, red); + addTearDown(red100.dispose); final ui.Image transparentRed100 = await createTestImage(100, 100, transparentRed); + addTearDown(transparentRed100.dispose); + expect( await matchesReferenceImage(red100).matchAsync(transparentRed100), equals('does not match on 57 pixels'), ); + final ui.Image green100 = await createTestImage(100, 100, green); + addTearDown(green100.dispose); + expect( await matchesReferenceImage(red100).matchAsync(green100), equals('does not match on 57 pixels'), ); }); + + testWidgetsWithLeakTracking('when widget does not look the same', (WidgetTester tester) async { + addTearDown(tester.view.reset); + tester.view + ..physicalSize = const Size(10, 10) + ..devicePixelRatio = 1; + + const ValueKey<String> repaintBoundaryKey = ValueKey<String>('boundary'); + + await tester.pumpWidget( + const RepaintBoundary( + key: repaintBoundaryKey, + child: ColoredBox(color: red), + ), + ); + + final ui.Image referenceImage = (tester.renderObject(find.byKey(repaintBoundaryKey)) as RenderRepaintBoundary).toImageSync(); + addTearDown(referenceImage.dispose); + + await tester.pumpWidget( + const RepaintBoundary( + key: repaintBoundaryKey, + child: ColoredBox(color: green), + ), + ); + + expect( + await matchesReferenceImage(referenceImage).matchAsync( + find.byKey(repaintBoundaryKey), + ), + equals('does not match on 100 pixels'), + ); + }); }); } diff --git a/packages/flutter_test/test/semantics_checker/flutter_test_config.dart b/packages/flutter_test/test/semantics_checker/flutter_test_config.dart index 88b2aee380d1a..f4a4af6b75ef0 100644 --- a/packages/flutter_test/test/semantics_checker/flutter_test_config.dart +++ b/packages/flutter_test/test/semantics_checker/flutter_test_config.dart @@ -22,9 +22,9 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async { void pipelineOwnerTestRun() { testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async { - final int outstandingHandles = tester.binding.pipelineOwner.debugOutstandingSemanticsHandles; - tester.binding.pipelineOwner.ensureSemantics(); - expect(tester.binding.pipelineOwner.debugOutstandingSemanticsHandles, outstandingHandles + 1); + final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles; + tester.binding.ensureSemantics(); + expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1); // SemanticsHandle is not disposed on purpose to verify in tearDown that // the test failed due to an active SemanticsHandle. }); diff --git a/packages/flutter_test/test/test_config/project_root/pubspec.yaml b/packages/flutter_test/test/test_config/project_root/pubspec.yaml index f4d908c200388..75c96b122feef 100644 --- a/packages/flutter_test/test/test_config/project_root/pubspec.yaml +++ b/packages/flutter_test/test/test_config/project_root/pubspec.yaml @@ -4,6 +4,6 @@ name: dummy environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' # PUBSPEC CHECKSUM: 0000 diff --git a/packages/flutter_test/test/test_text_input_test.dart b/packages/flutter_test/test/test_text_input_test.dart index eb911ff2c31d3..535a027b92cf3 100644 --- a/packages/flutter_test/test/test_text_input_test.dart +++ b/packages/flutter_test/test/test_text_input_test.dart @@ -76,4 +76,26 @@ void main() { expect(selectorNames, isNull); } }, variant: TargetPlatformVariant.all()); + + testWidgets('selector is called for ctrl + backspace on macOS', (WidgetTester tester) async { + List<dynamic>? selectorNames; + await SystemChannels.textInput.invokeMethod('TextInput.setClient', <dynamic>[1, <String, dynamic>{}]); + await SystemChannels.textInput.invokeMethod('TextInput.show'); + SystemChannels.textInput.setMethodCallHandler((MethodCall call) async { + if (call.method == 'TextInputClient.performSelectors') { + selectorNames = (call.arguments as List<dynamic>)[1] as List<dynamic>; + } + }); + await tester.sendKeyDownEvent(LogicalKeyboardKey.control); + await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace); + await tester.sendKeyUpEvent(LogicalKeyboardKey.backspace); + await tester.sendKeyUpEvent(LogicalKeyboardKey.control); + await SystemChannels.textInput.invokeMethod('TextInput.clearClient'); + + if (defaultTargetPlatform == TargetPlatform.macOS) { + expect(selectorNames, <dynamic>['deleteBackwardByDecomposingPreviousCharacter:']); + } else { + expect(selectorNames, isNull); + } + }, variant: TargetPlatformVariant.all()); } diff --git a/packages/flutter_test/test/utils/fake_and_mock_utils.dart b/packages/flutter_test/test/utils/fake_and_mock_utils.dart index db2b7ba438503..8f5fcf1720ad8 100644 --- a/packages/flutter_test/test/utils/fake_and_mock_utils.dart +++ b/packages/flutter_test/test/utils/fake_and_mock_utils.dart @@ -19,7 +19,7 @@ void verifyPropertyFaked<TProperty>({ required TProperty realValue, required TProperty fakeValue, required TProperty Function() propertyRetriever, - required Function(TestWidgetsFlutterBinding, TProperty fakeValue) propertyFaker, + required void Function(TestWidgetsFlutterBinding, TProperty fakeValue) propertyFaker, Matcher Function(TProperty) matcher = equals, }) { TProperty propertyBeforeFaking; @@ -45,8 +45,8 @@ void verifyPropertyReset<TProperty>({ required WidgetTester tester, required TProperty fakeValue, required TProperty Function() propertyRetriever, - required Function() propertyResetter, - required Function(TProperty fakeValue) propertyFaker, + required VoidCallback propertyResetter, + required ValueSetter<TProperty> propertyFaker, Matcher Function(TProperty) matcher = equals, }) { TProperty propertyBeforeFaking; diff --git a/packages/flutter_test/test/widget_tester_live_device_test.dart b/packages/flutter_test/test/widget_tester_live_device_test.dart index 4f9871011ecc6..c6468a2f47fa6 100644 --- a/packages/flutter_test/test/widget_tester_live_device_test.dart +++ b/packages/flutter_test/test/widget_tester_live_device_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; // Only check the initial lines of the message, since the message walks the @@ -82,7 +83,7 @@ No widgets found at Offset(1.0, 1.0). ), ); - final Size originalSize = tester.binding.createViewConfiguration().size; + final Size originalSize = tester.binding.createViewConfigurationFor(tester.binding.renderView).size; // ignore: deprecated_member_use await tester.binding.setSurfaceSize(const Size(2000, 1800)); try { await tester.pump(); @@ -126,6 +127,7 @@ class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding { // real devices touches sends event in the global coordinate system. // See the documentation of [handlePointerEventForSource] for details. if (source == TestBindingEventSource.test) { + final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId); final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView)); return super.handlePointerEventForSource(globalEvent); } diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index a168619b2e444..e8944bee55da2 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -16,11 +16,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:matcher/expect.dart' as matcher; import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports -const List<Widget> fooBarTexts = <Text>[ - Text('foo', textDirection: TextDirection.ltr), - Text('bar', textDirection: TextDirection.ltr), -]; - void main() { group('expectLater', () { testWidgets('completes when matcher completes', (WidgetTester tester) async { @@ -75,70 +70,6 @@ void main() { }); }, skip: true); // [intended] API testing - group('findsOneWidget', () { - testWidgets('finds exactly one widget', (WidgetTester tester) async { - await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); - expect(find.text('foo'), findsOneWidget); - }); - - testWidgets('fails with a descriptive message', (WidgetTester tester) async { - late TestFailure failure; - try { - expect(find.text('foo', skipOffstage: false), findsOneWidget); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - final String? message = failure.message; - expect(message, contains('Expected: exactly one matching node in the widget tree\n')); - expect(message, contains('Actual: _TextFinder:<zero widgets with text "foo">\n')); - expect(message, contains('Which: means none were found but one was expected\n')); - }); - }); - - group('findsNothing', () { - testWidgets('finds no widgets', (WidgetTester tester) async { - expect(find.text('foo'), findsNothing); - }); - - testWidgets('fails with a descriptive message', (WidgetTester tester) async { - await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); - - late TestFailure failure; - try { - expect(find.text('foo', skipOffstage: false), findsNothing); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - final String? message = failure.message; - - expect(message, contains('Expected: no matching nodes in the widget tree\n')); - expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo": Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n')); - expect(message, contains('Which: means one was found but none were expected\n')); - }); - - testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async { - await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); - - late TestFailure failure; - try { - expect(find.text('foo'), findsNothing); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - final String? message = failure.message; - - expect(message, contains('Expected: no matching nodes in the widget tree\n')); - expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n')); - expect(message, contains('Which: means one was found but none were expected\n')); - }); - }); - group('pumping', () { testWidgets('pumping', (WidgetTester tester) async { await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); @@ -188,223 +119,19 @@ void main() { await tester.pumpFrames(target, const Duration(milliseconds: 55)); - expect(logPaints, <int>[0, 17000, 34000, 50000]); + // `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump, + // so we expect 4 pumps of 16683 microseconds each in the 55ms duration. + expect(logPaints, <int>[0, 16683, 33366, 50049]); logPaints.clear(); await tester.pumpFrames(target, const Duration(milliseconds: 30), const Duration(milliseconds: 10)); - expect(logPaints, <int>[60000, 70000, 80000]); + // Since `pumpFrames` was given a 10ms interval per pump, we expect the + // results to continue from 50049 with 10000 microseconds per pump over + // the 30ms duration. + expect(logPaints, <int>[60049, 70049, 80049]); }); }); - - group('find.byElementPredicate', () { - testWidgets('fails with a custom description in the message', (WidgetTester tester) async { - await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); - - const String customDescription = 'custom description'; - late TestFailure failure; - try { - expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - expect(failure.message, contains('Actual: _ElementPredicateFinder:<zero widgets with $customDescription')); - }); - }); - - group('find.byWidgetPredicate', () { - testWidgets('fails with a custom description in the message', (WidgetTester tester) async { - await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); - - const String customDescription = 'custom description'; - late TestFailure failure; - try { - expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - expect(failure.message, contains('Actual: _WidgetPredicateFinder:<zero widgets with $customDescription')); - }); - }); - - group('find.descendant', () { - testWidgets('finds one descendant', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: fooBarTexts), - ], - )); - - expect(find.descendant( - of: find.widgetWithText(Row, 'foo'), - matching: find.text('bar'), - ), findsOneWidget); - }); - - testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: fooBarTexts), - Column(children: fooBarTexts), - ], - )); - - expect(find.descendant( - of: find.widgetWithText(Column, 'foo'), - matching: find.text('bar'), - ), findsNWidgets(2)); - }); - - testWidgets('fails with a descriptive message', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), - Text('bar', textDirection: TextDirection.ltr), - ], - )); - - late TestFailure failure; - try { - expect(find.descendant( - of: find.widgetWithText(Column, 'foo'), - matching: find.text('bar'), - ), findsOneWidget); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - expect( - failure.message, - contains( - 'Actual: _DescendantFinder:<zero widgets with text "bar" that has ancestor(s) with type "Column" which is an ancestor of text "foo"', - ), - ); - }); - }); - - group('find.ancestor', () { - testWidgets('finds one ancestor', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: fooBarTexts), - ], - )); - - expect(find.ancestor( - of: find.text('bar'), - matching: find.widgetWithText(Row, 'foo'), - ), findsOneWidget); - }); - - testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async { - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: Row( - children: <Widget>[ - Row(children: fooBarTexts), - ], - ), - ), - ); - - expect(find.ancestor( - of: find.text('bar'), - matching: find.byType(Row), - ), findsNWidgets(2)); - }); - - testWidgets('fails with a descriptive message', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), - Text('bar', textDirection: TextDirection.ltr), - ], - )); - - late TestFailure failure; - try { - expect(find.ancestor( - of: find.text('bar'), - matching: find.widgetWithText(Column, 'foo'), - ), findsOneWidget); - } on TestFailure catch (e) { - failure = e; - } - - expect(failure, isNotNull); - expect( - failure.message, - contains( - 'Actual: _AncestorFinder:<zero widgets with type "Column" which is an ancestor of text "foo" which is an ancestor of text "bar"', - ), - ); - }); - - testWidgets('Root not matched by default', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: fooBarTexts), - ], - )); - - expect(find.ancestor( - of: find.byType(Column), - matching: find.widgetWithText(Column, 'foo'), - ), findsNothing); - }); - - testWidgets('Match the root', (WidgetTester tester) async { - await tester.pumpWidget(const Row( - textDirection: TextDirection.ltr, - children: <Widget>[ - Column(children: fooBarTexts), - ], - )); - - expect(find.descendant( - of: find.byType(Column), - matching: find.widgetWithText(Column, 'foo'), - matchRoot: true, - ), findsOneWidget); - }); - - testWidgets('is fast in deep tree', (WidgetTester tester) async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: _deepWidgetTree( - depth: 1000, - child: Row( - children: <Widget>[ - _deepWidgetTree( - depth: 1000, - child: const Column(children: fooBarTexts), - ), - ], - ), - ), - ), - ); - - expect(find.ancestor( - of: find.text('bar'), - matching: find.byType(Row), - ), findsOneWidget); - }); - }); - group('pageBack', () { testWidgets('fails when there are no back buttons', (WidgetTester tester) async { await tester.pumpWidget(Container()); @@ -985,12 +712,3 @@ class _AlwaysRepaint extends CustomPainter { onPaint(); } } - -/// Wraps [child] in [depth] layers of [SizedBox] -Widget _deepWidgetTree({required int depth, required Widget child}) { - Widget tree = child; - for (int i = 0; i < depth; i += 1) { - tree = SizedBox(child: tree); - } - return tree; -} diff --git a/packages/flutter_test/test/window_test.dart b/packages/flutter_test/test/window_test.dart index 144890c744093..8ece4798eb405 100644 --- a/packages/flutter_test/test/window_test.dart +++ b/packages/flutter_test/test/window_test.dart @@ -8,7 +8,7 @@ import 'dart:ui' as ui show window; import 'dart:ui'; -import 'package:flutter/widgets.dart' show WidgetsBinding, WidgetsBindingObserver; +import 'package:flutter/widgets.dart' show WidgetsBinding; import 'package:flutter_test/flutter_test.dart'; import 'utils/fake_and_mock_utils.dart'; @@ -83,34 +83,6 @@ void main() { ); }); - testWidgets('TestWindow can fake locale', (WidgetTester tester) async { - verifyPropertyFaked<Locale>( - tester: tester, - realValue: ui.window.locale, - fakeValue: const Locale('fake_language_code'), - propertyRetriever: () { - return WidgetsBinding.instance.window.locale; - }, - propertyFaker: (TestWidgetsFlutterBinding binding, Locale fakeValue) { - binding.window.localeTestValue = fakeValue; - }, - ); - }); - - testWidgets('TestWindow can fake locales', (WidgetTester tester) async { - verifyPropertyFaked<List<Locale>>( - tester: tester, - realValue: ui.window.locales, - fakeValue: <Locale>[const Locale('fake_language_code')], - propertyRetriever: () { - return WidgetsBinding.instance.window.locales; - }, - propertyFaker: (TestWidgetsFlutterBinding binding, List<Locale> fakeValue) { - binding.window.localesTestValue = fakeValue; - }, - ); - }); - testWidgets('TestWindow can fake text scale factor', (WidgetTester tester) async { verifyPropertyFaked<double>( tester: tester, @@ -120,61 +92,7 @@ void main() { return WidgetsBinding.instance.window.textScaleFactor; }, propertyFaker: (TestWidgetsFlutterBinding binding, double fakeValue) { - binding.window.textScaleFactorTestValue = fakeValue; - }, - ); - }); - - testWidgets('TestWindow can fake clock format', (WidgetTester tester) async { - verifyPropertyFaked<bool>( - tester: tester, - realValue: ui.window.alwaysUse24HourFormat, - fakeValue: !ui.window.alwaysUse24HourFormat, - propertyRetriever: () { - return WidgetsBinding.instance.window.alwaysUse24HourFormat; - }, - propertyFaker: (TestWidgetsFlutterBinding binding, bool fakeValue) { - binding.window.alwaysUse24HourFormatTestValue = fakeValue; - }, - ); - }); - - testWidgets('TestWindow can fake brieflyShowPassword', (WidgetTester tester) async { - verifyPropertyFaked<bool>( - tester: tester, - realValue: ui.window.brieflyShowPassword, - fakeValue: !ui.window.brieflyShowPassword, - propertyRetriever: () => WidgetsBinding.instance.window.brieflyShowPassword, - propertyFaker: (TestWidgetsFlutterBinding binding, bool fakeValue) { - binding.window.brieflyShowPasswordTestValue = fakeValue; - }, - ); - }); - - testWidgets('TestWindow can fake default route name', (WidgetTester tester) async { - verifyPropertyFaked<String>( - tester: tester, - realValue: ui.window.defaultRouteName, - fakeValue: 'fake_route', - propertyRetriever: () { - return WidgetsBinding.instance.window.defaultRouteName; - }, - propertyFaker: (TestWidgetsFlutterBinding binding, String fakeValue) { - binding.window.defaultRouteNameTestValue = fakeValue; - }, - ); - }); - - testWidgets('TestWindow can fake accessibility features', (WidgetTester tester) async { - verifyPropertyFaked<AccessibilityFeatures>( - tester: tester, - realValue: ui.window.accessibilityFeatures, - fakeValue: const FakeAccessibilityFeatures(), - propertyRetriever: () { - return WidgetsBinding.instance.window.accessibilityFeatures; - }, - propertyFaker: (TestWidgetsFlutterBinding binding, AccessibilityFeatures fakeValue) { - binding.window.accessibilityFeaturesTestValue = fakeValue; + binding.platformDispatcher.textScaleFactorTestValue = fakeValue; }, ); }); @@ -188,7 +106,7 @@ void main() { return WidgetsBinding.instance.window.platformBrightness; }, propertyFaker: (TestWidgetsFlutterBinding binding, Brightness fakeValue) { - binding.window.platformBrightnessTestValue = fakeValue; + binding.platformDispatcher.platformBrightnessTestValue = fakeValue; }, ); }); @@ -200,7 +118,7 @@ void main() { // Set fake values for window properties. testWindow.devicePixelRatioTestValue = 2.5; - testWindow.textScaleFactorTestValue = 3.0; + tester.platformDispatcher.textScaleFactorTestValue = 3.0; // Erase fake window property values. testWindow.clearAllTestValues(); @@ -210,16 +128,6 @@ void main() { expect(WidgetsBinding.instance.window.textScaleFactor, originalTextScaleFactor); }); - testWidgets('TestWindow sends fake locales when WidgetsBindingObserver notifiers are called', (WidgetTester tester) async { - final List<Locale> defaultLocales = WidgetsBinding.instance.window.locales; - final TestObserver observer = TestObserver(); - retrieveTestBinding(tester).addObserver(observer); - final List<Locale> expectedValue = <Locale>[const Locale('fake_language_code')]; - retrieveTestBinding(tester).window.localesTestValue = expectedValue; - expect(observer.locales, equals(expectedValue)); - retrieveTestBinding(tester).window.localesTestValue = defaultLocales; - }); - testWidgets('Updates to window also update tester.view', (WidgetTester tester) async { tester.binding.window.devicePixelRatioTestValue = 7; tester.binding.window.displayFeaturesTestValue = <DisplayFeature>[const DisplayFeature(bounds: Rect.fromLTWH(0, 0, 20, 300), type: DisplayFeatureType.unknown, state: DisplayFeatureState.unknown)]; @@ -240,12 +148,3 @@ void main() { expect(tester.binding.window.gestureSettings, tester.view.gestureSettings); }); } - -class TestObserver with WidgetsBindingObserver { - List<Locale>? locales; - - @override - void didChangeLocales(List<Locale>? locales) { - this.locales = locales; - } -} diff --git a/packages/flutter_tools/README.md b/packages/flutter_tools/README.md index c52201c29cb7e..606eacfb3ad5b 100644 --- a/packages/flutter_tools/README.md +++ b/packages/flutter_tools/README.md @@ -67,14 +67,16 @@ Please avoid setting any other timeouts. #### Using local engine builds in integration tests The integration tests can be configured to use a specific local engine -variant by setting the `FLUTTER_LOCAL_ENGINE` environment variable to the -name of the local engine (e.g. "android_debug_unopt"). If the local engine build -requires a source path, this can be provided by setting the `FLUTTER_LOCAL_ENGINE_SRC_PATH` -environment variable. This second variable is not necessary if the `flutter` and -`engine` checkouts are in adjacent directories. +variant by setting the `FLUTTER_LOCAL_ENGINE` and `FLUTTER_LOCAL_ENGINE_HOST` +environment svariable to the name of the local engines (e.g. `android_debug_unopt` +and `host_debug_unopt`). If the local engine build requires a source path, this +can be provided by setting the `FLUTTER_LOCAL_ENGINE_SRC_PATH` environment +variable. This second variable is not necessary if the `flutter` and `engine` +checkouts are in adjacent directories. ```shell export FLUTTER_LOCAL_ENGINE=android_debug_unopt +export FLUTTER_LOCAL_ENGINE_HOST=host_debug_unopt flutter test test/integration.shard/some_test_case ``` diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh index f4df20b9bf658..3963a80839532 100755 --- a/packages/flutter_tools/bin/macos_assemble.sh +++ b/packages/flutter_tools/bin/macos_assemble.sh @@ -66,9 +66,23 @@ BuildApp() { EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'." EchoError "You can fix this by updating the LOCAL_ENGINE environment variable, or" EchoError "by running:" - EchoError " flutter build macos --local-engine=host_${build_mode}" + EchoError " flutter build macos --local-engine=host_${build_mode} --local-engine-host=host_${build_mode}" EchoError "or" - EchoError " flutter build macos --local-engine=host_${build_mode}_unopt" + EchoError " flutter build macos --local-engine=host_${build_mode}_unopt --local-engine-host=host_${build_mode}_unopt" + EchoError "========================================================================" + exit -1 + fi + fi + if [[ -n "$LOCAL_ENGINE_HOST" ]]; then + if [[ $(echo "$LOCAL_ENGINE_HOST" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then + EchoError "========================================================================" + EchoError "ERROR: Requested build with Flutter local engine at '${LOCAL_ENGINE_HOST}'" + EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'." + EchoError "You can fix this by updating the LOCAL_ENGINE_HOST environment variable, or" + EchoError "by running:" + EchoError " flutter build macos --local-engine=host_${build_mode} --local-engine-host=host_${build_mode}" + EchoError "or" + EchoError " flutter build macos --local-engine=host_${build_mode}_unopt --local-engine-host=host_${build_mode}_unopt" EchoError "========================================================================" exit -1 fi @@ -94,6 +108,9 @@ BuildApp() { if [[ -n "$LOCAL_ENGINE" ]]; then flutter_args+=("--local-engine=${LOCAL_ENGINE}") fi + if [[ -n "$LOCAL_ENGINE_HOST" ]]; then + flutter_args+=("--local-engine-host=${LOCAL_ENGINE_HOST}") + fi flutter_args+=( "assemble" "--no-version-check" @@ -106,6 +123,7 @@ BuildApp() { "-dSplitDebugInfo=${SPLIT_DEBUG_INFO}" "-dTrackWidgetCreation=${TRACK_WIDGET_CREATION}" "-dAction=${ACTION}" + "-dFrontendServerStarterPath=${FRONTEND_SERVER_STARTER_PATH}" "--DartDefines=${DART_DEFINES}" "--ExtraGenSnapshotOptions=${EXTRA_GEN_SNAPSHOT_OPTIONS}" "--ExtraFrontEndOptions=${EXTRA_FRONT_END_OPTIONS}" @@ -127,8 +145,8 @@ BuildApp() { RunCommand "${flutter_args[@]}" } -# Adds the App.framework as an embedded binary and the flutter_assets as -# resources. +# Adds the App.framework as an embedded binary, the flutter_assets as +# resources, and the native assets. EmbedFrameworks() { # Embed App.framework from Flutter into the app (after creating the Frameworks directory # if it doesn't already exist). @@ -147,6 +165,17 @@ EmbedFrameworks() { RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App" RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/FlutterMacOS.framework/FlutterMacOS" fi + + # Copy the native assets. These do not have to be codesigned here because, + # they are already codesigned in buildNativeAssetsMacOS. + local project_path="${SOURCE_ROOT}/.." + if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then + project_path="${FLUTTER_APPLICATION_PATH}" + fi + local native_assets_path="${project_path}/${FLUTTER_BUILD_DIR}/native_assets/macos/" + if [[ -d "$native_assets_path" ]]; then + RunCommand rsync -av --filter "- .DS_Store" --filter "- native_assets.yaml" "${native_assets_path}" "${xcode_frameworks_dir}" + fi } # Main entry point. diff --git a/packages/flutter_tools/bin/tool_backend.dart b/packages/flutter_tools/bin/tool_backend.dart index 411d53e962e53..d98c7055477db 100644 --- a/packages/flutter_tools/bin/tool_backend.dart +++ b/packages/flutter_tools/bin/tool_backend.dart @@ -13,6 +13,7 @@ Future<void> main(List<String> arguments) async { final String? dartDefines = Platform.environment['DART_DEFINES']; final bool dartObfuscation = Platform.environment['DART_OBFUSCATION'] == 'true'; + final String? frontendServerStarterPath = Platform.environment['FRONTEND_SERVER_STARTER_PATH']; final String? extraFrontEndOptions = Platform.environment['EXTRA_FRONT_END_OPTIONS']; final String? extraGenSnapshotOptions = Platform.environment['EXTRA_GEN_SNAPSHOT_OPTIONS']; final String? flutterEngine = Platform.environment['FLUTTER_ENGINE']; @@ -21,6 +22,7 @@ Future<void> main(List<String> arguments) async { ?? pathJoin(<String>['lib', 'main.dart']); final String? codeSizeDirectory = Platform.environment['CODE_SIZE_DIRECTORY']; final String? localEngine = Platform.environment['LOCAL_ENGINE']; + final String? localEngineHost = Platform.environment['LOCAL_ENGINE_HOST']; final String? projectDirectory = Platform.environment['PROJECT_DIR']; final String? splitDebugInfo = Platform.environment['SPLIT_DEBUG_INFO']; final String? bundleSkSLPath = Platform.environment['BUNDLE_SKSL_PATH']; @@ -46,9 +48,22 @@ ERROR: Requested build with Flutter local engine at '$localEngine' This engine is not compatible with FLUTTER_BUILD_MODE: '$buildMode'. You can fix this by updating the LOCAL_ENGINE environment variable, or by running: - flutter build <platform> --local-engine=host_$buildMode + flutter build <platform> --local-engine=<platform>_$buildMode --local-engine-host=host_$buildMode or - flutter build <platform> --local-engine=host_${buildMode}_unopt + flutter build <platform> --local-engine=<platform>_${buildMode}_unopt --local-engine-host=host_${buildMode}_unopt +======================================================================== +'''); + exit(1); + } + if (localEngineHost != null && !localEngineHost.contains(buildMode)) { + stderr.write(''' +ERROR: Requested build with Flutter local engine host at '$localEngineHost' +This engine is not compatible with FLUTTER_BUILD_MODE: '$buildMode'. +You can fix this by updating the LOCAL_ENGINE_HOST environment variable, or +by running: + flutter build <platform> --local-engine=<platform>_$buildMode --local-engine-host=host_$buildMode +or + flutter build <platform> --local-engine=<platform>_$buildMode --local-engine-host=host_${buildMode}_unopt ======================================================================== '''); exit(1); @@ -72,6 +87,7 @@ or '--prefixed-errors', if (flutterEngine != null) '--local-engine-src-path=$flutterEngine', if (localEngine != null) '--local-engine=$localEngine', + if (localEngineHost != null) '--local-engine-host=$localEngineHost', 'assemble', '--no-version-check', '--output=build', @@ -91,6 +107,8 @@ or '--DartDefines=$dartDefines', if (extraGenSnapshotOptions != null) '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', + if (frontendServerStarterPath != null) + '-dFrontendServerStarterPath=$frontendServerStarterPath', if (extraFrontEndOptions != null) '--ExtraFrontEndOptions=$extraFrontEndOptions', target, diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 0486a8741c3dc..6e3fb7174a015 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -171,6 +171,32 @@ class Context { exitApp(-1); } + /// Copies all files from [source] to [destination]. + /// + /// Does not copy `.DS_Store`. + /// + /// If [delete], delete extraneous files from [destination]. + void runRsync( + String source, + String destination, { + List<String> extraArgs = const <String>[], + bool delete = false, + }) { + runSync( + 'rsync', + <String>[ + '-8', // Avoid mangling filenames with encodings that do not match the current locale. + '-av', + if (delete) '--delete', + '--filter', + '- .DS_Store', + ...extraArgs, + source, + destination, + ], + ); + } + // Adds the App.framework as an embedded binary and the flutter_assets as // resources. void embedFlutterFrameworks() { @@ -185,33 +211,46 @@ class Context { xcodeFrameworksDir, ] ); - runSync( - 'rsync', - <String>[ - '-8', // Avoid mangling filenames with encodings that do not match the current locale. - '-av', - '--delete', - '--filter', - '- .DS_Store', - '${environment['BUILT_PRODUCTS_DIR']}/App.framework', - xcodeFrameworksDir, - ], + runRsync( + delete: true, + '${environment['BUILT_PRODUCTS_DIR']}/App.framework', + xcodeFrameworksDir, ); // Embed the actual Flutter.framework that the Flutter app expects to run against, // which could be a local build or an arch/type specific build. - runSync( - 'rsync', - <String>[ - '-av', - '--delete', - '--filter', - '- .DS_Store', - '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', - '$xcodeFrameworksDir/', - ], + runRsync( + delete: true, + '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', + '$xcodeFrameworksDir/', ); + // Copy the native assets. These do not have to be codesigned here because, + // they are already codesigned in buildNativeAssetsMacOS. + final String sourceRoot = environment['SOURCE_ROOT'] ?? ''; + String projectPath = '$sourceRoot/..'; + if (environment['FLUTTER_APPLICATION_PATH'] != null) { + projectPath = environment['FLUTTER_APPLICATION_PATH']!; + } + final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!; + final String nativeAssetsPath = '$projectPath/$flutterBuildDir/native_assets/ios/'; + final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; + if (Directory(nativeAssetsPath).existsSync()) { + if (verbose) { + print('♦ Copying native assets from $nativeAssetsPath.'); + } + runRsync( + extraArgs: <String>[ + '--filter', + '- native_assets.yaml', + ], + nativeAssetsPath, + xcodeFrameworksDir, + ); + } else if (verbose) { + print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist."); + } + addVmServiceBonjourService(); } @@ -345,6 +384,10 @@ class Context { flutterArgs.add('--local-engine=${environment['LOCAL_ENGINE']}'); } + if (environment['LOCAL_ENGINE_HOST'] != null && environment['LOCAL_ENGINE_HOST']!.isNotEmpty) { + flutterArgs.add('--local-engine-host=${environment['LOCAL_ENGINE_HOST']}'); + } + flutterArgs.addAll(<String>[ 'assemble', '--no-version-check', @@ -359,6 +402,7 @@ class Context { '-dTrackWidgetCreation=${environment['TRACK_WIDGET_CREATION'] ?? ''}', '-dDartObfuscation=${environment['DART_OBFUSCATION'] ?? ''}', '-dAction=${environment['ACTION'] ?? ''}', + '-dFrontendServerStarterPath=${environment['FRONTEND_SERVER_STARTER_PATH'] ?? ''}', '--ExtraGenSnapshotOptions=${environment['EXTRA_GEN_SNAPSHOT_OPTIONS'] ?? ''}', '--DartDefines=${environment['DART_DEFINES'] ?? ''}', '--ExtraFrontEndOptions=${environment['EXTRA_FRONT_END_OPTIONS'] ?? ''}', diff --git a/packages/flutter_tools/bin/xcode_debug.js b/packages/flutter_tools/bin/xcode_debug.js index 611ba1ba8fea4..35c39b88b5ae7 100644 --- a/packages/flutter_tools/bin/xcode_debug.js +++ b/packages/flutter_tools/bin/xcode_debug.js @@ -231,6 +231,7 @@ class CommandArguments { * return true. If the flag is not allowed for the current command, will * return `null`. * + * @param {!string} flag * @param {?string} value * @returns {?boolean} * @throws Will throw an error if the flag is allowed and `value` is not diff --git a/packages/flutter_tools/gradle/aar_init_script.gradle b/packages/flutter_tools/gradle/aar_init_script.gradle index 07f8f552993f7..e6fa84cc17fd8 100644 --- a/packages/flutter_tools/gradle/aar_init_script.gradle +++ b/packages/flutter_tools/gradle/aar_init_script.gradle @@ -45,11 +45,18 @@ void configureProject(Project project, String outputDir) { } String storageUrl = System.getenv('FLUTTER_STORAGE_BASE_URL') ?: "https://storage.googleapis.com" + + String engineRealm = Paths.get(getFlutterRoot(project), "bin", "internal", "engine.realm") + .toFile().text.trim() + if (engineRealm) { + engineRealm = engineRealm + "/" + } + // This is a Flutter plugin project. Plugin projects don't apply the Flutter Gradle plugin, // as a result, add the dependency on the embedding. project.repositories { maven { - url "$storageUrl/download.flutter.io" + url "$storageUrl/${engineRealm}download.flutter.io" } } String engineVersion = Paths.get(getFlutterRoot(project), "bin", "internal", "engine.version") diff --git a/packages/flutter_tools/gradle/app_plugin_loader.gradle b/packages/flutter_tools/gradle/app_plugin_loader.gradle index e5cfe9ca55db4..fd450e6b1b4d9 100644 --- a/packages/flutter_tools/gradle/app_plugin_loader.gradle +++ b/packages/flutter_tools/gradle/app_plugin_loader.gradle @@ -2,35 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This file is included from `<app>/android/settings.gradle`, -// so it can be versioned with the Flutter SDK. +// This file exists solely for the compatibility with projects that have +// not migrated to the declarative apply of the Flutter App Plugin Loader Gradle Plugin. -import groovy.json.JsonSlurper - -def flutterProjectRoot = rootProject.projectDir.parentFile - -// If this logic is changed, also change the logic in module_plugin_loader.gradle. -def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies') -if (!pluginsFile.exists()) { - return -} - -def object = new JsonSlurper().parseText(pluginsFile.text) -assert object instanceof Map -assert object.plugins instanceof Map -assert object.plugins.android instanceof List -// Includes the Flutter plugins that support the Android platform. -object.plugins.android.each { androidPlugin -> - assert androidPlugin.name instanceof String - assert androidPlugin.path instanceof String - // Skip plugins that have no native build (such as a Dart-only implementation - // of a federated plugin). - def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true - if (!needsBuild) { - return - } - def pluginDirectory = new File(androidPlugin.path, 'android') - assert pluginDirectory.exists() - include ":${androidPlugin.name}" - project(":${androidPlugin.name}").projectDir = pluginDirectory -} +def pathToThisDirectory = buildscript.sourceFile.parentFile +apply from: "$pathToThisDirectory/src/main/groovy/app_plugin_loader.groovy" diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 517a725f8cb94..1dbb5aab1f842 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -3,12 +3,8 @@ // found in the LICENSE file. plugins { - `groovy-gradle-plugin` -} - -repositories { - google() - mavenCentral() + `java-gradle-plugin` + `groovy` } @@ -22,6 +18,10 @@ gradlePlugin { id = "dev.flutter.flutter-gradle-plugin" implementationClass = "FlutterPlugin" } + create("flutterAppPluginLoaderPlugin") { + id = "dev.flutter.flutter-plugin-loader" + implementationClass = "FlutterAppPluginLoaderPlugin" + } } } diff --git a/packages/flutter_tools/gradle/resolve_dependencies.gradle b/packages/flutter_tools/gradle/resolve_dependencies.gradle index 817b730613141..6355654f2edfb 100644 --- a/packages/flutter_tools/gradle/resolve_dependencies.gradle +++ b/packages/flutter_tools/gradle/resolve_dependencies.gradle @@ -16,11 +16,17 @@ import java.nio.file.Paths String storageUrl = System.getenv('FLUTTER_STORAGE_BASE_URL') ?: "https://storage.googleapis.com" +String engineRealm = Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.realm") + .toFile().text.trim() +if (engineRealm) { + engineRealm = engineRealm + "/" +} + repositories { google() mavenCentral() maven { - url "$storageUrl/download.flutter.io" + url "$storageUrl/${engineRealm}download.flutter.io" } } diff --git a/packages/flutter_tools/gradle/settings.gradle.kts b/packages/flutter_tools/gradle/settings.gradle.kts new file mode 100644 index 0000000000000..f8d3e87ffa2d7 --- /dev/null +++ b/packages/flutter_tools/gradle/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} diff --git a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy new file mode 100644 index 0000000000000..402ab64e62271 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy @@ -0,0 +1,42 @@ +import groovy.json.JsonSlurper +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings + +apply plugin: FlutterAppPluginLoaderPlugin + +class FlutterAppPluginLoaderPlugin implements Plugin<Settings> { + // This string must match _kFlutterPluginsHasNativeBuildKey defined in + // packages/flutter_tools/lib/src/flutter_plugins.dart. + private final String nativeBuildKey = 'native_build' + + @Override + void apply(Settings settings) { + def flutterProjectRoot = settings.settingsDir.parentFile + + // If this logic is changed, also change the logic in module_plugin_loader.gradle. + def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies') + if (!pluginsFile.exists()) { + return + } + + def object = new JsonSlurper().parseText(pluginsFile.text) + assert object instanceof Map + assert object.plugins instanceof Map + assert object.plugins.android instanceof List + // Includes the Flutter plugins that support the Android platform. + object.plugins.android.each { androidPlugin -> + assert androidPlugin.name instanceof String + assert androidPlugin.path instanceof String + // Skip plugins that have no native build (such as a Dart-only implementation + // of a federated plugin). + def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true + if (!needsBuild) { + return + } + def pluginDirectory = new File(androidPlugin.path, 'android') + assert pluginDirectory.exists() + settings.include(":${androidPlugin.name}") + settings.project(":${androidPlugin.name}").projectDir = pluginDirectory + } + } +} diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 31eefbeab31ff..e6b01a40b5772 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -2,14 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import static groovy.io.FileType.FILES - import com.android.build.OutputFile import groovy.json.JsonSlurper -import java.nio.file.Path +import groovy.json.JsonGenerator +import groovy.xml.QName import java.nio.file.Paths -import java.util.regex.Matcher -import java.util.regex.Pattern +import java.util.Set import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask import org.gradle.api.GradleException @@ -50,13 +48,18 @@ class FlutterExtension { /** Sets the minSdkVersion used by default in Flutter app projects. */ static int minSdkVersion = 19 - /** Sets the targetSdkVersion used by default in Flutter app projects. */ + /** + * Sets the targetSdkVersion used by default in Flutter app projects. + * targetSdkVersion should always be the latest available stable version. + * + * See https://developer.android.com/guide/topics/manifest/uses-sdk-element. + */ static int targetSdkVersion = 33 /** * Sets the ndkVersion used by default in Flutter app projects. * Chosen as default version of the AGP version below as found in - * https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp + * https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp. */ static String ndkVersion = "23.1.7779620" @@ -161,9 +164,11 @@ class FlutterPlugin implements Plugin<Project> { private File flutterRoot private File flutterExecutable private String localEngine + private String localEngineHost private String localEngineSrcPath private Properties localProperties private String engineVersion + private String engineRealm /** * Flutter Docs Website URLs for help messages. @@ -189,11 +194,29 @@ class FlutterPlugin implements Plugin<Project> { } } + String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT) + if (flutterRootPath == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.") + } + flutterRoot = project.file(flutterRootPath) + if (!flutterRoot.isDirectory()) { + throw new GradleException("flutter.sdk must point to the Flutter SDK directory") + } + + engineVersion = useLocalEngine() + ? "+" // Match any version since there's only one. + : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim() + + engineRealm = Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.realm").toFile().text.trim() + if (engineRealm) { + engineRealm = engineRealm + "/" + } + // Configure the Maven repository. String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST String repository = useLocalEngine() ? project.property('local-engine-repo') - : "$hostedRepository/download.flutter.io" + : "$hostedRepository/${engineRealm}download.flutter.io" rootProject.allprojects { repositories { maven { @@ -243,19 +266,6 @@ class FlutterPlugin implements Plugin<Project> { } } - String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT) - if (flutterRootPath == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.") - } - flutterRoot = project.file(flutterRootPath) - if (!flutterRoot.isDirectory()) { - throw new GradleException("flutter.sdk must point to the Flutter SDK directory") - } - - engineVersion = useLocalEngine() - ? "+" // Match any version since there's only one. - : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim() - String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter" flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile(); @@ -315,6 +325,13 @@ class FlutterPlugin implements Plugin<Project> { } localEngine = engineOut.name localEngineSrcPath = engineOut.parentFile.parent + + String engineHostOutPath = project.property('local-engine-host-out') + File engineHostOut = project.file(engineHostOutPath) + if (!engineHostOut.isDirectory()) { + throw new GradleException('local-engine-host-out must point to a local engine host build') + } + localEngineHost = engineHostOut.name } project.android.buildTypes.all this.&addFlutterDependencies } @@ -733,80 +750,90 @@ class FlutterPlugin implements Plugin<Project> { } } - // Add a task that can be called on Flutter projects that prints application id of a build - // variant. + // Add a task that can be called on Flutter projects that outputs app link related project + // settings into a json file. // - // This task prints the application id in this format: + // See https://developer.android.com/training/app-links/ for more information about app link. // - // ApplicationId: com.example.my_id + // The json will be stored in <project>/build/app/app-link-settings-<variant>.json // - // Format of the output of this task is used by `AndroidProject.getApplicationIdForVariant`. - private static void addTasksForPrintApplicationId(Project project) { - project.android.applicationVariants.all { variant -> - // Warning: The name of this task is used by `AndroidProject.getApplicationIdForVariant`. - project.tasks.register("print${variant.name.capitalize()}ApplicationId") { - description "Prints out application id for the given build variant of this Android project" - doLast { - println "ApplicationId: ${variant.applicationId}"; - } - } - } - } - - // Add a task that can be called on Flutter projects that prints app link domains of a build - // variant. - // - // The app link domains refer to the host attributes of data tags in the apps' intent filters - // that support http/https schemes. See - // https://developer.android.com/guide/topics/manifest/intent-filter-element. + // An example json: + // { + // applicationId: "com.example.app", + // deeplinks: [ + // {"scheme":"http", "host":"example.com", "path":".*"}, + // {"scheme":"https","host":"example.com","path":".*"} + // ] + // } // - // This task prints app link domains in this format: - // - // Domain: domain.com - // Domain: another-domain.dev - // - // Format of the output of this task is used by `AndroidProject.getAppLinkDomainsForVariant`. - private static void addTasksForPrintAppLinkDomains(Project project) { + // The output file is parsed and used by devtool. + private static void addTasksForOutputsAppLinkSettings(Project project) { project.android.applicationVariants.all { variant -> - // Warning: The name of this task is used by `AndroidProject.getAppLinkDomainsForVariant`. - project.tasks.register("print${variant.name.capitalize()}AppLinkDomains") { - description "Prints out app links domain for the given build variant of this Android project" + // Warning: The name of this task is used by AndroidBuilder.outputsAppLinkSettings + project.tasks.register("output${variant.name.capitalize()}AppLinkSettings") { + description "stores app links settings for the given build variant of this Android project into a json file." variant.outputs.all { output -> + // Deeplinks are defined in AndroidManifest.xml and is only available after + // `processResourcesProvider`. def processResources = output.hasProperty("processResourcesProvider") ? output.processResourcesProvider.get() : output.processResources dependsOn processResources.name } doLast { + def appLinkSettings = new AppLinkSettings() + appLinkSettings.applicationId = variant.applicationId + appLinkSettings.deeplinks = [] as Set<Deeplink> variant.outputs.all { output -> def processResources = output.hasProperty("processResourcesProvider") ? output.processResourcesProvider.get() : output.processResources def manifest = new XmlParser().parse(processResources.manifestFile) manifest.application.activity.each { activity -> - // Find intent filters that have autoVerify = true and support http/https - // scheme. - activity.'intent-filter'.findAll { filter -> - def hasAutoVerify = filter.attributes().any { entry -> - return entry.key.getLocalPart() == "autoVerify" && entry.value - } - def hasHttpOrHttps = filter.data.any { data -> - data.attributes().any { entry -> - return entry.key.getLocalPart() == "scheme" && - (entry.value == "http" || entry.value == "https") - } - } - return hasAutoVerify && hasHttpOrHttps - }.each { appLinkIntent -> + activity.'intent-filter'.each { appLinkIntent -> // Print out the host attributes in data tags. + def schemes = [] as Set<String> + def hosts = [] as Set<String> + def paths = [] as Set<String> appLinkIntent.data.each { data -> data.attributes().each { entry -> - if (entry.key.getLocalPart() == "host") { - println "Domain: ${entry.value}" + if (entry.key instanceof QName) { + switch (entry.key.getLocalPart()) { + case "scheme": + schemes.add(entry.value) + break + case "host": + hosts.add(entry.value) + break + case "pathAdvancedPattern": + case "pathPattern": + case "path": + paths.add(entry.value) + break + case "pathPrefix": + paths.add("${entry.value}.*") + break + case "pathSuffix": + paths.add(".*${entry.value}") + break + } + } + } + } + schemes.each {scheme -> + hosts.each { host -> + if (!paths) { + appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: ".*")) + } else { + paths.each { path -> + appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: path)) + } } } } } } } + def generator = new JsonGenerator.Options().build() + new File(project.buildDir, "app-link-settings-${variant.name}.json").write(generator.toJson(appLinkSettings)) } } } @@ -940,6 +967,10 @@ class FlutterPlugin implements Plugin<Project> { if (project.hasProperty('track-widget-creation')) { trackWidgetCreationValue = project.property('track-widget-creation').toBoolean() } + String frontendServerStarterPathValue = null + if (project.hasProperty('frontend-server-starter-path')) { + frontendServerStarterPathValue = project.property('frontend-server-starter-path') + } String extraFrontEndOptionsValue = null if (project.hasProperty('extra-front-end-options')) { extraFrontEndOptionsValue = project.property('extra-front-end-options') @@ -987,8 +1018,7 @@ class FlutterPlugin implements Plugin<Project> { addTaskForJavaVersion(project) if(isFlutterAppProject()) { addTaskForPrintBuildVariants(project) - addTasksForPrintApplicationId(project) - addTasksForPrintAppLinkDomains(project) + addTasksForOutputsAppLinkSettings(project) } def targetPlatforms = getTargetPlatforms() def addFlutterDeps = { variant -> @@ -1018,6 +1048,7 @@ class FlutterPlugin implements Plugin<Project> { flutterExecutable this.flutterExecutable buildMode variantBuildMode localEngine this.localEngine + localEngineHost this.localEngineHost localEngineSrcPath this.localEngineSrcPath targetPath getFlutterTarget() verbose this.isVerbose() @@ -1028,6 +1059,7 @@ class FlutterPlugin implements Plugin<Project> { targetPlatformValues = targetPlatforms sourceDir getFlutterSourceDirectory() intermediateDir project.file("${project.buildDir}/$INTERMEDIATES_DIR/flutter/${variant.name}/") + frontendServerStarterPath frontendServerStarterPathValue extraFrontEndOptions extraFrontEndOptionsValue extraGenSnapshotOptions extraGenSnapshotOptionsValue splitDebugInfo splitDebugInfoValue @@ -1236,6 +1268,24 @@ class FlutterPlugin implements Plugin<Project> { } } +class AppLinkSettings { + String applicationId + Set<Deeplink> deeplinks +} + +class Deeplink { + String scheme, host, path + boolean equals(o) { + if (o == null) + throw new NullPointerException() + if (o.getClass() != getClass()) + return false + return scheme == o.scheme && + host == o.host && + path == o.path + } +} + abstract class BaseFlutterTask extends DefaultTask { @Internal File flutterRoot @@ -1246,6 +1296,8 @@ abstract class BaseFlutterTask extends DefaultTask { @Optional @Input String localEngine @Optional @Input + String localEngineHost + @Optional @Input String localEngineSrcPath @Optional @Input Boolean fastStart @@ -1266,6 +1318,8 @@ abstract class BaseFlutterTask extends DefaultTask { @Internal File intermediateDir @Optional @Input + String frontendServerStarterPath + @Optional @Input String extraFrontEndOptions @Optional @Input String extraGenSnapshotOptions @@ -1324,6 +1378,9 @@ abstract class BaseFlutterTask extends DefaultTask { args "--local-engine", localEngine args "--local-engine-src-path", localEngineSrcPath } + if (localEngineHost != null) { + args "--local-engine-host", localEngineHost + } if (verbose) { args "--verbose" } else { @@ -1367,6 +1424,9 @@ abstract class BaseFlutterTask extends DefaultTask { if (extraGenSnapshotOptions != null) { args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" } + if (frontendServerStarterPath != null) { + args "-dFrontendServerStarterPath=${frontendServerStarterPath}" + } if (extraFrontEndOptions != null) { args "--ExtraFrontEndOptions=${extraFrontEndOptions}" } diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 2fb826f17d10f..9bd3ea190d1f7 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -124,6 +124,14 @@ Future<void> main(List<String> args) async { windows: globals.platform.isWindows, ); }, + Terminal: () { + return AnsiTerminal( + stdio: globals.stdio, + platform: globals.platform, + now: DateTime.now(), + isCliAnimationEnabled: featureFlags.isCliAnimationEnabled, + ); + }, PreRunValidator: () => PreRunValidator(fileSystem: globals.fs), }, shutdownHooks: globals.shutdownHooks, @@ -156,7 +164,6 @@ List<FlutterCommand> generateCommands({ AssembleCommand(verboseHelp: verboseHelp, buildSystem: globals.buildSystem), AttachCommand( verboseHelp: verboseHelp, - artifacts: globals.artifacts, stdio: globals.stdio, logger: globals.logger, terminal: globals.terminal, diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart index 50f58d53b428a..262eed522926b 100644 --- a/packages/flutter_tools/lib/runner.dart +++ b/packages/flutter_tools/lib/runner.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - - import 'dart:async'; import 'package:args/command_runner.dart'; @@ -22,7 +20,6 @@ import 'src/context_runner.dart'; import 'src/doctor.dart'; import 'src/globals.dart' as globals; import 'src/reporting/crash_reporting.dart'; -import 'src/reporting/first_run.dart'; import 'src/reporting/reporting.dart'; import 'src/runner/flutter_command.dart'; import 'src/runner/flutter_command_runner.dart'; @@ -63,20 +60,20 @@ Future<int> run( StackTrace? firstStackTrace; return runZoned<Future<int>>(() async { try { - if (args.contains('--disable-telemetry') && - args.contains('--enable-telemetry')) { + if (args.contains('--disable-analytics') && + args.contains('--enable-analytics')) { throwToolExit( - 'Both enable and disable telemetry commands were detected ' + 'Both enable and disable analytics commands were detected ' 'when only one can be supplied per invocation.', exitCode: 1); } - // Disable analytics if user passes in the `--disable-telemetry` option - // `flutter --disable-telemetry` + // Disable analytics if user passes in the `--disable-analytics` option + // "flutter --disable-analytics" // - // Same functionality as `flutter config --no-analytics` for disabling + // Same functionality as "flutter config --no-analytics" for disabling // except with the `value` hard coded as false - if (args.contains('--disable-telemetry')) { + if (args.contains('--disable-analytics')) { // The tool sends the analytics event *before* toggling the flag // intentionally to be sure that opt-out events are sent correctly. AnalyticsConfigEvent(enabled: false).send(); @@ -95,12 +92,12 @@ Future<int> run( await globals.analytics.setTelemetry(false); } - // Enable analytics if user passes in the `--enable-telemetry` option - // `flutter --enable-telemetry` + // Enable analytics if user passes in the `--enable-analytics` option + // `flutter --enable-analytics` // // Same functionality as `flutter config --analytics` for enabling // except with the `value` hard coded as true - if (args.contains('--enable-telemetry')) { + if (args.contains('--enable-analytics')) { // The tool sends the analytics event *before* toggling the flag // intentionally to be sure that opt-out events are sent correctly. AnalyticsConfigEvent(enabled: true).send(); @@ -117,7 +114,7 @@ Future<int> run( // Triggering [runZoned]'s error callback does not necessarily mean that // we stopped executing the body. See https://github.com/dart-lang/sdk/issues/42150. if (firstError == null) { - return await _exit(0, shutdownHooks: shutdownHooks); + return await exitWithHooks(0, shutdownHooks: shutdownHooks); } // We already hit some error, so don't return success. The error path @@ -153,7 +150,7 @@ Future<int> _handleToolError( globals.printError('${error.message}\n'); globals.printError("Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options."); // Argument error exit code. - return _exit(64, shutdownHooks: shutdownHooks); + return exitWithHooks(64, shutdownHooks: shutdownHooks); } else if (error is ToolExit) { if (error.message != null) { globals.printError(error.message!); @@ -161,14 +158,14 @@ Future<int> _handleToolError( if (verbose) { globals.printError('\n$stackTrace\n'); } - return _exit(error.exitCode ?? 1, shutdownHooks: shutdownHooks); + return exitWithHooks(error.exitCode ?? 1, shutdownHooks: shutdownHooks); } else if (error is ProcessExit) { // We've caught an exit code. if (error.immediate) { exit(error.exitCode); return error.exitCode; } else { - return _exit(error.exitCode, shutdownHooks: shutdownHooks); + return exitWithHooks(error.exitCode, shutdownHooks: shutdownHooks); } } else { // We've crashed; emit a log report. @@ -178,7 +175,7 @@ Future<int> _handleToolError( // Print the stack trace on the bots - don't write a crash report. globals.stdio.stderrWrite('$error\n'); globals.stdio.stderrWrite('$stackTrace\n'); - return _exit(1, shutdownHooks: shutdownHooks); + return exitWithHooks(1, shutdownHooks: shutdownHooks); } // Report to both [Usage] and [CrashReportSender]. @@ -219,7 +216,7 @@ Future<int> _handleToolError( final File file = await _createLocalCrashReport(details); await globals.crashReporter!.informUser(details, file); - return _exit(1, shutdownHooks: shutdownHooks); + return exitWithHooks(1, shutdownHooks: shutdownHooks); // This catch catches all exceptions to ensure the message below is printed. } catch (error, st) { // ignore: avoid_catches_without_on_clauses globals.stdio.stderrWrite( @@ -285,76 +282,3 @@ Future<File> _createLocalCrashReport(CrashDetails details) async { return crashFile; } - -Future<int> _exit(int code, {required ShutdownHooks shutdownHooks}) async { - // Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()` - // before invoking the print welcome method because the print welcome method - // will set `messenger.shouldDisplayLicenseTerms()` to false - final FirstRunMessenger messenger = - FirstRunMessenger(persistentToolState: globals.persistentToolState!); - final bool legacyAnalyticsMessageShown = - messenger.shouldDisplayLicenseTerms(); - - // Prints the welcome message if needed for legacy analytics. - globals.flutterUsage.printWelcome(); - - // Ensure that the consent message has been displayed for unified analytics - if (globals.analytics.shouldShowMessage) { - globals.logger.printStatus(globals.analytics.getConsentMessage); - if (!globals.flutterUsage.enabled) { - globals.printStatus( - 'Please note that analytics reporting was already disabled, ' - 'and will continue to be disabled.\n'); - } - - // Because the legacy analytics may have also sent a message, - // the conditional below will print additional messaging informing - // users that the two consent messages they are receiving is not a - // bug - if (legacyAnalyticsMessageShown) { - globals.logger - .printStatus('You have received two consent messages because ' - 'the flutter tool is migrating to a new analytics system. ' - 'Disabling analytics collection will disable both the legacy ' - 'and new analytics collection systems. ' - 'You can disable analytics reporting by running `flutter --disable-telemetry`\n'); - } - - // Invoking this will onboard the flutter tool onto - // the package on the developer's machine and will - // allow for events to be sent to Google Analytics - // on subsequent runs of the flutter tool (ie. no events - // will be sent on the first run to allow developers to - // opt out of collection) - globals.analytics.clientShowedMessage(); - } - - // Send any last analytics calls that are in progress without overly delaying - // the tool's exit (we wait a maximum of 250ms). - if (globals.flutterUsage.enabled) { - final Stopwatch stopwatch = Stopwatch()..start(); - await globals.flutterUsage.ensureAnalyticsSent(); - globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms'); - } - - // Run shutdown hooks before flushing logs - await shutdownHooks.runShutdownHooks(globals.logger); - - final Completer<void> completer = Completer<void>(); - - // Give the task / timer queue one cycle through before we hard exit. - Timer.run(() { - try { - globals.printTrace('exiting with code $code'); - exit(code); - completer.complete(); - // This catches all exceptions because the error is propagated on the - // completer. - } catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses - completer.completeError(error, stackTrace); - } - }); - - await completer.future; - return code; -} diff --git a/packages/flutter_tools/lib/src/android/README.md b/packages/flutter_tools/lib/src/android/README.md new file mode 100644 index 0000000000000..fbc870bac46ee --- /dev/null +++ b/packages/flutter_tools/lib/src/android/README.md @@ -0,0 +1,94 @@ +# Flutter Tools for Android + +This section of the Flutter repository contains the command line developer tools +for building Flutter applications on Android. What follows are some notes about +updating this part of the tool. + +## Updating Android dependencies +The Android dependencies that Flutter uses to run on Android +include the Android NDK and SDK versions, Gradle, the Kotlin Gradle Plugin, +and the Android Gradle Plugin (AGP). The template versions of these +dependencies can be found in [gradle_utils.dart](gradle_utils.dart). + +Follow the guides below when*... + +### Updating the template version of... + +#### The Android SDK & NDK +All of the Android SDK/NDK versions noted in `gradle_utils.dart` +(`compileSdkVersion`, `minSdkVersion`, `targetSdkVersion`, `ndkVersion`) +versions should match the values in Flutter Gradle Plugin (`FlutterExtension`), +so updating any of these versions also requires an update in +[flutter.groovy](../../../gradle/src/main/groovy/flutter.groovy). + +When updating the Android `compileSdkVersion`, `minSdkVersion`, or +`targetSdkVersion`, make sure that: +- Framework integration & benchmark tests are running with at least that SDK +version. +- Flutter tools tests that perform String checks with the current template +SDK verisons are updated (you should see these fail if you do not fix them +preemptively). + +#### Gradle +When updating the Gradle version used in project templates +(`templateDefaultGradleVersion`), make sure that: +- Framework integration & benchmark tests are running with at least this Gradle +version. +- Flutter tools tests that perform String checks with the current template +Gradle version are updated (you should see these fail if you do not fix them +preemptively). + +#### The Kotlin Gradle Plugin +When updating the Kotlin Gradle Plugin (KGP) version used in project templates +(`templateKotlinGradlePluginVersion`), make sure that the framework integration +& benchmark tests are running with at least this KGP version. + +For information aboout the latest version, check https://kotlinlang.org/docs/releases.html#release-details. + +#### The Android Gradle Plugin (AGP) +When updating the Android Gradle Plugin (AGP) versions used in project templates +(`templateAndroidGradlePluginVersion`, `templateAndroidGradlePluginVersionForModule`), +make sure that: +- Framework integration & benchmark tests are running with at least this AGP +version. +- Flutter tools tests that perform String checks with the current template +AGP verisons are updated (you should see these fail if you do not fix them +preemptively). + +### A new version becomes available for... + +#### Gradle +When new versions of Gradle become available, make sure to: +- Check if the maximum version of Gradle that we support +(`maxKnownAndSupportedGradleVersion`) can be updated, and if so, take the +necessary steps to ensure we are testing this version in CI. +- Check that the Java version that is one higher than we currently support +(`oneMajorVersionHigherJavaVersion`) based on current maximum supported +Gradle version is up-to-date. +- Update the `_javaGradleCompatList` that contains the Java/Gradle +compatibility information known to the tool. +- Update the test cases in [gradle_utils_test.dart](../../..test/general.shard/android/gradle_utils_test.dart) that test compatibility between Java and Gradle versions +(relevant tests should fail if you do not fix them preemptively, but should also +be marked inline). +- Update the test cases in [create_test.dart](../../../test/commands.shard/permeable/create_test.dart) that test for a warning for Java/Gradle incompatibilities as needed +(relevant tests should fail if you do not fix them preemptively). + +For more information about the latest version, check https://gradle.org/releases/. + +#### The Android Gradle Plugin (AGP) +When new versions of the Android Gradle Plugin become available, make sure to: +- Update the maximum version of AGP that we know of (`maxKnownAgpVersion`). +- Check if the maximum version of AGP that we support +(`maxKnownAndSupportedAgpVersion`) can be updated, and if so, take the necessary +steps to ensure that we are testing this version in CI. +- Update the `_javaAgpCompatList` that contains the Java/AGP compatibility +information known to the tool. +- Update the test cases in [gradle_utils_test.dart](../../..test/general.shard/android/gradle_utils_test.dart) that test compatibility between Java and AGP versions +(relevant tests should fail if you do not fix them preemptively, but should also +be marked inline). +- Update the test cases in [create_test.dart](../../../test/commands.shard/permeable/create_test.dart) that test for a warning for Java/AGP incompatibilities as needed +(relevant tests should fail if you do not fix them preemptively). + +For information about the latest version, check https://developer.android.com/studio/releases/gradle-plugin#updating-gradle. + +\* There is an ongoing effort to reduce these steps; see https://github.com/flutter/flutter/issues/134780. diff --git a/packages/flutter_tools/lib/src/android/android_app_link_settings.dart b/packages/flutter_tools/lib/src/android/android_app_link_settings.dart deleted file mode 100644 index 8769fd883f3c6..0000000000000 --- a/packages/flutter_tools/lib/src/android/android_app_link_settings.dart +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:meta/meta.dart'; - -/// A data class for app links related project settings. -/// -/// See https://developer.android.com/training/app-links. -@immutable -class AndroidAppLinkSettings { - const AndroidAppLinkSettings({ - required this.applicationId, - required this.domains, - }); - - /// The application id of the android sub-project. - final String applicationId; - - /// The associated web domains of the android sub-project. - final List<String> domains; -} diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart index 70a7d9ff3bd22..0ece6ceaaf09a 100644 --- a/packages/flutter_tools/lib/src/android/android_builder.dart +++ b/packages/flutter_tools/lib/src/android/android_builder.dart @@ -43,14 +43,8 @@ abstract class AndroidBuilder { /// Returns a list of available build variant from the Android project. Future<List<String>> getBuildVariants({required FlutterProject project}); - /// Returns the application id for the given build variant. - Future<String> getApplicationIdForVariant( - String buildVariant, { - required FlutterProject project, - }); - - /// Returns a list of app link domains for the given build variant. - Future<List<String>> getAppLinkDomainsForVariant( + /// Outputs app link related project settings into a json file. + Future<void> outputsAppLinkSettings( String buildVariant, { required FlutterProject project, }); diff --git a/packages/flutter_tools/lib/src/android/android_device_discovery.dart b/packages/flutter_tools/lib/src/android/android_device_discovery.dart index c9655a11e9fab..1a5dd74d7dcaf 100644 --- a/packages/flutter_tools/lib/src/android/android_device_discovery.dart +++ b/packages/flutter_tools/lib/src/android/android_device_discovery.dart @@ -105,8 +105,8 @@ class AndroidDevices extends PollingDeviceDiscovery { bool _doesNotHaveAdb() { return _androidSdk == null || - _androidSdk?.adbPath == null || - !_processManager.canRun(_androidSdk!.adbPath); + _androidSdk.adbPath == null || + !_processManager.canRun(_androidSdk.adbPath); } // 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart index 204bc12a5fd83..bb92bb5be9aba 100644 --- a/packages/flutter_tools/lib/src/android/android_emulator.dart +++ b/packages/flutter_tools/lib/src/android/android_emulator.dart @@ -141,7 +141,7 @@ class AndroidEmulator extends Emulator { @override PlatformType get platformType => PlatformType.android; - String? _prop(String name) => _properties != null ? _properties![name] : null; + String? _prop(String name) => _properties != null ? _properties[name] : null; @override Future<void> launch({@visibleForTesting Duration? startupDuration, bool coldBoot = false}) async { diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 40b96eb33056b..8bc53d099b7c5 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -53,12 +53,12 @@ class AndroidWorkflow implements Workflow { @override bool get canListDevices => appliesToHostPlatform && _androidSdk != null - && _androidSdk?.adbPath != null; + && _androidSdk.adbPath != null; @override bool get canLaunchDevices => appliesToHostPlatform && _androidSdk != null - && _androidSdk?.adbPath != null - && (_androidSdk?.validateSdkWellFormed().isEmpty ?? false); + && _androidSdk.adbPath != null + && _androidSdk.validateSdkWellFormed().isEmpty; @override bool get canListEmulators => canListDevices && _androidSdk?.emulatorPath != null; @@ -105,13 +105,13 @@ class AndroidValidator extends DoctorValidator { return false; } messages.add(ValidationMessage(_userMessages.androidJdkLocation(_java!.binaryPath))); - if (!_java!.canRun()) { - messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(_java!.binaryPath))); + if (!_java.canRun()) { + messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(_java.binaryPath))); return false; } Version? javaVersion; try { - javaVersion = _java!.version; + javaVersion = _java.version; } on Exception catch (error) { _logger.printTrace(error.toString()); } @@ -253,13 +253,13 @@ class AndroidLicenseValidator extends DoctorValidator { final List<ValidationMessage> messages = <ValidationMessage>[]; // Match pre-existing early termination behavior - if (_androidSdk == null || _androidSdk?.latestVersion == null || - _androidSdk!.validateSdkWellFormed().isNotEmpty || + if (_androidSdk == null || _androidSdk.latestVersion == null || + _androidSdk.validateSdkWellFormed().isNotEmpty || ! await _checkJavaVersionNoOutput()) { return ValidationResult(ValidationType.missing, messages); } - final String sdkVersionText = _userMessages.androidStatusInfo(_androidSdk!.latestVersion!.buildToolsVersionName); + final String sdkVersionText = _userMessages.androidStatusInfo(_androidSdk.latestVersion!.buildToolsVersionName); // Check for licenses. switch (await licensesAccepted) { @@ -371,7 +371,7 @@ class AndroidLicenseValidator extends DoctorValidator { try { final Process process = await _processManager.start( - <String>[_androidSdk!.sdkManagerPath!, '--licenses'], + <String>[_androidSdk.sdkManagerPath!, '--licenses'], environment: _java?.environment, ); @@ -404,7 +404,7 @@ class AndroidLicenseValidator extends DoctorValidator { final int exitCode = await process.exitCode; if (exitCode != 0) { throwToolExit(_userMessages.androidCannotRunSdkManager( - _androidSdk?.sdkManagerPath ?? '', + _androidSdk.sdkManagerPath ?? '', 'exited code $exitCode', _platform, )); @@ -412,7 +412,7 @@ class AndroidLicenseValidator extends DoctorValidator { return true; } on ProcessException catch (e) { throwToolExit(_userMessages.androidCannotRunSdkManager( - _androidSdk?.sdkManagerPath ?? '', + _androidSdk.sdkManagerPath ?? '', e.toString(), _platform, )); diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 6d1e3e7345b12..c116923739fd8 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -35,6 +35,7 @@ import 'gradle_errors.dart'; import 'gradle_utils.dart'; import 'java.dart'; import 'migrations/android_studio_java_gradle_conflict_migration.dart'; +import 'migrations/min_sdk_version_migration.dart'; import 'migrations/top_level_gradle_build_file_migration.dart'; import 'multidex.dart'; @@ -51,31 +52,8 @@ final RegExp _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRege const String _kBuildVariantRegexGroupName = 'variant'; const String _kBuildVariantTaskName = 'printBuildVariants'; -/// The regex to grab variant names from print${BuildVariant}ApplicationId gradle task -/// -/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy -/// -/// The expected output from the task should be similar to: -/// -/// ApplicationId: com.example.my_id -final RegExp _kApplicationIdRegex = RegExp('^ApplicationId: (?<$_kApplicationIdRegexGroupName>.*)\$'); -const String _kApplicationIdRegexGroupName = 'applicationId'; -String _getPrintApplicationIdTaskFor(String buildVariant) { - return _taskForBuildVariant('print', buildVariant, 'ApplicationId'); -} - -/// The regex to grab app link domains from print${BuildVariant}AppLinkDomains gradle task -/// -/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy -/// -/// The expected output from the task should be similar to: -/// -/// Domain: domain.com -/// Domain: another-domain.dev -final RegExp _kAppLinkDomainsRegex = RegExp('^Domain: (?<$_kAppLinkDomainsGroupName>.*)\$'); -const String _kAppLinkDomainsGroupName = 'domain'; -String _getPrintAppLinkDomainsTaskFor(String buildVariant) { - return _taskForBuildVariant('print', buildVariant, 'AppLinkDomains'); +String _getOutputAppLinkSettingsTaskFor(String buildVariant) { + return _taskForBuildVariant('output', buildVariant, 'AppLinkSettings'); } /// The directory where the APK artifact is generated. @@ -330,8 +308,8 @@ class AndroidGradleBuilder implements AndroidBuilder { AndroidStudioJavaGradleConflictMigration(_logger, project: project.android, androidStudio: _androidStudio, - java: globals.java) - , + java: globals.java), + MinSdkVersionMigration(project.android, _logger), ]; final ProjectMigration migration = ProjectMigration(migrators); @@ -389,19 +367,20 @@ class AndroidGradleBuilder implements AndroidBuilder { final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo; if (localEngineInfo != null) { final Directory localEngineRepo = _getLocalEngineRepo( - engineOutPath: localEngineInfo.engineOutPath, + engineOutPath: localEngineInfo.targetOutPath, androidBuildInfo: androidBuildInfo, fileSystem: _fileSystem, ); _logger.printTrace( - 'Using local engine: ${localEngineInfo.engineOutPath}\n' + 'Using local engine: ${localEngineInfo.targetOutPath}\n' 'Local Maven repo: ${localEngineRepo.path}' ); command.add('-Plocal-engine-repo=${localEngineRepo.path}'); command.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); - command.add('-Plocal-engine-out=${localEngineInfo.engineOutPath}'); + command.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}'); + command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}'); command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( - localEngineInfo.engineOutPath)}'); + localEngineInfo.targetOutPath)}'); } else if (androidBuildInfo.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo .targetArchs @@ -714,17 +693,18 @@ class AndroidGradleBuilder implements AndroidBuilder { final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo; if (localEngineInfo != null) { final Directory localEngineRepo = _getLocalEngineRepo( - engineOutPath: localEngineInfo.engineOutPath, + engineOutPath: localEngineInfo.targetOutPath, androidBuildInfo: androidBuildInfo, fileSystem: _fileSystem, ); _logger.printTrace( - 'Using local engine: ${localEngineInfo.engineOutPath}\n' + 'Using local engine: ${localEngineInfo.targetOutPath}\n' 'Local Maven repo: ${localEngineRepo.path}' ); command.add('-Plocal-engine-repo=${localEngineRepo.path}'); command.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); - command.add('-Plocal-engine-out=${localEngineInfo.engineOutPath}'); + command.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}'); + command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}'); // Copy the local engine repo in the output directory. try { @@ -739,7 +719,7 @@ class AndroidGradleBuilder implements AndroidBuilder { ); } command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( - localEngineInfo.engineOutPath)}'); + localEngineInfo.targetOutPath)}'); } else if (androidBuildInfo.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo.targetArchs .map((AndroidArch e) => e.platformName).join(','); @@ -814,11 +794,11 @@ class AndroidGradleBuilder implements AndroidBuilder { } @override - Future<String> getApplicationIdForVariant( + Future<void> outputsAppLinkSettings( String buildVariant, { required FlutterProject project, }) async { - final String taskName = _getPrintApplicationIdTaskFor(buildVariant); + final String taskName = _getOutputAppLinkSettingsTaskFor(buildVariant); final Stopwatch sw = Stopwatch() ..start(); final RunResult result = await _runGradleTask( @@ -826,50 +806,12 @@ class AndroidGradleBuilder implements AndroidBuilder { options: const <String>['-q'], project: project, ); - _usage.sendTiming('print', 'application id', sw.elapsed); + _usage.sendTiming('outputs', 'app link settings', sw.elapsed); if (result.exitCode != 0) { _logger.printStatus(result.stdout, wrap: false); _logger.printError(result.stderr, wrap: false); - return ''; - } - for (final String line in LineSplitter.split(result.stdout)) { - final RegExpMatch? match = _kApplicationIdRegex.firstMatch(line); - if (match != null) { - return match.namedGroup(_kApplicationIdRegexGroupName)!; - } - } - return ''; - } - - @override - Future<List<String>> getAppLinkDomainsForVariant( - String buildVariant, { - required FlutterProject project, - }) async { - final String taskName = _getPrintAppLinkDomainsTaskFor(buildVariant); - final Stopwatch sw = Stopwatch() - ..start(); - final RunResult result = await _runGradleTask( - taskName, - options: const <String>['-q'], - project: project, - ); - _usage.sendTiming('print', 'application id', sw.elapsed); - - if (result.exitCode != 0) { - _logger.printStatus(result.stdout, wrap: false); - _logger.printError(result.stderr, wrap: false); - return const <String>[]; - } - final List<String> domains = <String>[]; - for (final String line in LineSplitter.split(result.stdout)) { - final RegExpMatch? match = _kAppLinkDomainsRegex.firstMatch(line); - if (match != null) { - domains.add(match.namedGroup(_kAppLinkDomainsGroupName)!); - } } - return domains; } } diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index 12d83a7ded7c2..ebf92513ee715 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -12,6 +12,7 @@ import '../base/os.dart'; import '../base/platform.dart'; import '../base/utils.dart'; import '../base/version.dart'; +import '../base/version_range.dart'; import '../build_info.dart'; import '../cache.dart'; import '../globals.dart' as globals; @@ -24,56 +25,78 @@ import 'android_sdk.dart'; // In general, Flutter aims to default to the latest version. // However, this currently requires to migrate existing integration tests to the latest supported values. // -// For more information about the latest version, check: -// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle -// https://kotlinlang.org/docs/releases.html#release-details +// Please see the README before changing any of these values. const String templateDefaultGradleVersion = '7.5'; const String templateAndroidGradlePluginVersion = '7.3.0'; -const String templateDefaultGradleVersionForModule = '7.3.0'; +const String templateAndroidGradlePluginVersionForModule = '7.3.0'; const String templateKotlinGradlePluginVersion = '1.7.10'; -// These versions should match the values in Flutter Gradle Plugin (FlutterExtension). -// The Flutter Gradle plugin is only applied to app projects, and modules that are built from source -// using (include_flutter.groovy). -// The remaining projects are: plugins, and modules compiled as AARs. In modules, the ephemeral directory -// `.android` is always regenerated after flutter pub get, so new versions are picked up after a -// Flutter upgrade. +// The Flutter Gradle Plugin is only applied to app projects, and modules that +// are built from source using (`include_flutter.groovy`). The remaining +// projects are: plugins, and modules compiled as AARs. In modules, the +// ephemeral directory `.android` is always regenerated after `flutter pub get`, +// so new versions are picked up after a Flutter upgrade. +// +// Please see the README before changing any of these values. const String compileSdkVersion = '33'; const String minSdkVersion = '19'; const String targetSdkVersion = '33'; const String ndkVersion = '23.1.7779620'; + +// Update these when new major versions of Java are supported by new Gradle +// versions that we support. +// Source of truth: https://docs.gradle.org/current/userguide/compatibility.html +const String oneMajorVersionHigherJavaVersion = '20'; + // Update this when new versions of Gradle come out including minor versions +// and should correspond to the maximum Gradle version we test in CI. // // Supported here means supported by the tooling for // flutter analyze --suggestions and does not imply broader flutter support. -const String _maxKnownAndSupportedGradleVersion = '8.0.2'; +const String maxKnownAndSupportedGradleVersion = '8.0.2'; + // Update this when new versions of AGP come out. +// +// Supported here means tooling is aware of this version's Java <-> AGP +// compatibility. @visibleForTesting -const String maxKnownAgpVersion = '8.1'; +const String maxKnownAndSupportedAgpVersion = '8.1'; + +// Update this when new versions of AGP come out. +const String maxKnownAgpVersion = '8.3'; + +// Oldest documented version of AGP that has a listed minimum +// compatible Java version. +const String oldestDocumentedJavaAgpCompatibilityVersion = '4.2'; // Expected content: // "classpath 'com.android.tools.build:gradle:7.3.0'" // Parentheticals are use to group which helps with version extraction. // "...build:gradle:(...)" where group(1) should be the version string. final RegExp _androidGradlePluginRegExp = - RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)'); + RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)'); // Expected content format (with lines above and below). // Version can have 2 or 3 numbers. // 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip' // '^\s*' protects against commented out lines. final RegExp distributionUrlRegex = - RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true); + RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true); // Modified version of the gradle distribution url match designed to only match // gradle.org urls so that we can guarantee any modifications to the url // still points to a hosted zip. final RegExp gradleOrgVersionMatch = -RegExp( + RegExp( r'^\s*distributionUrl\s*=\s*https\\://services\.gradle\.org/distributions/gradle-((?:\d|\.)+)-(.*)\.zip', multiLine: true -); + ); + +// This matches uncommented minSdkVersion lines in the module-level build.gradle +// file which have minSdkVersion 16,17, or 18 (the Jelly Bean api levels). +final RegExp jellyBeanMinSdkVersionMatch = + RegExp(r'(?<=^\s*)minSdkVersion 1[678](?=\s*(?://|$))', multiLine: true); // From https://docs.gradle.org/current/userguide/command_line_interface.html#command_line_interface const String gradleVersionFlag = r'--version'; @@ -350,13 +373,13 @@ bool validateGradleAndAgp(Logger logger, } // Check highest supported version before checking unknown versions. - if (isWithinVersionRange(agpV, min: '8.0', max: maxKnownAgpVersion)) { + if (isWithinVersionRange(agpV, min: '8.0', max: maxKnownAndSupportedAgpVersion)) { return isWithinVersionRange(gradleV, - min: '8.0', max: _maxKnownAndSupportedGradleVersion); + min: '8.0', max: maxKnownAndSupportedGradleVersion); } // Check if versions are newer than the max known versions. if (isWithinVersionRange(agpV, - min: _maxKnownAndSupportedGradleVersion, max: '100.100')) { + min: maxKnownAndSupportedAgpVersion, max: '100.100')) { // Assume versions we do not know about are valid but log. final bool validGradle = isWithinVersionRange(gradleV, min: '8.0', max: '100.00'); @@ -369,42 +392,42 @@ bool validateGradleAndAgp(Logger logger, // Max agp here is a made up version to contain all 7.4 changes. if (isWithinVersionRange(agpV, min: '7.4', max: '7.5')) { return isWithinVersionRange(gradleV, - min: '7.5', max: _maxKnownAndSupportedGradleVersion); + min: '7.5', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '7.3', max: '7.4', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '7.4', max: _maxKnownAndSupportedGradleVersion); + min: '7.4', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '7.2', max: '7.3', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '7.3.3', max: _maxKnownAndSupportedGradleVersion); + min: '7.3.3', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '7.1', max: '7.2', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '7.2', max: _maxKnownAndSupportedGradleVersion); + min: '7.2', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '7.0', max: '7.1', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '7.0', max: _maxKnownAndSupportedGradleVersion); + min: '7.0', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '4.2.0', max: '7.0', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '6.7.1', max: _maxKnownAndSupportedGradleVersion); + min: '6.7.1', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '4.1.0', max: '4.2.0', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '6.5', max: _maxKnownAndSupportedGradleVersion); + min: '6.5', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange(agpV, min: '4.0.0', max: '4.1.0', inclusiveMax: false)) { return isWithinVersionRange(gradleV, - min: '6.1.1', max: _maxKnownAndSupportedGradleVersion); + min: '6.1.1', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange( agpV, @@ -412,7 +435,7 @@ bool validateGradleAndAgp(Logger logger, max: '3.6.4', )) { return isWithinVersionRange(gradleV, - min: '5.6.4', max: _maxKnownAndSupportedGradleVersion); + min: '5.6.4', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange( agpV, @@ -420,7 +443,7 @@ bool validateGradleAndAgp(Logger logger, max: '3.5.4', )) { return isWithinVersionRange(gradleV, - min: '5.4.1', max: _maxKnownAndSupportedGradleVersion); + min: '5.4.1', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange( agpV, @@ -428,7 +451,7 @@ bool validateGradleAndAgp(Logger logger, max: '3.4.3', )) { return isWithinVersionRange(gradleV, - min: '5.1.1', max: _maxKnownAndSupportedGradleVersion); + min: '5.1.1', max: maxKnownAndSupportedGradleVersion); } if (isWithinVersionRange( agpV, @@ -436,24 +459,20 @@ bool validateGradleAndAgp(Logger logger, max: '3.3.3', )) { return isWithinVersionRange(gradleV, - min: '4.10.1', max: _maxKnownAndSupportedGradleVersion); + min: '4.10.1', max: maxKnownAndSupportedGradleVersion); } logger.printTrace('Unknown Gradle-Agp compatibility, $gradleV, $agpV'); return false; } -// Validate that the [javaVersion] and Gradle version are compatible with -// each other. -// -// Source of truth: -// https://docs.gradle.org/current/userguide/compatibility.html#java -bool validateJavaGradle(Logger logger, +/// Validate that the [javaVersion] and Gradle version are compatible with +/// each other. +/// +/// Source of truth: +/// https://docs.gradle.org/current/userguide/compatibility.html#java +bool validateJavaAndGradle(Logger logger, {required String? javaV, required String? gradleV}) { - // Update these when new major versions of Java are supported by android. - // Supported means Java <-> Gradle support. - const String oneMajorVersionHigherJavaVersion = '20'; - // https://docs.gradle.org/current/userguide/compatibility.html#java const String oldestSupportedJavaVersion = '1.8'; const String oldestDocumentedJavaGradleCompatibility = '2.0'; @@ -487,7 +506,7 @@ bool validateJavaGradle(Logger logger, // Assume versions Java versions newer than [maxSupportedJavaVersion] // required a higher gradle version. final bool validGradle = isWithinVersionRange(gradleV, - min: _maxKnownAndSupportedGradleVersion, max: '100.00'); + min: maxKnownAndSupportedGradleVersion, max: '100.00'); logger.printWarning( 'Newer than known valid Java version ($javaV), gradle ($gradleV).' '\n Treating as valid configuration.'); @@ -495,82 +514,7 @@ bool validateJavaGradle(Logger logger, } // Begin known Java <-> Gradle evaluation. - final List<JavaGradleCompat> compatList = <JavaGradleCompat>[ - JavaGradleCompat( - javaMin: '19', - javaMax: '20', - gradleMin: '7.6', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '18', - javaMax: '19', - gradleMin: '7.5', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '17', - javaMax: '18', - gradleMin: '7.3', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '16', - javaMax: '17', - gradleMin: '7.0', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '15', - javaMax: '16', - gradleMin: '6.7', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '14', - javaMax: '15', - gradleMin: '6.3', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '13', - javaMax: '14', - gradleMin: '6.0', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '12', - javaMax: '13', - gradleMin: '5.4', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '11', - javaMax: '12', - gradleMin: '5.0', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - // 1.11 is a made up java version to cover everything in 1.10.* - JavaGradleCompat( - javaMin: '1.10', - javaMax: '1.11', - gradleMin: '4.7', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '1.9', - javaMax: '1.10', - gradleMin: '4.3', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - JavaGradleCompat( - javaMin: '1.8', - javaMax: '1.9', - gradleMin: '2.0', - gradleMax: _maxKnownAndSupportedGradleVersion, - ), - ]; - for (final JavaGradleCompat data in compatList) { + for (final JavaGradleCompat data in _javaGradleCompatList) { if (isWithinVersionRange(javaV, min: data.javaMin, max: data.javaMax, inclusiveMax: false)) { return isWithinVersionRange(gradleV, min: data.gradleMin, max: data.gradleMax); } @@ -580,6 +524,105 @@ bool validateJavaGradle(Logger logger, return false; } +/// Returns compatibility information for the valid range of Gradle versions for +/// the specified Java version. +/// +/// Returns null when the tooling has not documented the compatibile Gradle +/// versions for the Java version (either the version is too old or too new). If +/// this seems like a mistake, the caller may need to update the +/// [_javaGradleCompatList] detailing Java/Gradle compatibility. +JavaGradleCompat? getValidGradleVersionRangeForJavaVersion( + Logger logger, { + required String javaV, +}) { + for (final JavaGradleCompat data in _javaGradleCompatList) { + if (isWithinVersionRange(javaV, min: data.javaMin, max: data.javaMax, inclusiveMax: false)) { + return data; + } + } + + logger.printTrace('Unable to determine valid Gradle version range for Java version $javaV.'); + return null; +} + +/// Validate that the specified Java and Android Gradle Plugin (AGP) versions are +/// compatible with each other. +/// +/// Returns true when the specified Java and AGP versions are +/// definitely compatible; otherwise, false is assumed by default. In addition, +/// this will return false when either a null Java or AGP version is provided. +/// +/// Source of truth are the AGP release notes: +/// https://developer.android.com/build/releases/gradle-plugin +bool validateJavaAndAgp(Logger logger, + {required String? javaV, required String? agpV}) { + if (javaV == null || agpV == null) { + logger.printTrace( + 'Java version or AGP version unknown ($javaV, $agpV).'); + return false; + } + + // Check if AGP version is too old to perform validation. + if (isWithinVersionRange(agpV, + min: '1.0', max: oldestDocumentedJavaAgpCompatibilityVersion, inclusiveMax: false)) { + logger.printTrace('AGP Version: $agpV is too old to determine Java compatibility.'); + return false; + } + + if (isWithinVersionRange(agpV, + min: maxKnownAndSupportedAgpVersion, max: '100.100', inclusiveMin: false)) { + logger.printTrace('AGP Version: $agpV is too new to determine Java compatibility.'); + return false; + } + + // Begin known Java <-> AGP evaluation. + for (final JavaAgpCompat data in _javaAgpCompatList) { + if (isWithinVersionRange(agpV, min: data.agpMin, max: data.agpMax)) { + return isWithinVersionRange(javaV, min: data.javaMin, max: '100.100'); + } + } + + logger.printTrace('Unknown Java-AGP compatibility $javaV, $agpV'); + return false; + } + + /// Returns compatibility information concerning the minimum AGP + /// version for the specified Java version. + JavaAgpCompat? getMinimumAgpVersionForJavaVersion(Logger logger, + {required String javaV}) { + for (final JavaAgpCompat data in _javaAgpCompatList) { + if (isWithinVersionRange(javaV, min: data.javaMin, max: '100.100')) { + return data; + } + } + + logger.printTrace('Unable to determine minimum AGP version for specified Java version.'); + return null; +} + +/// Returns valid Java range for specified Gradle and AGP verisons. +/// +/// Assumes that gradleV and agpV are compatible versions. +VersionRange getJavaVersionFor({required String gradleV, required String agpV}) { + // Find minimum Java version based on AGP compatibility. + String? minJavaVersion; + for (final JavaAgpCompat data in _javaAgpCompatList) { + if (isWithinVersionRange(agpV, min: data.agpMin, max: data.agpMax)) { + minJavaVersion = data.javaMin; + } + } + + // Find maximum Java version based on Gradle compatibility. + String? maxJavaVersion; + for (final JavaGradleCompat data in _javaGradleCompatList.reversed) { + if (isWithinVersionRange(gradleV, min: data.gradleMin, max: maxKnownAndSupportedGradleVersion)) { + maxJavaVersion = data.javaMax; + } + } + + return VersionRange(minJavaVersion, maxJavaVersion); +} + /// Returns the Gradle version that is required by the given Android Gradle plugin version /// by picking the largest compatible version from /// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle @@ -602,7 +645,7 @@ String getGradleVersionFor(String androidPluginVersion) { GradleForAgp(agpMin: '7.5.0', agpMax: '100.100', minRequiredGradle: '8.0'), // Assume if AGP is newer than this code know about return the highest gradle // version we know about. - GradleForAgp(agpMin: maxKnownAgpVersion, agpMax: maxKnownAgpVersion, minRequiredGradle: _maxKnownAndSupportedGradleVersion), + GradleForAgp(agpMin: maxKnownAgpVersion, agpMax: maxKnownAgpVersion, minRequiredGradle: maxKnownAndSupportedGradleVersion), ]; @@ -612,7 +655,7 @@ String getGradleVersionFor(String androidPluginVersion) { } } if (isWithinVersionRange(androidPluginVersion, min: maxKnownAgpVersion, max: '100.100')) { - return _maxKnownAndSupportedGradleVersion; + return maxKnownAndSupportedGradleVersion; } throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.'); } @@ -703,17 +746,63 @@ void exitWithNoSdkMessage() { } // Data class to hold normal/defined Java <-> Gradle compatability criteria. +// +// The [javaMax] is exclusive in terms of supporting the noted [gradleMin], +// whereas [javaMin] is inclusive. +@immutable class JavaGradleCompat { - JavaGradleCompat({ + const JavaGradleCompat({ required this.javaMin, required this.javaMax, required this.gradleMin, required this.gradleMax, }); + final String javaMin; final String javaMax; final String gradleMin; final String gradleMax; + + @override + bool operator ==(Object other) => + other is JavaGradleCompat && + other.javaMin == javaMin && + other.javaMax == javaMax && + other.gradleMin == gradleMin && + other.gradleMax == gradleMax; + + @override + int get hashCode => Object.hash(javaMin, javaMax, gradleMin, gradleMax); +} + +// Data class to hold defined Java <-> AGP compatibility criteria. +// +// The [agpMin] and [agpMax] are inclusive in terms of having the +// noted [javaMin] and [javaDefault] versions. +@immutable +class JavaAgpCompat { + const JavaAgpCompat({ + required this.javaMin, + required this.javaDefault, + required this.agpMin, + required this.agpMax, + }); + + final String javaMin; + final String javaDefault; + final String agpMin; + final String agpMax; + + @override + bool operator ==(Object other) => + other is JavaAgpCompat && + other.javaMin == javaMin && + other.javaDefault == javaDefault && + other.agpMin == agpMin && + other.agpMax == agpMax; + + @override + int get hashCode => Object.hash(javaMin, javaDefault, agpMin, agpMax); } class GradleForAgp { @@ -722,6 +811,7 @@ class GradleForAgp { required this.agpMax, required this.minRequiredGradle, }); + final String agpMin; final String agpMax; final String minRequiredGradle; @@ -735,3 +825,112 @@ String getGradlewFileName(Platform platform) { return 'gradlew'; } } + +/// List of compatible Java/Gradle versions. +/// +/// Should be updated when a new version of Java is supported by a new version +/// of Gradle, as https://docs.gradle.org/current/userguide/compatibility.html +/// details. +List<JavaGradleCompat> _javaGradleCompatList = const <JavaGradleCompat>[ + JavaGradleCompat( + javaMin: '19', + javaMax: '20', + gradleMin: '7.6', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '18', + javaMax: '19', + gradleMin: '7.5', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '17', + javaMax: '18', + gradleMin: '7.3', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '16', + javaMax: '17', + gradleMin: '7.0', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '15', + javaMax: '16', + gradleMin: '6.7', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '14', + javaMax: '15', + gradleMin: '6.3', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '13', + javaMax: '14', + gradleMin: '6.0', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '12', + javaMax: '13', + gradleMin: '5.4', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '11', + javaMax: '12', + gradleMin: '5.0', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + // 1.11 is a made up java version to cover everything in 1.10.* + JavaGradleCompat( + javaMin: '1.10', + javaMax: '1.11', + gradleMin: '4.7', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '1.9', + javaMax: '1.10', + gradleMin: '4.3', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + JavaGradleCompat( + javaMin: '1.8', + javaMax: '1.9', + gradleMin: '2.0', + gradleMax: maxKnownAndSupportedGradleVersion, + ), + ]; + + // List of compatible Java/AGP versions, where agpMax versions are inclusive. + // + // Should be updated whenever a new version of AGP is released as + // https://developer.android.com/build/releases/gradle-plugin details. + List<JavaAgpCompat> _javaAgpCompatList = const <JavaAgpCompat>[ + JavaAgpCompat( + javaMin: '17', + javaDefault: '17', + agpMin: '8.0', + agpMax: maxKnownAndSupportedAgpVersion, + ), + JavaAgpCompat( + javaMin: '11', + javaDefault: '11', + agpMin: '7.0', + agpMax: '7.4', + ), + JavaAgpCompat( + // You may use JDK 1.7 with AGP 4.2, but we treat 1.8 as the default since + // it is used by default for this AGP version and lower versions of Java + // are deprecated for executing Gradle. + javaMin: '1.8', + javaDefault: '1.8', + agpMin: '4.2', + agpMax: '4.2', + ), + ]; diff --git a/packages/flutter_tools/lib/src/android/migrations/android_studio_java_gradle_conflict_migration.dart b/packages/flutter_tools/lib/src/android/migrations/android_studio_java_gradle_conflict_migration.dart index 0a8307171336f..b8d4888370121 100644 --- a/packages/flutter_tools/lib/src/android/migrations/android_studio_java_gradle_conflict_migration.dart +++ b/packages/flutter_tools/lib/src/android/migrations/android_studio_java_gradle_conflict_migration.dart @@ -91,10 +91,10 @@ class AndroidStudioJavaGradleConflictMigration extends ProjectMigrator { return; } - if (_androidStudio == null || _androidStudio!.version == null) { + if (_androidStudio == null || _androidStudio.version == null) { logger.printTrace(androidStudioNotFound); return; - } else if (_androidStudio!.version!.major < androidStudioFlamingo.major) { + } else if (_androidStudio.version!.major < androidStudioFlamingo.major) { logger.printTrace(androidStudioVersionBelowFlamingo); return; } diff --git a/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart b/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart new file mode 100644 index 0000000000000..d7d4cd1fc029e --- /dev/null +++ b/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart @@ -0,0 +1,47 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import '../../base/file_system.dart'; +import '../../base/project_migrator.dart'; +import '../../project.dart'; +import '../gradle_utils.dart'; + +/// Replacement value for https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/BaseFlavor#minSdkVersion(kotlin.Int) +/// that instead of using a value defaults to the version defined by the +/// flutter sdk as the minimum supported by flutter. +@visibleForTesting +const String replacementMinSdkText = 'minSdkVersion flutter.minSdkVersion'; + +@visibleForTesting +const String appGradleNotFoundWarning = 'Module level build.gradle file not found, skipping minSdkVersion migration.'; + +class MinSdkVersionMigration extends ProjectMigrator { + MinSdkVersionMigration( + AndroidProject project, + super.logger, + ) : _project = project; + + final AndroidProject _project; + + @override + void migrate() { + // Skip applying migration in modules as the FlutterExtension is not applied. + if (_project.isModule) { + return; + } + try { + processFileLines(_project.appGradleFile); + } on FileSystemException { + // Skip if we cannot find the app level build.gradle file. + logger.printTrace(appGradleNotFoundWarning); + } + } + + @override + String migrateFileContents(String fileContents) { + return fileContents.replaceAll(jellyBeanMinSdkVersionMatch, replacementMinSdkText); + } +} diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index f226d139d2396..8775db6fec1f1 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/memory.dart'; +import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'base/common.dart'; @@ -283,15 +284,40 @@ class EngineBuildPaths { final String? webSdk; } -/// Information about a local engine build +/// Information about a local engine build (i.e. `--local-engine[-host]=...`). +/// +/// See https://github.com/flutter/flutter/wiki/The-flutter-tool#using-a-locally-built-engine-with-the-flutter-tool +/// for more information about local engine builds. class LocalEngineInfo { + /// Creates a reference to a local engine build. + /// + /// The [targetOutPath] and [hostOutPath] are assumed to be resolvable + /// paths to the built engine artifacts for the target (device) and host + /// (build) platforms, respectively. const LocalEngineInfo({ - required this.engineOutPath, - required this.localEngineName, + required this.targetOutPath, + required this.hostOutPath, }); - final String engineOutPath; - final String localEngineName; + /// The path to the engine artifacts for the target (device) platform. + /// + /// For example, if the target platform is Android debug, this would be a path + /// like `/path/to/engine/src/out/android_debug_unopt`. To retrieve just the + /// name (platform), see [localTargetName]. + final String targetOutPath; + + /// The path to the engine artifacts for the host (build) platform. + /// + /// For example, if the host platform is debug, this would be a path like + /// `/path/to/engine/src/out/host_debug_unopt`. To retrieve just the name + /// (platform), see [localHostName]. + final String hostOutPath; + + /// The name of the target (device) platform, i.e. `android_debug_unopt`. + String get localTargetName => globals.fs.path.basename(targetOutPath); + + /// The name of the host (build) platform, e.g. `host_debug_unopt`. + String get localHostName => globals.fs.path.basename(hostOutPath); } // Manages the engine artifacts of Flutter. @@ -302,12 +328,23 @@ abstract class Artifacts { /// If a [fileSystem] is not provided, creates a new [MemoryFileSystem] instance. /// /// Creates a [LocalEngineArtifacts] if `localEngine` is non-null - factory Artifacts.test({String? localEngine, FileSystem? fileSystem}) { - fileSystem ??= MemoryFileSystem.test(); - if (localEngine != null) { - return _TestLocalEngine(localEngine, fileSystem); - } - return _TestArtifacts(fileSystem); + @visibleForTesting + factory Artifacts.test({FileSystem? fileSystem}) { + return _TestArtifacts(fileSystem ?? MemoryFileSystem.test()); + } + + /// A test-specific implementation of artifacts that returns stable paths for + /// all artifacts, and uses a local engine. + /// + /// If a [fileSystem] is not provided, creates a new [MemoryFileSystem] instance. + @visibleForTesting + factory Artifacts.testLocalEngine({ + required String localEngine, + required String localEngineHost, + FileSystem? fileSystem, + }) { + return _TestLocalEngine( + localEngine, localEngineHost, fileSystem ?? MemoryFileSystem.test()); } static Artifacts getLocalEngine(EngineBuildPaths engineBuildPaths) { @@ -810,8 +847,8 @@ class CachedLocalEngineArtifacts implements Artifacts { }) : _fileSystem = fileSystem, localEngineInfo = LocalEngineInfo( - engineOutPath: engineOutPath, - localEngineName: fileSystem.path.basename(engineOutPath) + targetOutPath: engineOutPath, + hostOutPath: _hostEngineOutPath, ), _cache = cache, _processManager = processManager, @@ -922,28 +959,28 @@ class CachedLocalEngineArtifacts implements Artifacts { return _flutterTesterPath(platform!); case Artifact.isolateSnapshotData: case Artifact.vmSnapshotData: - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', artifactFileName); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'gen', 'flutter', 'lib', 'snapshot', artifactFileName); case Artifact.icuData: case Artifact.flutterXcframework: case Artifact.flutterMacOSFramework: - return _fileSystem.path.join(localEngineInfo.engineOutPath, artifactFileName); + return _fileSystem.path.join(localEngineInfo.targetOutPath, artifactFileName); case Artifact.platformKernelDill: if (platform == TargetPlatform.fuchsia_x64 || platform == TargetPlatform.fuchsia_arm64) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter_runner_patched_sdk', artifactFileName); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter_runner_patched_sdk', artifactFileName); } return _fileSystem.path.join(_getFlutterPatchedSdkPath(mode), artifactFileName); case Artifact.platformLibrariesJson: return _fileSystem.path.join(_getFlutterPatchedSdkPath(mode), 'lib', artifactFileName); case Artifact.flutterFramework: return _getIosEngineArtifactPath( - localEngineInfo.engineOutPath, environmentType, _fileSystem, _platform); + localEngineInfo.targetOutPath, environmentType, _fileSystem, _platform); case Artifact.flutterPatchedSdkPath: // When using local engine always use [BuildMode.debug] regardless of // what was specified in [mode] argument because local engine will // have only one flutter_patched_sdk in standard location, that // is happen to be what debug(non-release) mode is using. if (platform == TargetPlatform.fuchsia_x64 || platform == TargetPlatform.fuchsia_arm64) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter_runner_patched_sdk'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter_runner_patched_sdk'); } return _getFlutterPatchedSdkPath(BuildMode.debug); case Artifact.skyEnginePath: @@ -952,11 +989,11 @@ class CachedLocalEngineArtifacts implements Artifacts { final String hostPlatform = getNameForHostPlatform(getCurrentHostPlatform()); final String modeName = mode!.isRelease ? 'release' : mode.toString(); final String dartBinaries = 'dart_binaries-$modeName-$hostPlatform'; - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'host_bundle', dartBinaries, 'kernel_compiler.dart.snapshot'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'host_bundle', dartBinaries, 'kernel_compiler.dart.snapshot'); case Artifact.fuchsiaFlutterRunner: final String jitOrAot = mode!.isJit ? '_jit' : '_aot'; final String productOrNo = mode.isRelease ? '_product' : ''; - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter$jitOrAot${productOrNo}_runner-0.far'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter$jitOrAot${productOrNo}_runner-0.far'); case Artifact.fontSubset: return _fileSystem.path.join(_hostEngineOutPath, artifactFileName); case Artifact.constFinder: @@ -984,11 +1021,11 @@ class CachedLocalEngineArtifacts implements Artifacts { @override String getEngineType(TargetPlatform platform, [ BuildMode? mode ]) { - return _fileSystem.path.basename(localEngineInfo.engineOutPath); + return _fileSystem.path.basename(localEngineInfo.targetOutPath); } String _getFlutterPatchedSdkPath(BuildMode? buildMode) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, + return _fileSystem.path.join(localEngineInfo.targetOutPath, buildMode == BuildMode.release ? 'flutter_patched_sdk_product' : 'flutter_patched_sdk'); } @@ -1038,14 +1075,14 @@ class CachedLocalEngineArtifacts implements Artifacts { } String _getFlutterWebSdkPath() { - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter_web_sdk'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter_web_sdk'); } String _genSnapshotPath() { const List<String> clangDirs = <String>['.', 'clang_x64', 'clang_x86', 'clang_i386', 'clang_arm64']; final String genSnapshotName = _artifactToFileName(Artifact.genSnapshot, _platform)!; for (final String clangDir in clangDirs) { - final String genSnapshotPath = _fileSystem.path.join(localEngineInfo.engineOutPath, clangDir, genSnapshotName); + final String genSnapshotPath = _fileSystem.path.join(localEngineInfo.targetOutPath, clangDir, genSnapshotName); if (_processManager.canRun(genSnapshotPath)) { return genSnapshotPath; } @@ -1055,11 +1092,11 @@ class CachedLocalEngineArtifacts implements Artifacts { String _flutterTesterPath(TargetPlatform platform) { if (_platform.isLinux) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, _artifactToFileName(Artifact.flutterTester, _platform)); + return _fileSystem.path.join(localEngineInfo.targetOutPath, _artifactToFileName(Artifact.flutterTester, _platform)); } else if (_platform.isMacOS) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter_tester'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter_tester'); } else if (_platform.isWindows) { - return _fileSystem.path.join(localEngineInfo.engineOutPath, 'flutter_tester.exe'); + return _fileSystem.path.join(localEngineInfo.targetOutPath, 'flutter_tester.exe'); } throw Exception('Unsupported platform $platform.'); } @@ -1365,11 +1402,13 @@ class _TestArtifacts implements Artifacts { } class _TestLocalEngine extends _TestArtifacts { - _TestLocalEngine(String engineOutPath, super.fileSystem) : - localEngineInfo = - LocalEngineInfo( - engineOutPath: engineOutPath, - localEngineName: fileSystem.path.basename(engineOutPath) + _TestLocalEngine( + String engineOutPath, + String engineHostOutPath, + super.fileSystem, + ) : localEngineInfo = LocalEngineInfo( + targetOutPath: engineOutPath, + hostOutPath: engineHostOutPath, ); @override diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 7c8f054517cdb..7eda50bc1f7de 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -168,6 +168,7 @@ class ManifestAssetBundle implements AssetBundle { // We assume the main asset is designed for a device pixel ratio of 1.0. static const String _kAssetManifestJsonFilename = 'AssetManifest.json'; static const String _kAssetManifestBinFilename = 'AssetManifest.bin'; + static const String _kAssetManifestBinJsonFilename = 'AssetManifest.bin.json'; static const String _kNoticeFile = 'NOTICES'; // Comically, this can't be name with the more common .gz file extension @@ -233,8 +234,6 @@ class ManifestAssetBundle implements AssetBundle { // device. _lastBuildTimestamp = DateTime.now(); if (flutterManifest.isEmpty) { - entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}'); - entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular; entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}'); entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular; final ByteData emptyAssetManifest = @@ -242,6 +241,11 @@ class ManifestAssetBundle implements AssetBundle { entries[_kAssetManifestBinFilename] = DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes)); entryKinds[_kAssetManifestBinFilename] = AssetKind.regular; + // Create .bin.json on web builds. + if (targetPlatform == TargetPlatform.web_javascript) { + entries[_kAssetManifestBinJsonFilename] = DevFSStringContent('""'); + entryKinds[_kAssetManifestBinJsonFilename] = AssetKind.regular; + } return 0; } @@ -437,8 +441,8 @@ class ManifestAssetBundle implements AssetBundle { final Map<String, List<String>> assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants); - final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest)); final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest); + final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest)); final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts)); final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles); if (licenseResult.errorMessages.isNotEmpty) { @@ -464,6 +468,13 @@ class ManifestAssetBundle implements AssetBundle { _setIfChanged(_kAssetManifestJsonFilename, assetManifestJson, AssetKind.regular); _setIfChanged(_kAssetManifestBinFilename, assetManifestBinary, AssetKind.regular); + // Create .bin.json on web builds. + if (targetPlatform == TargetPlatform.web_javascript) { + final DevFSStringContent assetManifestBinaryJson = DevFSStringContent(json.encode( + base64.encode(assetManifestBinary.bytes) + )); + _setIfChanged(_kAssetManifestBinJsonFilename, assetManifestBinaryJson, AssetKind.regular); + } _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular); _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform); return 0; diff --git a/packages/flutter_tools/lib/src/base/command_help.dart b/packages/flutter_tools/lib/src/base/command_help.dart index f9922dbc21679..982f72787e9dc 100644 --- a/packages/flutter_tools/lib/src/base/command_help.dart +++ b/packages/flutter_tools/lib/src/base/command_help.dart @@ -259,8 +259,12 @@ class CommandHelpOption { message.write(''.padLeft(width - parentheticalText.length)); message.write(_terminal.color(parentheticalText, TerminalColor.grey)); - // Terminals seem to require this because we have both bolded and colored - // a line. Otherwise the next line comes out bold until a reset bold. + // Some terminals seem to have a buggy implementation of the SGR ANSI escape + // codes and seem to require that we explicitly request "normal intensity" + // at the end of the line to prevent the next line comes out bold, despite + // the fact that the line already contains a "normal intensity" code. + // This doesn't make much sense but has been reproduced by multiple users. + // See: https://github.com/flutter/flutter/issues/52204 if (_terminal.supportsColor) { message.write(AnsiTerminal.resetBold); } diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart index ebf2333dfdc8f..bcd1ed2545361 100644 --- a/packages/flutter_tools/lib/src/base/context.dart +++ b/packages/flutter_tools/lib/src/base/context.dart @@ -116,7 +116,7 @@ class AppContext { T? get<T>() { dynamic value = _generateIfNecessary(T, _overrides); if (value == null && _parent != null) { - value = _parent!.get<T>(); + value = _parent.get<T>(); } return _unboxNull(value ?? _generateIfNecessary(T, _fallbacks)) as T?; } diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart index 2a6352737b1fa..83473499a9125 100644 --- a/packages/flutter_tools/lib/src/base/io.dart +++ b/packages/flutter_tools/lib/src/base/io.dart @@ -191,15 +191,27 @@ class ProcessSignal { /// /// Returns true if the signal was delivered, false otherwise. /// - /// On Windows, this can only be used with [ProcessSignal.sigterm], which - /// terminates the process. + /// On Windows, this can only be used with [sigterm], which terminates the + /// process. /// - /// This is implemented by sending the signal using [Process.killPid]. + /// This is implemented by sending the signal using [io.Process.killPid] and + /// therefore cannot be faked in tests. To fake sending signals in tests, use + /// [kill] instead. bool send(int pid) { assert(!_platform.isWindows || this == ProcessSignal.sigterm); return io.Process.killPid(pid, _delegate); } + /// A more testable variant of [send]. + /// + /// Sends this signal to the given `process` by invoking [io.Process.kill]. + /// + /// In tests this method can be faked by passing a fake implementation of the + /// [io.Process] interface. + bool kill(io.Process process) { + return process.kill(_delegate); + } + @override String toString() => _delegate.toString(); } diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index 2c418a3b9dc0e..b6d185d067f80 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -36,7 +36,7 @@ abstract class Logger { /// If true, silences the logger output. bool quiet = false; - /// If true, this logger supports color output. + /// If true, this logger supports ANSI sequences and animations are enabled. bool get supportsColor; /// If true, this logger is connected to a terminal. @@ -443,7 +443,7 @@ class StdoutLogger extends Logger { bool get isVerbose => false; @override - bool get supportsColor => terminal.supportsColor; + bool get supportsColor => terminal.supportsColor && terminal.isCliAnimationEnabled; @override bool get hasTerminal => _stdio.stdinHasTerminal; @@ -772,7 +772,7 @@ class BufferLogger extends Logger { bool get isVerbose => _verbose; @override - bool get supportsColor => terminal.supportsColor; + bool get supportsColor => terminal.supportsColor && terminal.isCliAnimationEnabled; final StringBuffer _error = StringBuffer(); final StringBuffer _warning = StringBuffer(); diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart index 82a3c89b1d919..6fb36b0bc046a 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -8,6 +8,8 @@ import 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../convert.dart'; +import '../globals.dart' as globals; +import '../reporting/first_run.dart'; import 'io.dart'; import 'logger.dart'; @@ -564,3 +566,76 @@ class _DefaultProcessUtils implements ProcessUtils { } } } + +Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) async { + // Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()` + // before invoking the print welcome method because the print welcome method + // will set `messenger.shouldDisplayLicenseTerms()` to false + final FirstRunMessenger messenger = + FirstRunMessenger(persistentToolState: globals.persistentToolState!); + final bool legacyAnalyticsMessageShown = + messenger.shouldDisplayLicenseTerms(); + + // Prints the welcome message if needed for legacy analytics. + globals.flutterUsage.printWelcome(); + + // Ensure that the consent message has been displayed for unified analytics + if (globals.analytics.shouldShowMessage) { + globals.logger.printStatus(globals.analytics.getConsentMessage); + if (!globals.flutterUsage.enabled) { + globals.printStatus( + 'Please note that analytics reporting was already disabled, ' + 'and will continue to be disabled.\n'); + } + + // Because the legacy analytics may have also sent a message, + // the conditional below will print additional messaging informing + // users that the two consent messages they are receiving is not a + // bug + if (legacyAnalyticsMessageShown) { + globals.logger + .printStatus('You have received two consent messages because ' + 'the flutter tool is migrating to a new analytics system. ' + 'Disabling analytics collection will disable both the legacy ' + 'and new analytics collection systems. ' + 'You can disable analytics reporting by running `flutter --disable-analytics`\n'); + } + + // Invoking this will onboard the flutter tool onto + // the package on the developer's machine and will + // allow for events to be sent to Google Analytics + // on subsequent runs of the flutter tool (ie. no events + // will be sent on the first run to allow developers to + // opt out of collection) + globals.analytics.clientShowedMessage(); + } + + // Send any last analytics calls that are in progress without overly delaying + // the tool's exit (we wait a maximum of 250ms). + if (globals.flutterUsage.enabled) { + final Stopwatch stopwatch = Stopwatch()..start(); + await globals.flutterUsage.ensureAnalyticsSent(); + globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms'); + } + + // Run shutdown hooks before flushing logs + await shutdownHooks.runShutdownHooks(globals.logger); + + final Completer<void> completer = Completer<void>(); + + // Give the task / timer queue one cycle through before we hard exit. + Timer.run(() { + try { + globals.printTrace('exiting with code $code'); + exit(code); + completer.complete(); + // This catches all exceptions because the error is propagated on the + // completer. + } catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses + completer.completeError(error, stackTrace); + } + }); + + await completer.future; + return code; +} diff --git a/packages/flutter_tools/lib/src/base/signals.dart b/packages/flutter_tools/lib/src/base/signals.dart index 9185762cdfe54..a83d85b622e86 100644 --- a/packages/flutter_tools/lib/src/base/signals.dart +++ b/packages/flutter_tools/lib/src/base/signals.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import '../base/process.dart'; +import '../globals.dart' as globals; import 'async_guard.dart'; import 'io.dart'; @@ -18,7 +20,8 @@ abstract class Signals { @visibleForTesting factory Signals.test({ List<ProcessSignal> exitSignals = defaultExitSignals, - }) => LocalSignals._(exitSignals); + ShutdownHooks? shutdownHooks, + }) => LocalSignals._(exitSignals, shutdownHooks: shutdownHooks); // The default list of signals that should cause the process to exit. static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[ @@ -50,13 +53,17 @@ abstract class Signals { /// We use a singleton instance of this class to ensure that all handlers for /// fatal signals run before this class calls exit(). class LocalSignals implements Signals { - LocalSignals._(this.exitSignals); + LocalSignals._( + this.exitSignals, { + ShutdownHooks? shutdownHooks, + }) : _shutdownHooks = shutdownHooks ?? globals.shutdownHooks; static LocalSignals instance = LocalSignals._( Signals.defaultExitSignals, ); final List<ProcessSignal> exitSignals; + final ShutdownHooks _shutdownHooks; // A table mapping (signal, token) -> signal handler. final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable = @@ -144,7 +151,7 @@ class LocalSignals implements Signals { // If this was a signal that should cause the process to go down, then // call exit(); if (_shouldExitFor(s)) { - exit(0); + await exitWithHooks(0, shutdownHooks: _shutdownHooks); } } diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart index 720b92595fcf3..73c20f9fe6774 100644 --- a/packages/flutter_tools/lib/src/base/terminal.dart +++ b/packages/flutter_tools/lib/src/base/terminal.dart @@ -76,8 +76,14 @@ abstract class Terminal { factory Terminal.test({bool supportsColor, bool supportsEmoji}) = _TestTerminal; /// Whether the current terminal supports color escape codes. + /// + /// Check [isCliAnimationEnabled] as well before using `\r` or ANSI sequences + /// to perform animations. bool get supportsColor; + /// Whether to show animations on this terminal. + bool get isCliAnimationEnabled; + /// Whether the current terminal can display emoji. bool get supportsEmoji; @@ -152,6 +158,7 @@ class AnsiTerminal implements Terminal { required io.Stdio stdio, required Platform platform, DateTime? now, // Time used to determine preferredStyle. Defaults to 0001-01-01 00:00. + this.isCliAnimationEnabled = true, }) : _stdio = stdio, _platform = platform, @@ -199,6 +206,9 @@ class AnsiTerminal implements Terminal { @override bool get supportsColor => _platform.stdoutSupportsAnsi; + @override + final bool isCliAnimationEnabled; + // Assume unicode emojis are supported when not on Windows. // If we are on Windows, unicode emojis are supported in Windows Terminal, // which sets the WT_SESSION environment variable. See: @@ -275,14 +285,14 @@ class AnsiTerminal implements Terminal { } @override - String clearScreen() => supportsColor ? clear : '\n\n'; + String clearScreen() => supportsColor && isCliAnimationEnabled ? clear : '\n\n'; /// Returns ANSI codes to clear [numberOfLines] lines starting with the line /// the cursor is on. /// /// If the terminal does not support ANSI codes, returns an empty string. String clearLines(int numberOfLines) { - if (!supportsColor) { + if (!supportsColor || !isCliAnimationEnabled) { return ''; } return cursorBeginningOfLineCode + @@ -304,13 +314,19 @@ class AnsiTerminal implements Terminal { return; } final io.Stdin stdin = _stdio.stdin as io.Stdin; - // The order of setting lineMode and echoMode is important on Windows. - if (value) { - stdin.echoMode = false; - stdin.lineMode = false; - } else { - stdin.lineMode = true; - stdin.echoMode = true; + + try { + // The order of setting lineMode and echoMode is important on Windows. + if (value) { + stdin.echoMode = false; + stdin.lineMode = false; + } else { + stdin.lineMode = true; + stdin.echoMode = true; + } + } on io.StdinException { + // If the pipe to STDIN has been closed it's probably because the + // terminal has been closed, and there is nothing actionable to do here. } } @@ -402,6 +418,9 @@ class _TestTerminal implements Terminal { @override final bool supportsColor; + @override + bool get isCliAnimationEnabled => supportsColor; + @override final bool supportsEmoji; diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 8441db2a7df04..92e6feb285a61 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -183,10 +183,10 @@ class UserMessages { ' sudo xcodebuild -runFirstLaunch'; String get xcodeMissing => 'Xcode not installed; this is necessary for iOS and macOS development.\n' - 'Download at https://developer.apple.com/xcode/download/.'; + 'Download at https://developer.apple.com/xcode/.'; String get xcodeIncomplete => 'Xcode installation is incomplete; a full installation is necessary for iOS and macOS development.\n' - 'Download at: https://developer.apple.com/xcode/download/\n' + 'Download at: https://developer.apple.com/xcode/\n' 'Or install Xcode via the App Store.\n' 'Once installed, run:\n' ' sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer\n' @@ -239,7 +239,8 @@ class UserMessages { 'supported by Flutter yet.'; String get visualStudioNotLaunchable => 'The current Visual Studio installation is not launchable. Please reinstall Visual Studio.'; - String get visualStudioIsIncomplete => 'The current Visual Studio installation is incomplete. Please reinstall Visual Studio.'; + String get visualStudioIsIncomplete => 'The current Visual Studio installation is incomplete.\n' + 'Please use Visual Studio Installer to complete the installation or reinstall Visual Studio.'; String get visualStudioRebootRequired => 'Visual Studio requires a reboot of your system to complete installation.'; // Messages used in LinuxDoctorValidator @@ -308,6 +309,10 @@ class UserMessages { "you have compiled the engine in that directory, which should produce an 'out' directory"; String get runnerLocalEngineOrWebSdkRequired => 'You must specify --local-engine or --local-web-sdk if you are using a locally built engine or web sdk.'; + String get runnerLocalEngineRequiresHostEngine => + 'You are using a locally built engine (--local-engine) but have not specified --local-engine-host.\n' + 'You may be building with a different engine than the one you are running with. ' + 'See https://github.com/flutter/flutter/issues/132245 for details.'; String runnerNoEngineBuild(String engineBuildPath) => 'No Flutter engine build found at $engineBuildPath.'; String runnerNoWebSdk(String webSdkPath) => diff --git a/packages/flutter_tools/lib/src/base/version_range.dart b/packages/flutter_tools/lib/src/base/version_range.dart new file mode 100644 index 0000000000000..f5bc6788e24dd --- /dev/null +++ b/packages/flutter_tools/lib/src/base/version_range.dart @@ -0,0 +1,30 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart' show immutable; + +/// Data class that represents a range of versions in their String +/// representation. +/// +/// Both the [versionMin] and [versionMax] are inclusive versions, and undefined +/// values represent an unknown minimum/maximum version. +@immutable +class VersionRange{ + const VersionRange( + this.versionMin, + this.versionMax, + ); + + final String? versionMin; + final String? versionMax; + + @override + bool operator ==(Object other) => + other is VersionRange && + other.versionMin == versionMin && + other.versionMax == versionMax; + + @override + int get hashCode => Object.hash(versionMin, versionMax); +} diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 053197c8f1a17..b99d5f6effa45 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -23,6 +23,7 @@ class BuildInfo { this.mode, this.flavor, { this.trackWidgetCreation = false, + this.frontendServerStarterPath, List<String>? extraFrontEndOptions, List<String>? extraGenSnapshotOptions, List<String>? fileSystemRoots, @@ -46,6 +47,7 @@ class BuildInfo { this.packageConfig = PackageConfig.empty, this.initializeFromDill, this.assumeInitializeFromDillUpToDate = false, + this.buildNativeAssets = true, }) : extraFrontEndOptions = extraFrontEndOptions ?? const <String>[], extraGenSnapshotOptions = extraGenSnapshotOptions ?? const <String>[], fileSystemRoots = fileSystemRoots ?? const <String>[], @@ -82,6 +84,10 @@ class BuildInfo { /// Whether the build should track widget creation locations. final bool trackWidgetCreation; + /// If provided, the frontend server will be started in JIT mode from this + /// file. + final String? frontendServerStarterPath; + /// Extra command-line options for front-end. final List<String> extraFrontEndOptions; @@ -183,6 +189,9 @@ class BuildInfo { /// and skips the check and potential invalidation of files. final bool assumeInitializeFromDillUpToDate; + /// If set, builds native assets with `build.dart` from all packages. + final bool buildNativeAssets; + static const BuildInfo debug = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false); static const BuildInfo profile = BuildInfo(BuildMode.profile, null, treeShakeIcons: kIconTreeShakerEnabledDefault); static const BuildInfo jitRelease = BuildInfo(BuildMode.jitRelease, null, treeShakeIcons: kIconTreeShakerEnabledDefault); @@ -237,6 +246,8 @@ class BuildInfo { if (dartDefines.isNotEmpty) kDartDefines: encodeDartDefines(dartDefines), kDartObfuscation: dartObfuscation.toString(), + if (frontendServerStarterPath != null) + kFrontendServerStarterPath: frontendServerStarterPath!, if (extraFrontEndOptions.isNotEmpty) kExtraFrontEndOptions: extraFrontEndOptions.join(','), if (extraGenSnapshotOptions.isNotEmpty) @@ -274,6 +285,8 @@ class BuildInfo { if (dartDefines.isNotEmpty) 'DART_DEFINES': encodeDartDefines(dartDefines), 'DART_OBFUSCATION': dartObfuscation.toString(), + if (frontendServerStarterPath != null) + 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath!, if (extraFrontEndOptions.isNotEmpty) 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions.join(','), if (extraGenSnapshotOptions.isNotEmpty) @@ -310,6 +323,8 @@ class BuildInfo { if (dartDefines.isNotEmpty) '-Pdart-defines=${encodeDartDefines(dartDefines)}', '-Pdart-obfuscation=$dartObfuscation', + if (frontendServerStarterPath != null) + '-Pfrontend-server-starter-path=$frontendServerStarterPath', if (extraFrontEndOptions.isNotEmpty) '-Pextra-front-end-options=${extraFrontEndOptions.join(',')}', if (extraGenSnapshotOptions.isNotEmpty) @@ -647,7 +662,7 @@ List<DarwinArch> defaultIOSArchsForEnvironment( // Handle single-arch local engines. final LocalEngineInfo? localEngineInfo = artifacts.localEngineInfo; if (localEngineInfo != null) { - final String localEngineName = localEngineInfo.localEngineName; + final String localEngineName = localEngineInfo.localTargetName; if (localEngineName.contains('_arm64')) { return <DarwinArch>[ DarwinArch.arm64 ]; } @@ -670,7 +685,7 @@ List<DarwinArch> defaultMacOSArchsForEnvironment(Artifacts artifacts) { // Handle single-arch local engines. final LocalEngineInfo? localEngineInfo = artifacts.localEngineInfo; if (localEngineInfo != null) { - if (localEngineInfo.localEngineName.contains('_arm64')) { + if (localEngineInfo.localTargetName.contains('_arm64')) { return <DarwinArch>[ DarwinArch.arm64 ]; } return <DarwinArch>[ DarwinArch.x86_64 ]; @@ -777,6 +792,8 @@ TargetPlatform getTargetPlatformForName(String platform) { return TargetPlatform.windows_x64; case 'web-javascript': return TargetPlatform.web_javascript; + case 'flutter-tester': + return TargetPlatform.tester; } throw Exception('Unsupported platform name "$platform"'); } @@ -872,8 +889,9 @@ String getLinuxBuildDirectory([TargetPlatform? targetPlatform]) { } /// Returns the Windows build output directory. -String getWindowsBuildDirectory() { - return globals.fs.path.join(getBuildDirectory(), 'windows'); +String getWindowsBuildDirectory(TargetPlatform targetPlatform) { + final String arch = targetPlatform.simpleName; + return globals.fs.path.join(getBuildDirectory(), 'windows', arch); } /// Returns the Fuchsia build output directory. @@ -898,6 +916,9 @@ const String kTargetFile = 'TargetFile'; /// Whether to enable or disable track widget creation. const String kTrackWidgetCreation = 'TrackWidgetCreation'; +/// If provided, the frontend server will be started in JIT mode from this file. +const String kFrontendServerStarterPath = 'FrontendServerStarterPath'; + /// Additional configuration passed to the dart front end. /// /// This is expected to be a comma separated list of strings. diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 039bb5a7fdd6f..d5febecf4251a 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -6,6 +6,7 @@ import 'package:package_config/package_config.dart'; import '../../artifacts.dart'; import '../../base/build.dart'; +import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../build_info.dart'; @@ -19,6 +20,7 @@ import 'assets.dart'; import 'dart_plugin_registrant.dart'; import 'icon_tree_shaker.dart'; import 'localizations.dart'; +import 'native_assets.dart'; import 'shader_compiler.dart'; /// Copies the pre-built flutter bundle. @@ -125,6 +127,7 @@ class KernelSnapshot extends Target { @override List<Source> get inputs => const <Source>[ + Source.pattern('{BUILD_DIR}/native_assets.yaml'), Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/common.dart'), Source.artifact(Artifact.platformKernelDill), @@ -142,6 +145,7 @@ class KernelSnapshot extends Target { @override List<Target> get dependencies => const <Target>[ + NativeAssets(), GenerateLocalizationsTarget(), DartPluginRegistrantTarget(), ]; @@ -174,10 +178,18 @@ class KernelSnapshot extends Target { final TargetPlatform targetPlatform = getTargetPlatformForName(targetPlatformEnvironment); // This configuration is all optional. + final String? frontendServerStarterPath = environment.defines[kFrontendServerStarterPath]; final List<String> extraFrontEndOptions = decodeCommaSeparated(environment.defines, kExtraFrontEndOptions); final List<String>? fileSystemRoots = environment.defines[kFileSystemRoots]?.split(','); final String? fileSystemScheme = environment.defines[kFileSystemScheme]; + final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml'); + final String nativeAssets = nativeAssetsFile.path; + if (!await nativeAssetsFile.exists()) { + throwToolExit("$nativeAssets doesn't exist."); + } + environment.logger.printTrace('Embedding native assets mapping $nativeAssets in kernel.'); + TargetModel targetModel = TargetModel.flutter; if (targetPlatform == TargetPlatform.fuchsia_x64 || targetPlatform == TargetPlatform.fuchsia_arm64) { @@ -243,6 +255,7 @@ class KernelSnapshot extends Target { linkPlatformKernelIn: forceLinkPlatform || buildMode.isPrecompiled, mainPath: targetFileAbsolute, depFilePath: environment.buildDir.childFile('kernel_snapshot.d').path, + frontendServerStarterPath: frontendServerStarterPath, extraFrontEndOptions: extraFrontEndOptions, fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme, @@ -251,6 +264,7 @@ class KernelSnapshot extends Target { buildDir: environment.buildDir, targetOS: targetOS, checkDartPluginRegistry: environment.generateDartPluginRegistry, + nativeAssets: nativeAssets, ); if (output == null || output.errorCount != 0) { throw Exception(); diff --git a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart index 7f3b0dee8bd56..916d1b3f59299 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart @@ -28,8 +28,6 @@ List<Map<String, Object?>> _getList(Object? object, String errorMessage) { class IconTreeShaker { /// Creates a wrapper for icon font subsetting. /// - /// The environment parameter must not be null. - /// /// If the `fontManifest` parameter is null, [enabled] will return false since /// there are no fonts to shake. /// diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 2ced8b0164798..9f43d01fafb4d 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -287,21 +287,21 @@ abstract class UnpackIOS extends Target { if (archs == null) { throw MissingDefineException(kIosArchs, name); } - _copyFramework(environment, sdkRoot); + await _copyFramework(environment, sdkRoot); final File frameworkBinary = environment.outputDir.childDirectory('Flutter.framework').childFile('Flutter'); final String frameworkBinaryPath = frameworkBinary.path; - if (!frameworkBinary.existsSync()) { + if (!await frameworkBinary.exists()) { throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin'); } - _thinFramework(environment, frameworkBinaryPath, archs); + await _thinFramework(environment, frameworkBinaryPath, archs); if (buildMode == BuildMode.release) { - _bitcodeStripFramework(environment, frameworkBinaryPath); + await _bitcodeStripFramework(environment, frameworkBinaryPath); } await _signFramework(environment, frameworkBinary, buildMode); } - void _copyFramework(Environment environment, String sdkRoot) { + Future<void> _copyFramework(Environment environment, String sdkRoot) async { final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem); final String basePath = environment.artifacts.getArtifactPath( Artifact.flutterFramework, @@ -310,7 +310,7 @@ abstract class UnpackIOS extends Target { environmentType: environmentType, ); - final ProcessResult result = environment.processManager.runSync(<String>[ + final ProcessResult result = await environment.processManager.run(<String>[ 'rsync', '-av', '--delete', @@ -328,16 +328,20 @@ abstract class UnpackIOS extends Target { } /// Destructively thin Flutter.framework to include only the specified architectures. - void _thinFramework(Environment environment, String frameworkBinaryPath, String archs) { + Future<void> _thinFramework( + Environment environment, + String frameworkBinaryPath, + String archs, + ) async { final List<String> archList = archs.split(' ').toList(); - final ProcessResult infoResult = environment.processManager.runSync(<String>[ + final ProcessResult infoResult = await environment.processManager.run(<String>[ 'lipo', '-info', frameworkBinaryPath, ]); final String lipoInfo = infoResult.stdout as String; - final ProcessResult verifyResult = environment.processManager.runSync(<String>[ + final ProcessResult verifyResult = await environment.processManager.run(<String>[ 'lipo', frameworkBinaryPath, '-verify_arch', @@ -355,7 +359,7 @@ abstract class UnpackIOS extends Target { } // Thin in-place. - final ProcessResult extractResult = environment.processManager.runSync(<String>[ + final ProcessResult extractResult = await environment.processManager.run(<String>[ 'lipo', '-output', frameworkBinaryPath, @@ -374,8 +378,11 @@ abstract class UnpackIOS extends Target { /// Destructively strip bitcode from the framework. This can be removed /// when the framework is no longer built with bitcode. - void _bitcodeStripFramework(Environment environment, String frameworkBinaryPath) { - final ProcessResult stripResult = environment.processManager.runSync(<String>[ + Future<void> _bitcodeStripFramework( + Environment environment, + String frameworkBinaryPath, + ) async { + final ProcessResult stripResult = await environment.processManager.run(<String>[ 'xcrun', 'bitcode_strip', frameworkBinaryPath, diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 366268d59f0e4..dadf976f473ac 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -80,7 +80,7 @@ abstract class UnpackMacOS extends Target { if (!frameworkBinary.existsSync()) { throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin'); } - _thinFramework(environment, frameworkBinaryPath); + await _thinFramework(environment, frameworkBinaryPath); } static const List<String> _copyDenylist = <String>['entitlements.txt', 'without_entitlements.txt']; @@ -96,17 +96,21 @@ abstract class UnpackMacOS extends Target { } } - void _thinFramework(Environment environment, String frameworkBinaryPath) { + Future<void> _thinFramework( + Environment environment, + String frameworkBinaryPath, + ) async { final String archs = environment.defines[kDarwinArchs] ?? 'x86_64 arm64'; final List<String> archList = archs.split(' ').toList(); - final ProcessResult infoResult = environment.processManager.runSync(<String>[ + final ProcessResult infoResult = + await environment.processManager.run(<String>[ 'lipo', '-info', frameworkBinaryPath, ]); final String lipoInfo = infoResult.stdout as String; - final ProcessResult verifyResult = environment.processManager.runSync(<String>[ + final ProcessResult verifyResult = await environment.processManager.run(<String>[ 'lipo', frameworkBinaryPath, '-verify_arch', diff --git a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart new file mode 100644 index 0000000000000..7a5ee21788003 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart @@ -0,0 +1,245 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' show Asset; +import 'package:package_config/package_config_types.dart'; + +import '../../base/common.dart'; +import '../../base/file_system.dart'; +import '../../base/platform.dart'; +import '../../build_info.dart'; +import '../../dart/package_map.dart'; +import '../../ios/native_assets.dart'; +import '../../linux/native_assets.dart'; +import '../../macos/native_assets.dart'; +import '../../macos/xcode.dart'; +import '../../native_assets.dart'; +import '../../windows/native_assets.dart'; +import '../build_system.dart'; +import '../depfile.dart'; +import '../exceptions.dart'; +import 'common.dart'; + +/// Builds the right native assets for a Flutter app. +/// +/// The build mode and target architecture can be changed from the +/// native build project (Xcode etc.), so only `flutter assemble` has the +/// information about build-mode and target architecture. +/// Invocations of flutter_tools other than `flutter assemble` are dry runs. +/// +/// This step needs to be consistent with the dry run invocations in `flutter +/// run`s so that the kernel mapping of asset id to dylib lines up after hot +/// restart. +/// +/// [KernelSnapshot] depends on this target. We produce a native_assets.yaml +/// here, and embed that mapping inside the kernel snapshot. +/// +/// The build always produces a valid native_assets.yaml and a native_assets.d +/// even if there are no native assets. This way the caching logic won't try to +/// rebuild. +class NativeAssets extends Target { + const NativeAssets({ + @visibleForTesting NativeAssetsBuildRunner? buildRunner, + }) : _buildRunner = buildRunner; + + final NativeAssetsBuildRunner? _buildRunner; + + @override + Future<void> build(Environment environment) async { + final String? targetPlatformEnvironment = environment.defines[kTargetPlatform]; + if (targetPlatformEnvironment == null) { + throw MissingDefineException(kTargetPlatform, name); + } + final TargetPlatform targetPlatform = getTargetPlatformForName(targetPlatformEnvironment); + + final Uri projectUri = environment.projectDir.uri; + final FileSystem fileSystem = environment.fileSystem; + final File packagesFile = fileSystem + .directory(projectUri) + .childDirectory('.dart_tool') + .childFile('package_config.json'); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + packagesFile, + logger: environment.logger, + ); + final NativeAssetsBuildRunner buildRunner = _buildRunner ?? + NativeAssetsBuildRunnerImpl( + projectUri, + packageConfig, + fileSystem, + environment.logger, + ); + + final List<Uri> dependencies; + switch (targetPlatform) { + case TargetPlatform.ios: + final String? iosArchsEnvironment = environment.defines[kIosArchs]; + if (iosArchsEnvironment == null) { + throw MissingDefineException(kIosArchs, name); + } + final List<DarwinArch> iosArchs = iosArchsEnvironment.split(' ').map(getDarwinArchForName).toList(); + final String? environmentBuildMode = environment.defines[kBuildMode]; + if (environmentBuildMode == null) { + throw MissingDefineException(kBuildMode, name); + } + final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode); + final String? sdkRoot = environment.defines[kSdkRoot]; + if (sdkRoot == null) { + throw MissingDefineException(kSdkRoot, name); + } + final EnvironmentType environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem)!; + dependencies = await buildNativeAssetsIOS( + environmentType: environmentType, + darwinArchs: iosArchs, + buildMode: buildMode, + projectUri: projectUri, + codesignIdentity: environment.defines[kCodesignIdentity], + fileSystem: fileSystem, + buildRunner: buildRunner, + yamlParentDirectory: environment.buildDir.uri, + ); + case TargetPlatform.darwin: + final String? darwinArchsEnvironment = environment.defines[kDarwinArchs]; + if (darwinArchsEnvironment == null) { + throw MissingDefineException(kDarwinArchs, name); + } + final List<DarwinArch> darwinArchs = darwinArchsEnvironment.split(' ').map(getDarwinArchForName).toList(); + final String? environmentBuildMode = environment.defines[kBuildMode]; + if (environmentBuildMode == null) { + throw MissingDefineException(kBuildMode, name); + } + final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode); + (_, dependencies) = await buildNativeAssetsMacOS( + darwinArchs: darwinArchs, + buildMode: buildMode, + projectUri: projectUri, + codesignIdentity: environment.defines[kCodesignIdentity], + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case TargetPlatform.linux_arm64: + case TargetPlatform.linux_x64: + final String? environmentBuildMode = environment.defines[kBuildMode]; + if (environmentBuildMode == null) { + throw MissingDefineException(kBuildMode, name); + } + final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode); + (_, dependencies) = await buildNativeAssetsLinux( + targetPlatform: targetPlatform, + buildMode: buildMode, + projectUri: projectUri, + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case TargetPlatform.windows_x64: + final String? environmentBuildMode = environment.defines[kBuildMode]; + if (environmentBuildMode == null) { + throw MissingDefineException(kBuildMode, name); + } + final BuildMode buildMode = BuildMode.fromCliName(environmentBuildMode); + (_, dependencies) = await buildNativeAssetsWindows( + targetPlatform: targetPlatform, + buildMode: buildMode, + projectUri: projectUri, + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case TargetPlatform.tester: + if (const LocalPlatform().isMacOS) { + (_, dependencies) = await buildNativeAssetsMacOS( + buildMode: BuildMode.debug, + projectUri: projectUri, + codesignIdentity: environment.defines[kCodesignIdentity], + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + flutterTester: true, + ); + } else if (const LocalPlatform().isLinux) { + (_, dependencies) = await buildNativeAssetsLinux( + buildMode: BuildMode.debug, + projectUri: projectUri, + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + flutterTester: true, + ); + } else if (const LocalPlatform().isWindows) { + (_, dependencies) = await buildNativeAssetsWindows( + buildMode: BuildMode.debug, + projectUri: projectUri, + yamlParentDirectory: environment.buildDir.uri, + fileSystem: fileSystem, + buildRunner: buildRunner, + flutterTester: true, + ); + } else { + // TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757 + // Write the file we claim to have in the [outputs]. + await writeNativeAssetsYaml(<Asset>[], environment.buildDir.uri, fileSystem); + dependencies = <Uri>[]; + } + case TargetPlatform.android_arm: + case TargetPlatform.android_arm64: + case TargetPlatform.android_x64: + case TargetPlatform.android_x86: + case TargetPlatform.android: + case TargetPlatform.fuchsia_arm64: + case TargetPlatform.fuchsia_x64: + case TargetPlatform.web_javascript: + // TODO(dacoharkes): Implement other OSes. https://github.com/flutter/flutter/issues/129757 + // Write the file we claim to have in the [outputs]. + await writeNativeAssetsYaml(<Asset>[], environment.buildDir.uri, fileSystem); + dependencies = <Uri>[]; + } + + final File nativeAssetsFile = environment.buildDir.childFile('native_assets.yaml'); + final Depfile depfile = Depfile( + <File>[ + for (final Uri dependency in dependencies) fileSystem.file(dependency), + ], + <File>[ + nativeAssetsFile, + ], + ); + final File outputDepfile = environment.buildDir.childFile('native_assets.d'); + if (!outputDepfile.parent.existsSync()) { + outputDepfile.parent.createSync(recursive: true); + } + environment.depFileService.writeToFile(depfile, outputDepfile); + if (!await nativeAssetsFile.exists()) { + throwToolExit("${nativeAssetsFile.path} doesn't exist."); + } + if (!await outputDepfile.exists()) { + throwToolExit("${outputDepfile.path} doesn't exist."); + } + } + + @override + List<String> get depfiles => <String>[ + 'native_assets.d', + ]; + + @override + List<Target> get dependencies => <Target>[]; + + @override + List<Source> get inputs => const <Source>[ + Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart'), + // If different packages are resolved, different native assets might need to be built. + Source.pattern('{PROJECT_DIR}/.dart_tool/package_config_subset'), + ]; + + @override + String get name => 'native_assets'; + + @override + List<Source> get outputs => const <Source>[ + Source.pattern('{BUILD_DIR}/native_assets.yaml'), + ]; +} diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart index 6eed94ee93a88..ef7bfdbddb095 100644 --- a/packages/flutter_tools/lib/src/bundle.dart +++ b/packages/flutter_tools/lib/src/bundle.dart @@ -33,7 +33,7 @@ String getDefaultCachedKernelPath({ }) { final StringBuffer buffer = StringBuffer(); final List<String> cacheFrontEndOptions = extraFrontEndOptions.toList() - ..removeWhere((String arg) => arg.startsWith('--enable-experiment=') || arg == '--flutter-widget-cache'); + ..removeWhere((String arg) => arg.startsWith('--enable-experiment=')); buffer.writeAll(dartDefines); buffer.writeAll(cacheFrontEndOptions); String buildPrefix = ''; diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 13efbde879dde..5a03827cb3ecd 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -190,6 +190,7 @@ class Cache { httpClient: HttpClient(), allowedBaseUrls: <String>[ storageBaseUrl, + realmlessStorageBaseUrl, cipdBaseUrl, ], ); @@ -447,6 +448,22 @@ class Cache { } String? _engineRevision; + /// The "realm" for the storage URL. + /// + /// For production artifacts from Engine post-submit and release builds, + /// this string will be empty, and the `storageBaseUrl` will be unmodified. + /// When non-empty, this string will be appended to the `storageBaseUrl` after + /// a '/'. For artifacts generated by Engine presubmits, the realm should be + /// "flutter_archives_v2". + String get storageRealm { + _storageRealm ??= getRealmFor('engine'); + if (_storageRealm == null) { + throwToolExit('Could not determine engine realm.'); + } + return _storageRealm!; + } + String? _storageRealm; + /// The base for URLs that store Flutter engine artifacts that are fetched /// during the installation of the Flutter SDK. /// @@ -459,11 +476,14 @@ class Cache { /// * [cipdBaseUrl], which determines how CIPD artifacts are fetched. /// * [Cache] class-level dartdocs that explain how artifact mirrors work. String get storageBaseUrl { - final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl]; + String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl]; if (overrideUrl == null) { - return 'https://storage.googleapis.com'; + return storageRealm.isEmpty + ? 'https://storage.googleapis.com' + : 'https://storage.googleapis.com/$storageRealm'; } // verify that this is a valid URI. + overrideUrl = storageRealm.isEmpty ? overrideUrl : '$overrideUrl/$storageRealm'; try { Uri.parse(overrideUrl); } on FormatException catch (err) { @@ -473,6 +493,12 @@ class Cache { return overrideUrl; } + String get realmlessStorageBaseUrl { + return storageRealm.isEmpty + ? storageBaseUrl + : storageBaseUrl.replaceAll('/$storageRealm', ''); + } + /// The base for URLs that store Flutter engine artifacts in CIPD. /// /// For some platforms, such as Web and Fuchsia, CIPD artifacts are fetched @@ -532,7 +558,7 @@ class Cache { /// Return the top-level directory in the cache; this is `bin/cache`. Directory getRoot() { if (_rootOverride != null) { - return _fileSystem.directory(_fileSystem.path.join(_rootOverride!.path, 'bin', 'cache')); + return _fileSystem.directory(_fileSystem.path.join(_rootOverride.path, 'bin', 'cache')); } else { return _fileSystem.directory(_fileSystem.path.join(flutterRoot!, 'bin', 'cache')); } @@ -607,6 +633,16 @@ class Cache { return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null; } + String? getRealmFor(String artifactName) { + final File realmFile = _fileSystem.file(_fileSystem.path.join( + _rootOverride?.path ?? flutterRoot!, + 'bin', + 'internal', + '$artifactName.realm', + )); + return realmFile.existsSync() ? realmFile.readAsStringSync().trim() : ''; + } + /// Delete all stamp files maintained by the cache. void clearStampFiles() { try { diff --git a/packages/flutter_tools/lib/src/cmake.dart b/packages/flutter_tools/lib/src/cmake.dart index 09adc69f8e673..2ac7d88e31bfd 100644 --- a/packages/flutter_tools/lib/src/cmake.dart +++ b/packages/flutter_tools/lib/src/cmake.dart @@ -4,9 +4,9 @@ import 'package:pub_semver/pub_semver.dart'; +import 'base/logger.dart'; import 'build_info.dart'; import 'cmake_project.dart'; -import 'globals.dart' as globals; /// Extracts the `BINARY_NAME` from a project's CMake file. /// @@ -41,12 +41,12 @@ String _determineVersionString(CmakeBasedProject project, BuildInfo buildInfo) { : buildName; } -Version _determineVersion(CmakeBasedProject project, BuildInfo buildInfo) { +Version _determineVersion(CmakeBasedProject project, BuildInfo buildInfo, Logger logger) { final String version = _determineVersionString(project, buildInfo); try { return Version.parse(version); } on FormatException { - globals.printWarning('Warning: could not parse version $version, defaulting to 1.0.0.'); + logger.printWarning('Warning: could not parse version $version, defaulting to 1.0.0.'); return Version(1, 0, 0); } @@ -74,25 +74,27 @@ void writeGeneratedCmakeConfig( String flutterRoot, CmakeBasedProject project, BuildInfo buildInfo, - Map<String, String> environment) { + Map<String, String> environment, + Logger logger, +) { // Only a limited set of variables are needed by the CMake files themselves, // the rest are put into a list to pass to the re-entrant build step. final String escapedFlutterRoot = _escapeBackslashes(flutterRoot); final String escapedProjectDir = _escapeBackslashes(project.parent.directory.path); - final Version version = _determineVersion(project, buildInfo); + final Version version = _determineVersion(project, buildInfo, logger); final int? buildVersion = _tryDetermineBuildVersion(version); // Since complex Dart build identifiers cannot be converted into integers, // different Dart versions may be converted into the same Windows numeric version. // Warn the user as some Windows installers, like MSI, don't update files if their versions are equal. if (buildVersion == null && project is WindowsProject) { - final String buildIdentifier = version.build.join('.'); - globals.printWarning( - 'Warning: build identifier $buildIdentifier in version $version is not numeric ' - 'and cannot be converted into a Windows build version number. Defaulting to 0.\n' - 'This may cause issues with Windows installers.' - ); + final String buildIdentifier = version.build.join('.'); + logger.printWarning( + 'Warning: build identifier $buildIdentifier in version $version is not numeric ' + 'and cannot be converted into a Windows build version number. Defaulting to 0.\n' + 'This may cause issues with Windows installers.' + ); } final StringBuffer buffer = StringBuffer(''' diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index 2c1c63798bf3c..68b66a425caca 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -11,11 +11,14 @@ import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/terminal.dart'; +import '../project.dart'; import '../project_validator.dart'; import '../runner/flutter_command.dart'; import 'analyze_base.dart'; import 'analyze_continuously.dart'; import 'analyze_once.dart'; +import 'android_analyze.dart'; +import 'ios_analyze.dart'; import 'validate_project.dart'; class AnalyzeCommand extends FlutterCommand { @@ -99,6 +102,71 @@ class AnalyzeCommand extends FlutterCommand { argParser.addFlag('fatal-warnings', help: 'Treat warning level issues as fatal.', defaultsTo: true); + + argParser.addFlag('android', + negatable: false, + help: 'Analyze Android sub-project. Used by internal tools only.', + hide: !verboseHelp, + ); + + argParser.addFlag('ios', + negatable: false, + help: 'Analyze iOS Xcode sub-project. Used by internal tools only.', + hide: !verboseHelp, + ); + + if (verboseHelp) { + argParser.addSeparator('Usage: flutter analyze --android [arguments]'); + } + + argParser.addFlag('list-build-variants', + negatable: false, + help: 'Print out a list of available build variants for the ' + 'Android sub-project.', + hide: !verboseHelp, + ); + + argParser.addFlag('output-app-link-settings', + negatable: false, + help: 'Output a JSON with Android app link settings into a file. ' + 'The "--build-variant" must also be set.', + hide: !verboseHelp, + ); + + argParser.addOption('build-variant', + help: 'Sets the Android build variant to be analyzed.', + valueHelp: 'build variant', + hide: !verboseHelp, + ); + + if (verboseHelp) { + argParser.addSeparator('Usage: flutter analyze --ios [arguments]'); + } + + argParser.addFlag('list-build-options', + help: 'Print out a list of available build options for the ' + 'iOS Xcode sub-project.', + hide: !verboseHelp, + ); + + argParser.addFlag('output-universal-link-settings', + negatable: false, + help: 'Output a JSON with iOS Xcode universal link settings into a file. ' + 'The "--configuration" and "--target" must be set.', + hide: !verboseHelp, + ); + + argParser.addOption('configuration', + help: 'Sets the iOS build configuration to be analyzed.', + valueHelp: 'configuration', + hide: !verboseHelp, + ); + + argParser.addOption('target', + help: 'Sets the iOS build target to be analyzed.', + valueHelp: 'target', + hide: !verboseHelp, + ); } /// The working directory for testing analysis using dartanalyzer. @@ -142,12 +210,91 @@ class AnalyzeCommand extends FlutterCommand { return false; } + // Don't run pub if asking for android analysis. + if (boolArg('android')) { + return false; + } + return super.shouldRunPub; } @override Future<FlutterCommandResult> runCommand() async { - if (boolArg('suggestions')) { + if (boolArg('android')) { + final AndroidAnalyzeOption option; + final String? buildVariant; + if (argResults!['list-build-variants'] as bool && argResults!['output-app-link-settings'] as bool) { + throwToolExit('Only one of "--list-build-variants" or "--output-app-link-settings" can be provided'); + } + if (argResults!['list-build-variants'] as bool) { + option = AndroidAnalyzeOption.listBuildVariant; + buildVariant = null; + } else if (argResults!['output-app-link-settings'] as bool) { + option = AndroidAnalyzeOption.outputAppLinkSettings; + buildVariant = argResults!['build-variant'] as String?; + if (buildVariant == null) { + throwToolExit('"--build-variant" must be provided'); + } + } else { + throwToolExit('No argument is provided to analyze. Use -h to see available commands.'); + } + final Set<String> items = findDirectories(argResults!, _fileSystem); + final String directoryPath; + if (items.isEmpty) { // user did not specify any path + directoryPath = _fileSystem.currentDirectory.path; + } else if (items.length > 1) { // if the user sends more than one path + throwToolExit('The Android analyze can process only one directory path'); + } else { + directoryPath = items.first; + } + await AndroidAnalyze( + fileSystem: _fileSystem, + option: option, + userPath: directoryPath, + buildVariant: buildVariant, + logger: _logger, + ).analyze(); + } else if (boolArg('ios')) { + final IOSAnalyzeOption option; + final String? configuration; + final String? target; + if (argResults!['list-build-options'] as bool && argResults!['output-universal-link-settings'] as bool) { + throwToolExit('Only one of "--list-build-options" or "--output-universal-link-settings" can be provided'); + } + if (argResults!['list-build-options'] as bool) { + option = IOSAnalyzeOption.listBuildOptions; + configuration = null; + target = null; + } else if (argResults!['output-universal-link-settings'] as bool) { + option = IOSAnalyzeOption.outputUniversalLinkSettings; + configuration = argResults!['configuration'] as String?; + if (configuration == null) { + throwToolExit('"--configuration" must be provided'); + } + target = argResults!['target'] as String?; + if (target == null) { + throwToolExit('"--target" must be provided'); + } + } else { + throwToolExit('No argument is provided to analyze. Use -h to see available commands.'); + } + final Set<String> items = findDirectories(argResults!, _fileSystem); + final String directoryPath; + if (items.isEmpty) { // user did not specify any path + directoryPath = _fileSystem.currentDirectory.path; + } else if (items.length > 1) { // if the user sends more than one path + throwToolExit('The iOS analyze can process only one directory path'); + } else { + directoryPath = items.first; + } + await IOSAnalyze( + project: FlutterProject.fromDirectory(_fileSystem.directory(directoryPath)), + option: option, + configuration: configuration, + target: target, + logger: _logger, + ).analyze(); + } else if (boolArg('suggestions')) { final String directoryPath; if (boolArg('watch')) { throwToolExit('flag --watch is not compatible with --suggestions'); diff --git a/packages/flutter_tools/lib/src/commands/android_analyze.dart b/packages/flutter_tools/lib/src/commands/android_analyze.dart new file mode 100644 index 0000000000000..c4389ff41e166 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/android_analyze.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../base/file_system.dart'; +import '../base/logger.dart'; +import '../convert.dart'; +import '../project.dart'; + +/// The type of analysis to perform. +enum AndroidAnalyzeOption { + /// Prints out available build variants of the Android sub-project. + /// + /// An example output: + /// ["debug", "profile", "release"] + listBuildVariant, + + /// Outputs app link settings of the Android sub-project into a file. + /// + /// The file path will be printed after the command is run successfully. + outputAppLinkSettings, +} + +/// Analyze the Android sub-project of a Flutter project. +/// +/// The [userPath] must be point to a flutter project. +class AndroidAnalyze { + AndroidAnalyze({ + required this.fileSystem, + required this.option, + required this.userPath, + this.buildVariant, + required this.logger, + }) : assert(option == AndroidAnalyzeOption.listBuildVariant || buildVariant != null); + + final FileSystem fileSystem; + final AndroidAnalyzeOption option; + final String? buildVariant; + final String userPath; + final Logger logger; + + Future<void> analyze() async { + final FlutterProject project = FlutterProject.fromDirectory(fileSystem.directory(userPath)); + switch (option) { + case AndroidAnalyzeOption.listBuildVariant: + logger.printStatus(jsonEncode(await project.android.getBuildVariants())); + case AndroidAnalyzeOption.outputAppLinkSettings: + assert(buildVariant != null); + await project.android.outputsAppLinkSettings(variant: buildVariant!); + final String filePath = fileSystem.path.join(project.directory.path, 'build', 'app', 'app-link-settings-$buildVariant.json`'); + logger.printStatus('result saved in $filePath'); + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 6bd6539181917..dfcd365d4098c 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -7,9 +7,7 @@ import 'dart:async'; import 'package:vm_service/vm_service.dart'; import '../android/android_device.dart'; -import '../artifacts.dart'; import '../base/common.dart'; -import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -65,7 +63,6 @@ class AttachCommand extends FlutterCommand { AttachCommand({ bool verboseHelp = false, HotRunnerFactory? hotRunnerFactory, - required Artifacts? artifacts, required Stdio stdio, required Logger logger, required Terminal terminal, @@ -73,15 +70,14 @@ class AttachCommand extends FlutterCommand { required Platform platform, required ProcessInfo processInfo, required FileSystem fileSystem, - }): _artifacts = artifacts, - _hotRunnerFactory = hotRunnerFactory ?? HotRunnerFactory(), - _stdio = stdio, - _logger = logger, - _terminal = terminal, - _signals = signals, - _platform = platform, - _processInfo = processInfo, - _fileSystem = fileSystem { + }) : _hotRunnerFactory = hotRunnerFactory ?? HotRunnerFactory(), + _stdio = stdio, + _logger = logger, + _terminal = terminal, + _signals = signals, + _platform = platform, + _processInfo = processInfo, + _fileSystem = fileSystem { addBuildModeFlags(verboseHelp: verboseHelp, defaultToRelease: false, excludeRelease: true); usesTargetOption(); usesPortOptions(verboseHelp: verboseHelp); @@ -145,7 +141,6 @@ class AttachCommand extends FlutterCommand { } final HotRunnerFactory _hotRunnerFactory; - final Artifacts? _artifacts; final Stdio _stdio; final Logger _logger; final Terminal _terminal; @@ -267,13 +262,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. throwToolExit('Did not find any valid target devices.'); } - final Artifacts? overrideArtifacts = device.artifactOverrides ?? _artifacts; - await context.run<void>( - body: () => _attachToDevice(device), - overrides: <Type, Generator>{ - Artifacts: () => overrideArtifacts, - }, - ); + await _attachToDevice(device); return FlutterCommandResult.success(); } @@ -288,7 +277,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. logger: _logger, ), notifyingLogger: (_logger is NotifyingLogger) - ? _logger as NotifyingLogger + ? _logger : NotifyingLogger(verbose: _logger.isVerbose, parent: _logger), logToStdout: true, ) @@ -502,6 +491,13 @@ known, it can be explicitly provided to attach via the command-line, e.g. for (final ForwardedPort port in ports) { await device.portForwarder!.unforward(port); } + // However we exited from the runner, ensure the terminal has line mode + // and echo mode enabled before we return the user to the shell. + try { + _terminal.singleCharMode = false; + } on StdinException { + // Do nothing, if the STDIN handle is no longer available, there is nothing actionable for us to do at this point + } } } diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 27a483aa08cab..fba0c14e95a41 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import '../base/analyze_size.dart'; import '../base/common.dart'; +import '../base/error_handling_io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/utils.dart'; @@ -20,6 +21,7 @@ import '../globals.dart' as globals; import '../ios/application_package.dart'; import '../ios/mac.dart'; import '../ios/plist_parser.dart'; +import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'build.dart'; @@ -378,7 +380,8 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleShortVersionStringKey); xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleVersionKey); - xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleDisplayNameKey); + xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleDisplayNameKey) + ?? globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleNameKey); xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kMinimumOSVersionKey); xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleIdentifierKey); @@ -485,7 +488,9 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ], ); } finally { - generatedExportPlist?.deleteSync(); + if (generatedExportPlist != null) { + ErrorHandlingFileSystem.deleteIfExists(generatedExportPlist); + } status?.stop(); } @@ -723,6 +728,21 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { if (result.output != null) { globals.printStatus('Built ${result.output}.'); + // When an app is successfully built, record to analytics whether Impeller + // is enabled or disabled. + final BuildableIOSApp app = await buildableIOSApp; + final String plistPath = app.project.infoPlist.path; + final bool? impellerEnabled = globals.plistParser.getValueFromFile<bool>( + plistPath, PlistParser.kFLTEnableImpellerKey, + ); + BuildEvent( + impellerEnabled == false + ? 'plist-impeller-disabled' + : 'plist-impeller-enabled', + type: 'ios', + flutterUsage: globals.flutterUsage, + ).send(); + return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart index ea3c50bbfa9c1..66eea554289b6 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -270,6 +270,28 @@ class BuildIOSFrameworkCommand extends BuildFrameworkCommand { final Status status = globals.logger.startProgress( ' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); + + // Copy the native assets. The native assets have already been signed in + // buildNativeAssetsMacOS. + final Directory nativeAssetsDirectory = globals.fs + .directory(getBuildDirectory()) + .childDirectory('native_assets/ios/'); + if (await nativeAssetsDirectory.exists()) { + final ProcessResult rsyncResult = await globals.processManager.run(<Object>[ + 'rsync', + '-av', + '--filter', + '- .DS_Store', + '--filter', + '- native_assets.yaml', + nativeAssetsDirectory.path, + modeDirectory.path, + ]); + if (rsyncResult.exitCode != 0) { + throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}'); + } + } + try { // Delete the intermediaries since they would have been copied into our // output frameworks. diff --git a/packages/flutter_tools/lib/src/commands/build_linux.dart b/packages/flutter_tools/lib/src/commands/build_linux.dart index 78515070034c5..dc709fc02b73b 100644 --- a/packages/flutter_tools/lib/src/commands/build_linux.dart +++ b/packages/flutter_tools/lib/src/commands/build_linux.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. + import '../base/analyze_size.dart'; import '../base/common.dart'; +import '../base/logger.dart'; import '../base/os.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -83,18 +85,20 @@ class BuildLinuxCommand extends BuildSubCommand { 'Cross-build from Linux x64 host to Linux arm64 target is not currently supported.'); } displayNullSafetyMode(buildInfo); + final Logger logger = globals.logger; await buildLinux( flutterProject.linux, buildInfo, target: targetFile, sizeAnalyzer: SizeAnalyzer( fileSystem: globals.fs, - logger: globals.logger, + logger: logger, flutterUsage: globals.flutterUsage, ), needCrossBuild: needCrossBuild, targetPlatform: targetPlatform, targetSysroot: stringArg('target-sysroot')!, + logger: logger, ); return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart index 77a02511dc01b..19c1e17ed505f 100644 --- a/packages/flutter_tools/lib/src/commands/build_macos_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_macos_framework.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/utils.dart'; @@ -96,6 +97,26 @@ class BuildMacOSFrameworkCommand extends BuildFrameworkCommand { globals.logger.printStatus(' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}'); + // Copy the native assets. + final Directory nativeAssetsDirectory = globals.fs + .directory(getBuildDirectory()) + .childDirectory('native_assets/macos/'); + if (await nativeAssetsDirectory.exists()) { + final ProcessResult rsyncResult = await globals.processManager.run(<Object>[ + 'rsync', + '-av', + '--filter', + '- .DS_Store', + '--filter', + '- native_assets.yaml', + nativeAssetsDirectory.path, + modeDirectory.path, + ]); + if (rsyncResult.exitCode != 0) { + throwToolExit('Failed to copy native assets:\n${rsyncResult.stderr}'); + } + } + // Delete the intermediaries since they would have been copied into our // output frameworks. if (buildOutput.existsSync()) { diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart index 3b5128b9f8334..a0afbe2232cb3 100644 --- a/packages/flutter_tools/lib/src/commands/build_web.dart +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -72,8 +72,9 @@ class BuildWebCommand extends BuildSubCommand { ); argParser.addOption('dart2js-optimization', help: 'Sets the optimization level used for Dart compilation to JavaScript. ' - 'Valid values range from O0 to O4.', - defaultsTo: JsCompilerConfig.kDart2jsDefaultOptimizationLevel + 'Valid values range from O1 to O4.', + defaultsTo: JsCompilerConfig.kDart2jsDefaultOptimizationLevel, + allowed: const <String>['O1', 'O2', 'O3', 'O4'], ); argParser.addFlag('dump-info', negatable: false, help: 'Passes "--dump-info" to the Javascript compiler which generates ' diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart index ae03cf38616b6..0b22f065c9654 100644 --- a/packages/flutter_tools/lib/src/commands/config.dart +++ b/packages/flutter_tools/lib/src/commands/config.dart @@ -11,11 +11,20 @@ import '../features.dart'; import '../globals.dart' as globals; import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; class ConfigCommand extends FlutterCommand { ConfigCommand({ bool verboseHelp = false }) { + argParser.addFlag( + 'list', + help: 'List all settings and their current values.', + negatable: false, + ); argParser.addFlag('analytics', - help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.'); + hide: !verboseHelp, + help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.\n' + '(An alias for "--${FlutterGlobalOptions.kEnableAnalyticsFlag}" ' + 'and "--${FlutterGlobalOptions.kDisableAnalyticsFlag}" top level flags.)'); argParser.addFlag('clear-ios-signing-cert', negatable: false, help: 'Clear the saved development certificate choice used to sign apps for iOS device deployment.'); @@ -69,37 +78,7 @@ class ConfigCommand extends FlutterCommand { bool get shouldUpdateCache => false; @override - String get usageFooter { - // List all config settings. for feature flags, include whether they - // are available. - final Map<String, Feature> featuresByName = <String, Feature>{}; - final String channel = globals.flutterVersion.channel; - for (final Feature feature in allFeatures) { - final String? configSetting = feature.configSetting; - if (configSetting != null) { - featuresByName[configSetting] = feature; - } - } - String values = globals.config.keys - .map<String>((String key) { - String configFooter = ''; - if (featuresByName.containsKey(key)) { - final FeatureChannelSetting setting = featuresByName[key]!.getSettingForChannel(channel); - if (!setting.available) { - configFooter = '(Unavailable)'; - } - } - return ' $key: ${globals.config.getValue(key)} $configFooter'; - }).join('\n'); - if (values.isEmpty) { - values = ' No settings have been configured.'; - } - final bool analyticsEnabled = globals.flutterUsage.enabled && - !globals.flutterUsage.suppressAnalytics; - return - '\nSettings:\n$values\n\n' - 'Analytics reporting is currently ${analyticsEnabled ? 'enabled' : 'disabled'}.'; - } + String get usageFooter => '\n$analyticsUsage'; /// Return null to disable analytics recording of the `config` command. @override @@ -117,6 +96,11 @@ class ConfigCommand extends FlutterCommand { ' flutter config --android-studio-dir "/opt/Android Studio"'); } + if (boolArg('list')) { + globals.printStatus(settingsText); + return FlutterCommandResult.success(); + } + if (boolArg('machine')) { await handleMachine(); return FlutterCommandResult.success(); @@ -129,6 +113,7 @@ class ConfigCommand extends FlutterCommand { globals.config.removeValue(configSetting); } } + globals.printStatus(requireReloadTipText); return FlutterCommandResult.success(); } @@ -191,7 +176,7 @@ class ConfigCommand extends FlutterCommand { if (argResults == null || argResults!.arguments.isEmpty) { globals.printStatus(usage); } else { - globals.printStatus('\nYou may need to restart any open editors for them to read new settings.'); + globals.printStatus('\n$requireReloadTipText'); } return FlutterCommandResult.success(); @@ -230,4 +215,50 @@ class ConfigCommand extends FlutterCommand { globals.printStatus('Setting "$keyName" value to "$keyValue".'); } } + + /// List all config settings. for feature flags, include whether they are available. + String get settingsText { + final Map<String, Feature> featuresByName = <String, Feature>{}; + final String channel = globals.flutterVersion.channel; + for (final Feature feature in allFeatures) { + final String? configSetting = feature.configSetting; + if (configSetting != null) { + featuresByName[configSetting] = feature; + } + } + final Set<String> keys = <String>{ + ...allFeatures.map((Feature e) => e.configSetting).whereType<String>(), + ...globals.config.keys, + }; + final Iterable<String> settings = keys.map<String>((String key) { + Object? value = globals.config.getValue(key); + value ??= '(Not set)'; + final StringBuffer buffer = StringBuffer(' $key: $value'); + if (featuresByName.containsKey(key)) { + final FeatureChannelSetting setting = featuresByName[key]!.getSettingForChannel(channel); + if (!setting.available) { + buffer.write(' (Unavailable)'); + } + } + return buffer.toString(); + }); + final StringBuffer buffer = StringBuffer(); + buffer.writeln('All Settings:'); + if (settings.isEmpty) { + buffer.writeln(' No configs have been configured.'); + } else { + buffer.writeln(settings.join('\n')); + } + return buffer.toString(); + } + + /// List the status of the analytics reporting. + String get analyticsUsage { + final bool analyticsEnabled = + globals.flutterUsage.enabled && !globals.flutterUsage.suppressAnalytics; + return 'Analytics reporting is currently ${analyticsEnabled ? 'enabled' : 'disabled'}.'; + } + + /// Raising the reload tip for setting changes. + final String requireReloadTipText = 'You may need to restart any open editors for them to read new settings.'; } diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 66a9e1127e600..97d70b067288a 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; + import '../android/gradle_utils.dart' as gradle; import '../base/common.dart'; import '../base/context.dart'; @@ -9,6 +11,8 @@ import '../base/file_system.dart'; import '../base/net.dart'; import '../base/terminal.dart'; import '../base/utils.dart'; +import '../base/version.dart'; +import '../base/version_range.dart'; import '../convert.dart'; import '../dart/pub.dart'; import '../features.dart'; @@ -36,10 +40,11 @@ class CreateCommand extends CreateBase { argParser.addOption( 'template', abbr: 't', - allowed: FlutterProjectType.values.map<String>((FlutterProjectType e) => e.cliName), + allowed: FlutterProjectType.enabledValues + .map<String>((FlutterProjectType e) => e.cliName), help: 'Specify the type of project to create.', valueHelp: 'type', - allowedHelp: CliEnum.allowedHelp(FlutterProjectType.values), + allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues), ); argParser.addOption( 'sample', @@ -206,12 +211,14 @@ class CreateCommand extends CreateBase { final FlutterProjectType template = _getProjectType(projectDir); final bool generateModule = template == FlutterProjectType.module; final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin; + final bool generateFfiPackage = template == FlutterProjectType.packageFfi; final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi; + final bool generateFfi = generateFfiPlugin || generateFfiPackage; final bool generatePackage = template == FlutterProjectType.package; final List<String> platforms = stringsArg('platforms'); // `--platforms` does not support module or package. - if (argResults!.wasParsed('platforms') && (generateModule || generatePackage)) { + if (argResults!.wasParsed('platforms') && (generateModule || generatePackage || generateFfiPackage)) { final String template = generateModule ? 'module' : 'package'; throwToolExit( 'The "--platforms" argument is not supported in $template template.', @@ -225,15 +232,15 @@ class CreateCommand extends CreateBase { 'The web platform is not supported in plugin_ffi template.', exitCode: 2, ); - } else if (generateFfiPlugin && argResults!.wasParsed('ios-language')) { + } else if (generateFfi && argResults!.wasParsed('ios-language')) { throwToolExit( - 'The "ios-language" option is not supported with the plugin_ffi ' + 'The "ios-language" option is not supported with the ${template.cliName} ' 'template: the language will always be C or C++.', exitCode: 2, ); - } else if (generateFfiPlugin && argResults!.wasParsed('android-language')) { + } else if (generateFfi && argResults!.wasParsed('android-language')) { throwToolExit( - 'The "android-language" option is not supported with the plugin_ffi ' + 'The "android-language" option is not supported with the ${template.cliName} ' 'template: the language will always be C or C++.', exitCode: 2, ); @@ -306,6 +313,7 @@ class CreateCommand extends CreateBase { flutterRoot: flutterRoot, withPlatformChannelPluginHook: generateMethodChannelsPlugin, withFfiPluginHook: generateFfiPlugin, + withFfiPackage: generateFfiPackage, withEmptyMain: emptyArgument, androidLanguage: stringArg('android-language'), iosLanguage: stringArg('ios-language'), @@ -393,6 +401,15 @@ class CreateCommand extends CreateBase { projectType: template, ); pubContext = PubContext.createPlugin; + case FlutterProjectType.packageFfi: + generatedFileCount += await _generateFfiPackage( + relativeDir, + templateContext, + overwrite: overwrite, + printStatusWhenWriting: !creatingNewProject, + projectType: template, + ); + pubContext = PubContext.createPackage; } if (boolArg('pub')) { @@ -403,14 +420,21 @@ class CreateCommand extends CreateBase { offline: boolArg('offline'), outputMode: PubOutputMode.summaryOnly, ); - await project.ensureReadyForPlatformSpecificTooling( - androidPlatform: includeAndroid, - iosPlatform: includeIos, - linuxPlatform: includeLinux, - macOSPlatform: includeMacos, - windowsPlatform: includeWindows, - webPlatform: includeWeb, - ); + // Setting `includeIos` etc to false as with FlutterProjectType.package + // causes the example sub directory to not get os sub directories. + // This will lead to `flutter build ios` to fail in the example. + // TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874 + // Then this if can be removed. + if (!generateFfiPackage) { + await project.ensureReadyForPlatformSpecificTooling( + androidPlatform: includeAndroid, + iosPlatform: includeIos, + linuxPlatform: includeLinux, + macOSPlatform: includeMacos, + windowsPlatform: includeWindows, + webPlatform: includeWeb, + ); + } } if (sampleCode != null) { _applySample(relativeDir, sampleCode); @@ -481,6 +505,19 @@ Your $application code is in $relativeAppMain. } } + // Show warning for Java/AGP or Java/Gradle incompatibility if building for + // Android and Java version has been detected. + if (includeAndroid && globals.java?.version != null) { + _printIncompatibleJavaAgpGradleVersionsWarning( + javaVersion: versionToParsableString(globals.java?.version)!, + templateGradleVersion: templateContext['gradleVersion']! as String, + templateAgpVersion: templateContext['agpVersion']! as String, + templateAgpVersionForModule: templateContext['agpVersionForModule']! as String, + projectType: template, + projectDirPath: projectDirPath, + ); + } + return FlutterCommandResult.success(); } @@ -663,6 +700,48 @@ Your $application code is in $relativeAppMain. return generatedCount; } + Future<int> _generateFfiPackage( + Directory directory, + Map<String, Object?> templateContext, { + bool overwrite = false, + bool printStatusWhenWriting = true, + required FlutterProjectType projectType, + }) async { + int generatedCount = 0; + final String? description = argResults!.wasParsed('description') + ? stringArg('description') + : 'A new Dart FFI package project.'; + templateContext['description'] = description; + generatedCount += await renderMerged( + <String>[ + 'package_ffi', + ], + directory, + templateContext, + overwrite: overwrite, + printStatusWhenWriting: printStatusWhenWriting, + ); + + final FlutterProject project = FlutterProject.fromDirectory(directory); + + final String? projectName = templateContext['projectName'] as String?; + final String exampleProjectName = '${projectName}_example'; + templateContext['projectName'] = exampleProjectName; + templateContext['description'] = 'Demonstrates how to use the $projectName package.'; + templateContext['pluginProjectName'] = projectName; + + generatedCount += await generateApp( + <String>['app'], + project.example.directory, + templateContext, + overwrite: overwrite, + pluginExampleApp: true, + printStatusWhenWriting: printStatusWhenWriting, + projectType: projectType, + ); + return generatedCount; + } + // Takes an application template and replaces the main.dart with one from the // documentation website in sampleCode. Returns the difference in the number // of files after applying the sample, since it also deletes the application's @@ -791,3 +870,165 @@ For more details, see: https://flutter.dev/docs/get-started/web '''); } } + +// Prints a warning if the specified Java version conflicts with either the +// template Gradle or AGP version. +// +// Assumes the specified templateGradleVersion and templateAgpVersion are +// compatible, meaning that the Java version may only conflict with one of the +// template Gradle or AGP versions. +void _printIncompatibleJavaAgpGradleVersionsWarning({ + required String javaVersion, + required String templateGradleVersion, + required String templateAgpVersion, + required String templateAgpVersionForModule, + required FlutterProjectType projectType, + required String projectDirPath}) { + // Determine if the Java version specified conflicts with the template Gradle or AGP version. + final bool javaGradleVersionsCompatible = gradle.validateJavaAndGradle(globals.logger, javaV: javaVersion, gradleV: templateGradleVersion); + bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersion); + String relevantTemplateAgpVersion = templateAgpVersion; + + if (projectType == FlutterProjectType.module && Version.parse(templateAgpVersion)! < Version.parse(templateAgpVersionForModule)!) { + // If a module is being created, make sure to check for Java/AGP compatibility between the highest used version of AGP in the module template. + javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersionForModule); + relevantTemplateAgpVersion = templateAgpVersionForModule; + } + + if (javaGradleVersionsCompatible && javaAgpVersionsCompatible) { + return; + } + + // Determine header of warning with recommended fix of re-configuring Java version. + final String incompatibleVersionsAndRecommendedOptionMessage = getIncompatibleJavaGradleAgpMessageHeader(javaGradleVersionsCompatible, templateGradleVersion, relevantTemplateAgpVersion, projectType.cliName); + + if (!javaGradleVersionsCompatible) { + + if (projectType == FlutterProjectType.plugin || projectType == FlutterProjectType.pluginFfi) { + // Only impacted files could be in sample code. + return; + } + + // Gradle template version incompatible with Java version. + final gradle.JavaGradleCompat? validCompatibleGradleVersionRange = gradle.getValidGradleVersionRangeForJavaVersion(globals.logger, javaV: javaVersion); + final String compatibleGradleVersionMessage = validCompatibleGradleVersionRange == null ? '' : ' (compatible Gradle version range: ${validCompatibleGradleVersionRange.gradleMin} - ${validCompatibleGradleVersionRange.gradleMax})'; + + globals.printWarning(''' +$incompatibleVersionsAndRecommendedOptionMessage + +Alternatively, to continue using your configured Java version, update the Gradle +version specified in the following file to a compatible Gradle version$compatibleGradleVersionMessage: +${_getGradleWrapperPropertiesFilePath(projectType, projectDirPath)} + +You may also update the Gradle version used by running +`./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>`. + +See +https://docs.gradle.org/current/userguide/compatibility.html#java for details +on compatible Java/Gradle versions, and see +https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper +for more details on using the Gradle Wrapper command to update the Gradle version +used. +''', + emphasis: true + ); + return; + } + + // AGP template version incompatible with Java version. + final gradle.JavaAgpCompat? minimumCompatibleAgpVersion = gradle.getMinimumAgpVersionForJavaVersion(globals.logger, javaV: javaVersion); + final String compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null ? '' : ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})'; + final String gradleBuildFilePaths = ' - ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n - ')}'; + + globals.printWarning(''' +$incompatibleVersionsAndRecommendedOptionMessage + +Alternatively, to continue using your configured Java version, update the AGP +version specified in the following files to a compatible AGP +version$compatibleAgpVersionMessage as necessary: +$gradleBuildFilePaths + +See +https://developer.android.com/build/releases/gradle-plugin for details on +compatible Java/AGP versions. +''', + emphasis: true + ); +} + +// Returns incompatible Java/template Gradle/template AGP message header based +// on incompatibility and project type. +@visibleForTesting +String getIncompatibleJavaGradleAgpMessageHeader( + bool javaGradleVersionsCompatible, + String templateGradleVersion, + String templateAgpVersion, + String projectType) { + final String incompatibleDependency = javaGradleVersionsCompatible ? 'Android Gradle Plugin (AGP)' :'Gradle' ; + final String incompatibleDependencyVersion = javaGradleVersionsCompatible ? 'AGP version $templateAgpVersion' : 'Gradle version $templateGradleVersion'; + final VersionRange validJavaRange = gradle.getJavaVersionFor(gradleV: templateGradleVersion, agpV: templateAgpVersion); + // validJavaRange should have non-null verisonMin and versionMax since it based on our template AGP and Gradle versions. + final String validJavaRangeMessage = '(Java ${validJavaRange.versionMin!} <= compatible Java version < Java ${validJavaRange.versionMax!})'; + + return ''' +The configured version of Java detected may conflict with the $incompatibleDependency version in your new Flutter $projectType. + +[RECOMMENDED] If so, to keep the default $incompatibleDependencyVersion, make +sure to download a compatible Java version +$validJavaRangeMessage. +You may configure this compatible Java version by running: +`flutter config --jdk-dir=<JDK_DIRECTORY>` +Note that this is a global configuration for Flutter. +'''; +} + +// Returns path of the gradle-wrapper.properties file for the specified +// generated project type. +String? _getGradleWrapperPropertiesFilePath(FlutterProjectType projectType, String projectDirPath) { + String gradleWrapperPropertiesFilePath = ''; + switch (projectType) { + case FlutterProjectType.app: + case FlutterProjectType.skeleton: + gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, 'android/gradle/wrapper/gradle-wrapper.properties'); + case FlutterProjectType.module: + gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, '.android/gradle/wrapper/gradle-wrapper.properties'); + case FlutterProjectType.plugin: + case FlutterProjectType.pluginFfi: + case FlutterProjectType.package: + case FlutterProjectType.packageFfi: + // TODO(camsim99): Add relevant file path for packageFfi when Android is supported. + // No gradle-wrapper.properties files not part of sample code that + // can be determined. + return null; + } + return gradleWrapperPropertiesFilePath; +} + +// Returns the path(s) of the build.gradle file(s) for the specified generated +// project type. +List<String>? _getBuildGradleConfigurationFilePaths(FlutterProjectType projectType, String projectDirPath) { + final List<String> buildGradleConfigurationFilePaths = <String>[]; + switch (projectType) { + case FlutterProjectType.app: + case FlutterProjectType.skeleton: + case FlutterProjectType.pluginFfi: + buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/build.gradle')); + case FlutterProjectType.module: + const String moduleBuildGradleFilePath = '.android/build.gradle'; + const String moduleAppBuildGradleFlePath = '.android/app/build.gradle'; + const String moduleFlutterBuildGradleFilePath = '.android/Flutter/build.gradle'; + buildGradleConfigurationFilePaths.addAll(<String>[ + globals.fs.path.join(projectDirPath, moduleBuildGradleFilePath), + globals.fs.path.join(projectDirPath, moduleAppBuildGradleFlePath), + globals.fs.path.join(projectDirPath, moduleFlutterBuildGradleFilePath), + ]); + case FlutterProjectType.plugin: + buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/app/build.gradle')); + case FlutterProjectType.package: + case FlutterProjectType.packageFfi: + // TODO(camsim99): Add any relevant file paths for packageFfi when Android is supported. + // No build.gradle file because there is no platform-specific implementation. + return null; + } + return buildGradleConfigurationFilePaths; +} diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 90c3fa00bbb82..293f96bd8d5b9 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -352,6 +352,7 @@ abstract class CreateBase extends FlutterCommand { String? gradleVersion, bool withPlatformChannelPluginHook = false, bool withFfiPluginHook = false, + bool withFfiPackage = false, bool withEmptyMain = false, bool ios = false, bool android = false, @@ -399,9 +400,11 @@ abstract class CreateBase extends FlutterCommand { 'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase, 'pluginDartClass': pluginDartClass, 'pluginProjectUUID': const Uuid().v4().toUpperCase(), + 'withFfi': withFfiPluginHook || withFfiPackage, + 'withFfiPackage': withFfiPackage, 'withFfiPluginHook': withFfiPluginHook, 'withPlatformChannelPluginHook': withPlatformChannelPluginHook, - 'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook, + 'withPluginHook': withFfiPluginHook || withFfiPackage || withPlatformChannelPluginHook, 'withEmptyMain': withEmptyMain, 'androidLanguage': androidLanguage, 'iosLanguage': iosLanguage, @@ -419,9 +422,9 @@ abstract class CreateBase extends FlutterCommand { 'dartSdkVersionBounds': dartSdkVersionBounds, 'implementationTests': implementationTests, 'agpVersion': agpVersion, + 'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule, 'kotlinVersion': kotlinVersion, 'gradleVersion': gradleVersion, - 'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule, 'compileSdkVersion': gradle.compileSdkVersion, 'minSdkVersion': gradle.minSdkVersion, 'ndkVersion': gradle.ndkVersion, @@ -784,12 +787,38 @@ bool isValidPackageName(String name) { !_keywords.contains(name); } +/// Returns a potential valid name from the given [name]. +/// +/// If a valid name cannot be found, returns `null`. +@visibleForTesting +String? potentialValidPackageName(String name){ + String newName = name.toLowerCase(); + if (newName.startsWith(RegExp(r'[0-9]'))) { + newName = '_$newName'; + } + newName = newName.replaceAll('-', '_'); + if (isValidPackageName(newName)) { + return newName; + } else { + return null; + } +} + // Return null if the project name is legal. Return a validation message if // we should disallow the project name. String? _validateProjectName(String projectName) { if (!isValidPackageName(projectName)) { - return '"$projectName" is not a valid Dart package name.\n\n' - 'See https://dart.dev/tools/pub/pubspec#name for more information.'; + final String? potentialValidName = potentialValidPackageName(projectName); + + return <String>[ + '"$projectName" is not a valid Dart package name.', + '\n\n', + 'The name should be all lowercase, with underscores to separate words, "just_like_this".', + 'Use only basic Latin letters and Arabic digits: [a-z0-9_].', + "Also, make sure the name is a valid Dart identifier—that it doesn't start with digits and isn't a reserved word.\n", + 'See https://dart.dev/tools/pub/pubspec#name for more information.', + if (potentialValidName != null) '\nTry "$potentialValidName" instead.', + ].join(); } if (_packageDependencies.contains(projectName)) { return "Invalid project name: '$projectName' - this will conflict with Flutter " diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index ab1659fecb95e..b63a9acdbb3c3 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -335,6 +335,7 @@ class DaemonDomain extends Domain { registerHandler('version', version); registerHandler('shutdown', shutdown); registerHandler('getSupportedPlatforms', getSupportedPlatforms); + registerHandler('setNotifyVerbose', setNotifyVerbose); sendEvent( 'daemon.connected', @@ -346,7 +347,7 @@ class DaemonDomain extends Domain { _subscription = daemon.notifyingLogger!.onMessage.listen((LogMessage message) { if (daemon.logToStdout) { - if (message.level == 'status') { + if (message.level == 'status' || message.level == 'trace') { // We use `print()` here instead of `stdout.writeln()` in order to // capture the print output for testing. // ignore: avoid_print @@ -461,6 +462,11 @@ class DaemonDomain extends Domain { }; } } + + /// If notifyVerbose is set, the daemon will forward all verbose logs. + Future<void> setNotifyVerbose(Map<String, Object?> args) async { + daemon.notifyingLogger?.notifyVerbose = _getBoolArg(args, 'verbose') ?? true; + } } typedef RunOrAttach = Future<void> Function({ @@ -1210,7 +1216,7 @@ Object? _toJsonable(Object? obj) { } class NotifyingLogger extends DelegatingLogger { - NotifyingLogger({ required this.verbose, required Logger parent }) : super(parent) { + NotifyingLogger({ required this.verbose, required Logger parent, this.notifyVerbose = false }) : super(parent) { _messageController = StreamController<LogMessage>.broadcast( onListen: _onListen, ); @@ -1220,6 +1226,8 @@ class NotifyingLogger extends DelegatingLogger { final List<LogMessage> messageBuffer = <LogMessage>[]; late StreamController<LogMessage> _messageController; + bool notifyVerbose = false; + void _onListen() { if (messageBuffer.isNotEmpty) { messageBuffer.forEach(_messageController.add); @@ -1277,6 +1285,10 @@ class NotifyingLogger extends DelegatingLogger { @override void printTrace(String message) { + if (notifyVerbose) { + _sendMessage(LogMessage('trace', message)); + return; + } if (!verbose) { return; } diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart index 9792a3d9f4be2..b71259248d52a 100644 --- a/packages/flutter_tools/lib/src/commands/devices.dart +++ b/packages/flutter_tools/lib/src/commands/devices.dart @@ -168,48 +168,43 @@ class DevicesCommandOutput { } if (allDevices.isEmpty) { - _printNoDevicesDetected(); + _logger.printStatus('No authorized devices detected.'); } else { if (attachedDevices.isNotEmpty) { - _logger.printStatus('${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n'); - await Device.printDevices(attachedDevices, _logger); + _logger.printStatus('Found ${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:'); + await Device.printDevices(attachedDevices, _logger, prefix: ' '); } if (wirelessDevices.isNotEmpty) { if (attachedDevices.isNotEmpty) { _logger.printStatus(''); } - _logger.printStatus('${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:\n'); - await Device.printDevices(wirelessDevices, _logger); + _logger.printStatus('Found ${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:'); + await Device.printDevices(wirelessDevices, _logger, prefix: ' '); } } - await _printDiagnostics(); + await _printDiagnostics(foundAny: allDevices.isNotEmpty); } - void _printNoDevicesDetected() { - final StringBuffer status = StringBuffer('No devices detected.'); - status.writeln(); + Future<void> _printDiagnostics({ required bool foundAny }) async { + final StringBuffer status = StringBuffer(); status.writeln(); + final List<String> diagnostics = await _deviceManager?.getDeviceDiagnostics() ?? <String>[]; + if (diagnostics.isNotEmpty) { + for (final String diagnostic in diagnostics) { + status.writeln(diagnostic); + status.writeln(); + } + } status.writeln('Run "flutter emulators" to list and start any available device emulators.'); status.writeln(); - status.write('If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. '); + status.write('If you expected ${ foundAny ? 'another' : 'a' } device to be detected, please run "flutter doctor" to diagnose potential issues. '); if (deviceDiscoveryTimeout == null) { - status.write('You may also try increasing the time to wait for connected devices with the --${FlutterOptions.kDeviceTimeout} flag. '); + status.write('You may also try increasing the time to wait for connected devices with the "--${FlutterOptions.kDeviceTimeout}" flag. '); } status.write('Visit https://flutter.dev/setup/ for troubleshooting tips.'); - _logger.printStatus(status.toString()); } - Future<void> _printDiagnostics() async { - final List<String> diagnostics = await _deviceManager?.getDeviceDiagnostics() ?? <String>[]; - if (diagnostics.isNotEmpty) { - _logger.printStatus(''); - for (final String diagnostic in diagnostics) { - _logger.printStatus('• $diagnostic', hangingIndent: 2); - } - } - } - Future<void> printDevicesAsJson(List<Device> devices) async { _logger.printStatus( const JsonEncoder.withIndent(' ').convert( @@ -266,8 +261,8 @@ class DevicesCommandOutputWithExtendedWirelessDeviceDiscovery extends DevicesCom // Display list of attached devices. if (attachedDevices.isNotEmpty) { - _logger.printStatus('${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n'); - await Device.printDevices(attachedDevices, _logger); + _logger.printStatus('Found ${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:'); + await Device.printDevices(attachedDevices, _logger, prefix: ' '); _logger.printStatus(''); numLinesToClear += 1; } @@ -292,8 +287,8 @@ class DevicesCommandOutputWithExtendedWirelessDeviceDiscovery extends DevicesCom if (_logger.isVerbose && _includeAttachedDevices) { // Reprint the attach devices. if (attachedDevices.isNotEmpty) { - _logger.printStatus('\n${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n'); - await Device.printDevices(attachedDevices, _logger); + _logger.printStatus('\nFound ${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:'); + await Device.printDevices(attachedDevices, _logger, prefix: ' '); } } else if (terminal.supportsColor && terminal is AnsiTerminal) { _logger.printStatus( @@ -309,16 +304,16 @@ class DevicesCommandOutputWithExtendedWirelessDeviceDiscovery extends DevicesCom if (wirelessDevices.isEmpty) { if (attachedDevices.isEmpty) { // No wireless or attached devices were found. - _printNoDevicesDetected(); + _logger.printStatus('No authorized devices detected.'); } else { // Attached devices found, wireless devices not found. _logger.printStatus(_noWirelessDevicesFoundMessage); } } else { // Display list of wireless devices. - _logger.printStatus('${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:\n'); - await Device.printDevices(wirelessDevices, _logger); + _logger.printStatus('Found ${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:'); + await Device.printDevices(wirelessDevices, _logger, prefix: ' '); } - await _printDiagnostics(); + await _printDiagnostics(foundAny: wirelessDevices.isNotEmpty || attachedDevices.isNotEmpty); } } diff --git a/packages/flutter_tools/lib/src/commands/generate_localizations.dart b/packages/flutter_tools/lib/src/commands/generate_localizations.dart index a7931f0618f5f..f7dd479fbe48e 100644 --- a/packages/flutter_tools/lib/src/commands/generate_localizations.dart +++ b/packages/flutter_tools/lib/src/commands/generate_localizations.dart @@ -5,6 +5,7 @@ import 'package:process/process.dart'; import '../artifacts.dart'; +import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../localizations/gen_l10n.dart'; @@ -199,6 +200,13 @@ class GenerateLocalizationsCommand extends FlutterCommand { 'suppress-warnings', help: 'When specified, all warnings will be suppressed.\n' ); + argParser.addFlag( + 'relax-syntax', + help: 'When specified, the syntax will be relaxed so that the special character ' + '"{" is treated as a string if it is not followed by a valid placeholder ' + 'and "}" is treated as a string if it does not close any previous "{" ' + 'that is treated as a special character.', + ); } final FileSystem _fileSystem; @@ -217,6 +225,10 @@ class GenerateLocalizationsCommand extends FlutterCommand { @override Future<FlutterCommandResult> runCommand() async { + // Validate the rest of the args. + if (argResults!.rest.isNotEmpty) { + throwToolExit('Unexpected positional argument "${argResults!.rest.first}".'); + } // Keep in mind that this is also defined in the following locations: // 1. flutter_tools/lib/src/build_system/targets/localizations.dart // 2. flutter_tools/test/general.shard/build_system/targets/localizations_test.dart diff --git a/packages/flutter_tools/lib/src/commands/ios_analyze.dart b/packages/flutter_tools/lib/src/commands/ios_analyze.dart new file mode 100644 index 0000000000000..4b5a1b0956ccb --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/ios_analyze.dart @@ -0,0 +1,68 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../base/logger.dart'; +import '../convert.dart'; +import '../ios/xcodeproj.dart'; +import '../project.dart'; + +/// The type of analysis to perform. +enum IOSAnalyzeOption { + /// Prints out available build variants of the iOS Xcode sub-project. + /// + /// An example output: + /// + /// {"configurations":["Debug","Release","Profile"],"targets":["Runner","RunnerTests"]} + listBuildOptions, + + /// Outputs universal link settings of the iOS Xcode sub-project into a file. + /// + /// The file path will be printed after the command is run successfully. + outputUniversalLinkSettings, +} + +/// Analyze the iOS Xcode sub-project of a Flutter project. +/// +/// The [userPath] must be point to a flutter project. +class IOSAnalyze { + IOSAnalyze({ + required this.project, + required this.option, + this.configuration, + this.target, + required this.logger, + }) : assert(option == IOSAnalyzeOption.listBuildOptions || + (configuration != null && target != null)); + + final FlutterProject project; + final IOSAnalyzeOption option; + final String? configuration; + final String? target; + final Logger logger; + + Future<void> analyze() async { + switch (option) { + case IOSAnalyzeOption.listBuildOptions: + final XcodeProjectInfo? info = await project.ios.projectInfo(); + final Map<String, List<String>> result; + if (info == null) { + result = const <String, List<String>>{}; + } else { + result = <String, List<String>>{ + 'configurations': info.buildConfigurations, + 'targets': info.targets, + }; + } + logger.printStatus(jsonEncode(result)); + case IOSAnalyzeOption.outputUniversalLinkSettings: + final String filePath = await project.ios.outputsUniversalLinkSettings( + configuration: configuration!, + target: target!, + ); + logger.printStatus('result saved in $filePath'); + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index 096bd72cd48bc..824077cd8c9f2 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -6,8 +6,10 @@ import 'package:args/args.dart'; import '../base/common.dart'; import '../base/os.dart'; +import '../base/utils.dart'; import '../build_info.dart'; import '../build_system/build_system.dart'; +import '../build_system/targets/localizations.dart'; import '../cache.dart'; import '../dart/generate_synthetic_packages.dart'; import '../dart/pub.dart'; @@ -304,6 +306,32 @@ class PackagesGetCommand extends FlutterCommand { environment: environment, buildSystem: globals.buildSystem, ); + } else if (rootProject.directory.childFile('l10n.yaml').existsSync()) { + final Environment environment = Environment( + artifacts: globals.artifacts!, + logger: globals.logger, + cacheDir: globals.cache.getRoot(), + engineVersion: globals.flutterVersion.engineRevision, + fileSystem: globals.fs, + flutterRootDir: globals.fs.directory(Cache.flutterRoot), + outputDir: globals.fs.directory(getBuildDirectory()), + processManager: globals.processManager, + platform: globals.platform, + usage: globals.flutterUsage, + projectDir: rootProject.directory, + generateDartPluginRegistry: true, + ); + final BuildResult result = await globals.buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + ); + if (result.hasException) { + throwToolExit( + 'Generating synthetic localizations package failed with ${result.exceptions.length} ${pluralize('error', result.exceptions.length)}:' + '\n\n' + '${result.exceptions.values.map<Object?>((ExceptionMeasurement e) => e.exception).join('\n\n')}', + ); + } } } final String? relativeTarget = target == null ? null : globals.fs.path.relative(target); diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 61829b16ebbe2..bb6538fcf379f 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -145,16 +145,20 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ) ..addFlag('enable-software-rendering', negatable: false, - help: 'Enable rendering using the Skia software backend. ' + help: '(deprecated) Enable rendering using the Skia software backend. ' 'This is useful when testing Flutter on emulators. By default, ' 'Flutter will attempt to either use OpenGL or Vulkan and fall back ' - 'to software when neither is available.', + 'to software when neither is available. This option is not supported ' + 'when using the Impeller rendering engine.', + hide: !verboseHelp, ) ..addFlag('skia-deterministic-rendering', negatable: false, - help: 'When combined with "--enable-software-rendering", this should provide completely ' + help: '(deprecated) When combined with "--enable-software-rendering", this should provide completely ' 'deterministic (i.e. reproducible) Skia rendering. This is useful for testing purposes ' - '(e.g. when comparing screenshots).', + '(e.g. when comparing screenshots). This option is not supported ' + 'when using the Impeller rendering engine.', + hide: !verboseHelp, ) ..addMultiOption('dart-entrypoint-args', abbr: 'a', @@ -311,6 +315,7 @@ class RunCommand extends RunCommandBase { requiresPubspecYaml(); usesFilesystemOptions(hide: !verboseHelp); usesExtraDartFlagOptions(verboseHelp: verboseHelp); + usesFrontendServerStarterPathOption(verboseHelp: verboseHelp); addEnableExperimentation(hide: !verboseHelp); usesInitializeFromDillOption(hide: !verboseHelp); @@ -682,19 +687,6 @@ class RunCommand extends RunCommandBase { throwToolExit('Hot reload is not supported by ${device.name}. Run with "--no-hot".'); } } - if (await device.isLocalEmulator && await device.supportsHardwareRendering) { - if (boolArg('enable-software-rendering')) { - globals.printStatus( - 'Using software rendering with device ${device.name}. You may get better performance ' - 'with hardware mode by configuring hardware rendering for your device.' - ); - } else { - globals.printStatus( - 'Using hardware rendering with device ${device.name}. If you notice graphics artifacts, ' - 'consider enabling software rendering with "--enable-software-rendering".' - ); - } - } } List<String>? expFlags; diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart index 21b531ecf597b..db3686e49f6b1 100644 --- a/packages/flutter_tools/lib/src/commands/screenshot.dart +++ b/packages/flutter_tools/lib/src/commands/screenshot.dart @@ -18,7 +18,6 @@ const String _kType = 'type'; const String _kVmServiceUrl = 'vm-service-url'; const String _kDeviceType = 'device'; const String _kSkiaType = 'skia'; -const String _kRasterizerType = 'rasterizer'; class ScreenshotCommand extends FlutterCommand { ScreenshotCommand({required this.fs}) { @@ -33,7 +32,7 @@ class ScreenshotCommand extends FlutterCommand { aliases: <String>[ 'observatory-url' ], // for historical reasons valueHelp: 'URI', help: 'The VM Service URL to which to connect.\n' - 'This is required when "--$_kType" is "$_kSkiaType" or "$_kRasterizerType".\n' + 'This is required when "--$_kType" is "$_kSkiaType".\n' 'To find the VM service URL, use "flutter run" and look for ' '"A Dart VM Service ... is available at" in the output.', ); @@ -41,13 +40,12 @@ class ScreenshotCommand extends FlutterCommand { _kType, valueHelp: 'type', help: 'The type of screenshot to retrieve.', - allowed: const <String>[_kDeviceType, _kSkiaType, _kRasterizerType], + allowed: const <String>[_kDeviceType, _kSkiaType], allowedHelp: const <String, String>{ _kDeviceType: "Delegate to the device's native screenshot capabilities. This " 'screenshots the entire screen currently being displayed (including content ' 'not rendered by Flutter, like the device status bar).', _kSkiaType: 'Render the Flutter app as a Skia picture. Requires "--$_kVmServiceUrl".', - _kRasterizerType: 'Render the Flutter app using the rasterizer. Requires "--$_kVmServiceUrl."', }, defaultsTo: _kDeviceType, ); @@ -116,8 +114,6 @@ class ScreenshotCommand extends FlutterCommand { await runScreenshot(outputFile); case _kSkiaType: success = await runSkia(outputFile); - case _kRasterizerType: - success = await runRasterizer(outputFile); } return success ? FlutterCommandResult.success() @@ -173,30 +169,6 @@ class ScreenshotCommand extends FlutterCommand { return true; } - Future<bool> runRasterizer(File? outputFile) async { - final Uri vmServiceUrl = Uri.parse(stringArg(_kVmServiceUrl)!); - final FlutterVmService vmService = await connectToVmService(vmServiceUrl, logger: globals.logger); - final vm_service.Response? response = await vmService.screenshot(); - if (response == null) { - globals.printError( - 'The screenshot request failed, probably because the device was ' - 'disconnected', - ); - return false; - } - outputFile ??= globals.fsUtils.getUniqueFile( - fs.currentDirectory, - 'flutter', - 'png', - ); - final IOSink sink = outputFile.openWrite(); - sink.add(base64.decode(response.json?['screenshot'] as String)); - await sink.close(); - _showOutputFileInfo(outputFile); - ensureOutputIsNotJsonRpcError(outputFile); - return true; - } - static void checkOutput(File outputFile, FileSystem fs) { if (!fs.file(outputFile.path).existsSync()) { throwToolExit( diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 8bd076edcd5d1..a7279c5916d19 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:meta/meta.dart'; +import 'package:package_config/package_config_types.dart'; import '../asset.dart'; import '../base/common.dart'; @@ -66,6 +67,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { requiresPubspecYaml(); usesPubOption(); addNullSafetyModeOptions(hide: !verboseHelp); + usesFrontendServerStarterPathOption(verboseHelp: verboseHelp); usesTrackWidgetCreation(verboseHelp: verboseHelp); addEnableExperimentation(hide: !verboseHelp); usesDartDefineOption(); @@ -131,6 +133,13 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { defaultsTo: 'coverage/lcov.info', help: 'Where to store coverage information (if coverage is enabled).', ) + ..addMultiOption('coverage-package', + help: 'A regular expression matching packages names ' + 'to include in the coverage report (if coverage is enabled). ' + 'If unset, matches the current package name.', + valueHelp: 'package-name-regexp', + splitCommas: false, + ) ..addFlag('machine', hide: !verboseHelp, negatable: false, @@ -395,10 +404,14 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { CoverageCollector? collector; if (boolArg('coverage') || boolArg('merge-coverage') || boolArg('branch-coverage')) { - final String projectName = flutterProject.manifest.appName; + final Set<String> packagesToInclude = _getCoveragePackages( + stringsArg('coverage-package'), + flutterProject, + buildInfo.packageConfig, + ); collector = CoverageCollector( verbose: !machine, - libraryNames: <String>{projectName}, + libraryNames: packagesToInclude, packagesPath: buildInfo.packagesPath, resolver: await CoverageCollector.getResolver(buildInfo.packagesPath), testTimeRecorder: testTimeRecorder, @@ -508,6 +521,30 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { return FlutterCommandResult.success(); } + Set<String> _getCoveragePackages( + List<String> packagesRegExps, + FlutterProject flutterProject, + PackageConfig packageConfig, + ) { + final String projectName = flutterProject.manifest.appName; + final Set<String> packagesToInclude = <String>{ + if (packagesRegExps.isEmpty) projectName, + }; + try { + for (final String regExpStr in packagesRegExps) { + final RegExp regExp = RegExp(regExpStr); + packagesToInclude.addAll( + packageConfig.packages + .map((Package e) => e.name) + .where((String e) => regExp.hasMatch(e)), + ); + } + } on FormatException catch (e) { + throwToolExit('Regular expression syntax is invalid. $e'); + } + return packagesToInclude; + } + /// Parses a test file/directory target passed as an argument and returns it /// as an absolute file:/// [URI] with optional querystring for name/line/col. Uri _parseTestArgument(String arg) { diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart index 5d240051fba88..bf3bc59a90d72 100644 --- a/packages/flutter_tools/lib/src/commands/update_packages.dart +++ b/packages/flutter_tools/lib/src/commands/update_packages.dart @@ -34,10 +34,12 @@ const Map<String, String> kManuallyPinnedDependencies = <String, String>{ 'video_player': '2.2.11', // Keep pinned to latest until 1.0.0. 'material_color_utilities': '0.5.0', - // https://github.com/flutter/flutter/issues/111304 - 'url_launcher_android': '6.0.17', // https://github.com/flutter/flutter/issues/115660 'archive': '3.3.2', + // https://github.com/flutter/flutter/issues/135716 + 'leak_tracker': '9.0.7', + // https://github.com/flutter/flutter/issues/135716 + 'leak_tracker_flutter_testing': '1.0.5', }; class UpdatePackagesCommand extends FlutterCommand { @@ -1753,7 +1755,7 @@ Directory createTemporaryFlutterSdk( // Fill in SDK dependency constraint. output.write(''' environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' '''); output.writeln('dependencies:'); @@ -1785,7 +1787,7 @@ description: Dart SDK extensions for dart:ui homepage: http://flutter.io # sky_engine requires sdk_ext support in the analyzer which was added in 1.11.x environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' '''); return directory; diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 6774080a54586..e7571a69eac32 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -227,6 +227,7 @@ class KernelCompiler { TargetModel targetModel = TargetModel.flutter, bool linkPlatformKernelIn = false, bool aot = false, + String? frontendServerStarterPath, List<String>? extraFrontEndOptions, List<String>? fileSystemRoots, String? fileSystemScheme, @@ -240,12 +241,15 @@ class KernelCompiler { required bool trackWidgetCreation, required List<String> dartDefines, required PackageConfig packageConfig, + String? nativeAssets, }) async { final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null; - final String frontendServer = _artifacts.getArtifactPath( - Artifact.frontendServerSnapshotForEngineDartSdk, - platform: platform, - ); + final String frontendServer = (frontendServerStarterPath == null || frontendServerStarterPath.isEmpty) + ? _artifacts.getArtifactPath( + Artifact.frontendServerSnapshotForEngineDartSdk, + platform: platform, + ) + : frontendServerStarterPath; // This is a URI, not a file path, so the forward slash is correct even on Windows. if (!sdkRoot.endsWith('/')) { sdkRoot = '$sdkRoot/'; @@ -337,6 +341,10 @@ class KernelCompiler { 'package:flutter/src/dart_plugin_registrant.dart', '-Dflutter.dart_plugin_registrant=$dartPluginRegistrantUri', ], + if (nativeAssets != null) ...<String>[ + '--native-assets', + nativeAssets, + ], // See: https://github.com/flutter/flutter/issues/103994 '--verbosity=error', ...?extraFrontEndOptions, @@ -381,9 +389,10 @@ class _RecompileRequest extends _CompilationRequest { this.invalidatedFiles, this.outputPath, this.packageConfig, - this.suppressErrors, - {this.additionalSourceUri} - ); + this.suppressErrors, { + this.additionalSourceUri, + this.nativeAssetsYamlUri, + }); Uri mainUri; List<Uri>? invalidatedFiles; @@ -391,6 +400,7 @@ class _RecompileRequest extends _CompilationRequest { PackageConfig packageConfig; bool suppressErrors; final Uri? additionalSourceUri; + final Uri? nativeAssetsYamlUri; @override Future<CompilerOutput?> _run(DefaultResidentCompiler compiler) async => @@ -483,6 +493,7 @@ abstract class ResidentCompiler { bool assumeInitializeFromDillUpToDate, TargetModel targetModel, bool unsafePackageSerialization, + String? frontendServerStarterPath, List<String> extraFrontEndOptions, String platformDill, List<String>? dartDefines, @@ -515,6 +526,7 @@ abstract class ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }); Future<CompilerOutput?> compileExpression( @@ -585,7 +597,7 @@ class DefaultResidentCompiler implements ResidentCompiler { required this.buildMode, required Logger logger, required ProcessManager processManager, - required Artifacts artifacts, + required this.artifacts, required Platform platform, required FileSystem fileSystem, this.testCompilation = false, @@ -597,6 +609,7 @@ class DefaultResidentCompiler implements ResidentCompiler { this.assumeInitializeFromDillUpToDate = false, this.targetModel = TargetModel.flutter, this.unsafePackageSerialization = false, + this.frontendServerStarterPath, this.extraFrontEndOptions, this.platformDill, List<String>? dartDefines, @@ -604,7 +617,6 @@ class DefaultResidentCompiler implements ResidentCompiler { @visibleForTesting StdoutHandler? stdoutHandler, }) : _logger = logger, _processManager = processManager, - _artifacts = artifacts, _stdoutHandler = stdoutHandler ?? StdoutHandler(logger: logger, fileSystem: fileSystem), _platform = platform, dartDefines = dartDefines ?? const <String>[], @@ -615,7 +627,7 @@ class DefaultResidentCompiler implements ResidentCompiler { final Logger _logger; final ProcessManager _processManager; - final Artifacts _artifacts; + final Artifacts artifacts; final Platform _platform; final bool testCompilation; @@ -628,6 +640,7 @@ class DefaultResidentCompiler implements ResidentCompiler { final String? initializeFromDill; final bool assumeInitializeFromDillUpToDate; final bool unsafePackageSerialization; + final String? frontendServerStarterPath; final List<String>? extraFrontEndOptions; final List<String> dartDefines; final String? librariesSpec; @@ -664,6 +677,7 @@ class DefaultResidentCompiler implements ResidentCompiler { File? dartPluginRegistrant, String? projectRootPath, FileSystem? fs, + Uri? nativeAssetsYaml, }) async { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); @@ -682,6 +696,7 @@ class DefaultResidentCompiler implements ResidentCompiler { packageConfig, suppressErrors, additionalSourceUri: additionalSourceUri, + nativeAssetsYamlUri: nativeAssetsYaml, )); return completer.future; } @@ -700,12 +715,22 @@ class DefaultResidentCompiler implements ResidentCompiler { toMultiRootPath(request.additionalSourceUri!, fileSystemScheme, fileSystemRoots, _platform.isWindows); } + final String? nativeAssets = request.nativeAssetsYamlUri?.toString(); final Process? server = _server; if (server == null) { - return _compile(mainUri, request.outputPath, additionalSourceUri: additionalSourceUri); + return _compile( + mainUri, + request.outputPath, + additionalSourceUri: additionalSourceUri, + nativeAssetsUri: nativeAssets, + ); } final String inputKey = Uuid().generateV4(); + if (nativeAssets != null && nativeAssets.isNotEmpty) { + server.stdin.writeln('native-assets $nativeAssets'); + _logger.printTrace('<- native-assets $nativeAssets'); + } server.stdin.writeln('recompile $mainUri $inputKey'); _logger.printTrace('<- recompile $mainUri $inputKey'); final List<Uri>? invalidatedFiles = request.invalidatedFiles; @@ -747,16 +772,19 @@ class DefaultResidentCompiler implements ResidentCompiler { Future<CompilerOutput?> _compile( String scriptUri, - String? outputPath, - {String? additionalSourceUri} - ) async { + String? outputPath, { + String? additionalSourceUri, + String? nativeAssetsUri, + }) async { final TargetPlatform? platform = (targetModel == TargetModel.dartdevc) ? TargetPlatform.web_javascript : null; - final String frontendServer = _artifacts.getArtifactPath( - Artifact.frontendServerSnapshotForEngineDartSdk, - platform: platform, - ); + final String frontendServer = (frontendServerStarterPath == null || frontendServerStarterPath!.isEmpty) + ? artifacts.getArtifactPath( + Artifact.frontendServerSnapshotForEngineDartSdk, + platform: platform, + ) + : frontendServerStarterPath!; final List<String> command = <String>[ - _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform), + artifacts.getArtifactPath(Artifact.engineDartBinary, platform: platform), '--disable-dart-dev', frontendServer, '--sdk-root', @@ -807,6 +835,10 @@ class DefaultResidentCompiler implements ResidentCompiler { 'package:flutter/src/dart_plugin_registrant.dart', '-Dflutter.dart_plugin_registrant=$additionalSourceUri', ], + if (nativeAssetsUri != null) ...<String>[ + '--native-assets', + nativeAssetsUri, + ], if (platformDill != null) ...<String>[ '--platform', platformDill!, @@ -843,6 +875,11 @@ class DefaultResidentCompiler implements ResidentCompiler { } })); + if (nativeAssetsUri != null && nativeAssetsUri.isNotEmpty) { + _server?.stdin.writeln('native-assets $nativeAssetsUri'); + _logger.printTrace('<- native-assets $nativeAssetsUri'); + } + _server?.stdin.writeln('compile $scriptUri'); _logger.printTrace('<- compile $scriptUri'); diff --git a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart new file mode 100644 index 0000000000000..e55c235ea45ce --- /dev/null +++ b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart @@ -0,0 +1,187 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +typedef _OutputSender = void Function(String category, String message, {bool? parseStackFrames, int? variablesReference}); + +/// A formatter for improving the display of Flutter structured errors over DAP. +/// +/// The formatter deserializes a `Flutter.Error` event and produces output +/// similar to the `renderedErrorText` field, but may include ansi color codes +/// to provide improved formatting (such as making stack frames from non-user +/// code faint) if the client indicated support. +/// +/// Lines that look like stack frames will be marked so they can be parsed by +/// the base adapter and attached as [Source]s to allow them to be clickable +/// in the client. +class FlutterErrorFormatter { + final List<_BatchedOutput> batchedOutput = <_BatchedOutput>[]; + + /// Formats a Flutter error. + /// + /// If this is not the first error since the reload, only a summary will be + /// included. + void formatError(Map<String, Object?> errorData) { + final _ErrorData data = _ErrorData(errorData); + + const int assumedTerminalSize = 80; + const String barChar = '═'; + final String headerPrefix = barChar * 8; + final String headerSuffix = barChar * math.max( assumedTerminalSize - (data.description?.length ?? 0) - 2 - headerPrefix.length, 0); + final String header = '$headerPrefix ${data.description} $headerSuffix'; + _write(''); + _write(header, isError: true); + + if (data.errorsSinceReload == 0) { + data.properties.forEach(_writeNode); + data.children.forEach(_writeNode); + } else { + data.properties.forEach(_writeSummary); + } + + _write(barChar * header.length, isError: true); + } + + /// Sends all collected output through [sendOutput]. + void sendOutput(_OutputSender sendOutput) { + for (final _BatchedOutput output in batchedOutput) { + sendOutput( + output.isError ? 'stderr' : 'stdout', + output.output, + parseStackFrames: output.parseStackFrames, + ); + } + } + + /// Writes [text] to the output. + /// + /// If the last item in the batch has the same settings as this item, it will + /// be appended to the same item, otherwise a new item will be added to the + /// batch. + void _write( + String? text, { + int indent = 0, + bool isError = false, + bool parseStackFrames = false, + }) { + if (text != null) { + final String indentString = ' ' * indent; + final String message = '$indentString${text.trim()}'; + + _BatchedOutput? output = batchedOutput.lastOrNull; + if (output == null || output.isError != isError || output.parseStackFrames != parseStackFrames) { + batchedOutput.add(output = _BatchedOutput(isError, parseStackFrames: parseStackFrames)); + } + output.writeln(message); + } + } + + /// Writes [node] to the output using [indent], recursing unless [recursive] + /// is `false`. + void _writeNode(_ErrorNode node, {int indent = 0, bool recursive = true}) { + // Errors, summaries and lines starting "Exception:" are marked as errors so + // they go to stderr instead of stdout (this may cause the client to colour + // them like errors). + final bool showAsError = node.level == _DiagnosticsNodeLevel.error || + node.level == _DiagnosticsNodeLevel.summary || + (node.description?.startsWith('Exception: ') ?? false); + + if (node.showName && node.name != null) { + _write('${node.name}: ${node.description}', indent: indent, isError: showAsError); + } else if (node.description?.startsWith('#') ?? false) { + // Possible stack frame. + _write(node.description, indent: indent, isError: showAsError, parseStackFrames: true); + } else { + _write(node.description, indent: indent, isError: showAsError); + } + + if (recursive) { + if (node.style != _DiagnosticsNodeStyle.flat) { + indent++; + } + _writeNodes(node.properties, indent: indent); + _writeNodes(node.children, indent: indent); + } + } + + /// Writes [nodes] to the output. + void _writeNodes(List<_ErrorNode> nodes, {int indent = 0, bool recursive = true}) { + for (final _ErrorNode child in nodes) { + _writeNode(child, indent: indent, recursive: recursive); + } + } + + /// Writes a simple summary of [node] to the output. + void _writeSummary(_ErrorNode node) { + final bool allChildrenAreLeaf = node.children.isNotEmpty && + !node.children.any((_ErrorNode child) => child.children.isNotEmpty); + if (node.level == _DiagnosticsNodeLevel.summary || allChildrenAreLeaf) { + _writeNode(node, recursive: false); + } + } +} + +/// A container for output to be sent to the client. +/// +/// When multiple lines are being sent, they may be written to the same batch +/// if the output options (error/stackFrame) are the same. +class _BatchedOutput { + _BatchedOutput(this.isError, {this.parseStackFrames = false}); + + final bool isError; + final bool parseStackFrames; + final StringBuffer _buffer = StringBuffer(); + + String get output => _buffer.toString(); + + void writeln(String output) => _buffer.writeln(output); +} + +enum _DiagnosticsNodeLevel { + error, + summary, +} + +enum _DiagnosticsNodeStyle { + flat, +} + +class _ErrorData extends _ErrorNode { + _ErrorData(super.data); + + int get errorsSinceReload => data['errorsSinceReload'] as int? ?? 0; + String get renderedErrorText => data['renderedErrorText'] as String? ?? ''; +} + +class _ErrorNode { + _ErrorNode(this.data); + + final Map<Object, Object?> data; + + List<_ErrorNode> get children => asList('children', _ErrorNode.new); + String? get description => asString('description'); + _DiagnosticsNodeLevel? get level => asEnum('level', _DiagnosticsNodeLevel.values); + String? get name => asString('name'); + List<_ErrorNode> get properties => asList('properties', _ErrorNode.new); + bool get showName => data['showName'] != false; + _DiagnosticsNodeStyle? get style => asEnum('style', _DiagnosticsNodeStyle.values); + + String? asString(String field) { + final Object? value = data[field]; + return value is String ? value : null; + } + + T? asEnum<T extends Enum>(String field, Iterable<T> enumValues) { + final String? value = asString(field); + return value != null ? enumValues.asNameMap()[value] : null; + } + + List<T> asList<T>(String field, T Function(Map<Object, Object?>) constructor) { + final Object? objects = data[field]; + return objects is List && objects.every((Object? element) => element is Map<String, Object?>) + ? objects.cast<Map<Object, Object?>>().map(constructor).toList() + : <T>[]; + } +} diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart index e0e3402bb4466..891ba224a4d6d 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart @@ -12,6 +12,7 @@ import '../base/io.dart'; import '../cache.dart'; import '../convert.dart'; import '../globals.dart' as globals show fs; +import 'error_formatter.dart'; import 'flutter_adapter_args.dart'; import 'flutter_base_adapter.dart'; @@ -219,17 +220,14 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter with VmServiceInfoFile /// Sends OutputEvents to the client for a Flutter.Error event. void _handleFlutterErrorEvent(vm.ExtensionData? data) { - final Map<String, dynamic>? errorData = data?.data; + final Map<String, Object?>? errorData = data?.data; if (errorData == null) { return; } - final String errorText = (errorData['renderedErrorText'] as String?) - ?? (errorData['description'] as String?) - // We should never not error text, but if we do at least send something - // so it's not just completely silent. - ?? 'Unknown error in Flutter.Error event'; - sendOutput('stderr', '$errorText\n'); + FlutterErrorFormatter() + ..formatError(errorData) + ..sendOutput(sendOutput); } /// Called by [launchRequest] to request that we actually start the app to be run/debugged. diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart index 8e90a6e9b6147..5943a27d356e5 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart @@ -24,6 +24,7 @@ class FlutterAttachRequestArguments super.cwd, super.env, super.additionalProjectPaths, + super.allowAnsiColorOutput, super.debugSdkLibraries, super.debugExternalPackageLibraries, super.evaluateGettersInDebugViews, @@ -109,6 +110,7 @@ class FlutterLaunchRequestArguments super.cwd, super.env, super.additionalProjectPaths, + super.allowAnsiColorOutput, super.debugSdkLibraries, super.debugExternalPackageLibraries, super.evaluateGettersInDebugViews, diff --git a/packages/flutter_tools/lib/src/desktop_device.dart b/packages/flutter_tools/lib/src/desktop_device.dart index c8937258ddde4..83271771d758a 100644 --- a/packages/flutter_tools/lib/src/desktop_device.dart +++ b/packages/flutter_tools/lib/src/desktop_device.dart @@ -209,8 +209,6 @@ abstract class DesktopDevice extends Device { /// steps to be run. void onAttached(ApplicationPackage package, BuildInfo buildInfo, Process process) {} - bool get supportsImpeller => false; - /// Computes a set of environment variables used to pass debugging information /// to the engine without interfering with application level command line /// arguments. @@ -268,14 +266,12 @@ abstract class DesktopDevice extends Device { if (debuggingOptions.purgePersistentCache) { addFlag('purge-persistent-cache=true'); } - if (supportsImpeller) { - switch (debuggingOptions.enableImpeller) { - case ImpellerStatus.enabled: - addFlag('enable-impeller=true'); - case ImpellerStatus.disabled: - case ImpellerStatus.platformDefault: - addFlag('enable-impeller=false'); - } + switch (debuggingOptions.enableImpeller) { + case ImpellerStatus.enabled: + addFlag('enable-impeller=true'); + case ImpellerStatus.disabled: + case ImpellerStatus.platformDefault: + addFlag('enable-impeller=false'); } // Options only supported when there is a VM Service connection between the // tool and the device, usually in debug or profile mode. diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 4136071ee2723..93e0f3a363ade 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -401,7 +401,6 @@ class UpdateFSReport { bool success = false, int invalidatedSourcesCount = 0, int syncedBytes = 0, - this.fastReassembleClassName, int scannedSourcesCount = 0, Duration compileDuration = Duration.zero, Duration transferDuration = Duration.zero, @@ -423,7 +422,6 @@ class UpdateFSReport { Duration get findInvalidatedDuration => _findInvalidatedDuration; bool _success; - String? fastReassembleClassName; int _invalidatedSourcesCount; int _syncedBytes; int _scannedSourcesCount; @@ -435,7 +433,6 @@ class UpdateFSReport { if (!report._success) { _success = false; } - fastReassembleClassName ??= report.fastReassembleClassName; _invalidatedSourcesCount += report._invalidatedSourcesCount; _syncedBytes += report._syncedBytes; _scannedSourcesCount += report._scannedSourcesCount; @@ -495,7 +492,6 @@ class DevFS { DateTime? lastCompiled; DateTime? _previousCompiled; PackageConfig? lastPackageConfig; - File? _widgetCacheOutputFile; Uri? _baseUri; Uri? get baseUri => _baseUri; @@ -555,22 +551,6 @@ class DevFS { lastCompiled = _previousCompiled; } - - /// If the build method of a single widget was modified, return the widget name. - /// - /// If any other changes were made, or there is an error scanning the file, - /// return `null`. - String? _checkIfSingleWidgetReloadApplied() { - final File? widgetCacheOutputFile = _widgetCacheOutputFile; - if (widgetCacheOutputFile != null && widgetCacheOutputFile.existsSync()) { - final String widget = widgetCacheOutputFile.readAsStringSync().trim(); - if (widget.isNotEmpty) { - return widget; - } - } - return null; - } - /// Updates files on the device. /// /// Returns the number of bytes synced. @@ -596,7 +576,6 @@ class DevFS { final DateTime candidateCompileTime = DateTime.now(); didUpdateFontManifest = false; lastPackageConfig = packageConfig; - _widgetCacheOutputFile = _fileSystem.file('$dillOutputPath.incremental.dill.widget_cache'); // Update modified files final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{}; @@ -741,7 +720,6 @@ class DevFS { success: true, syncedBytes: syncedBytes, invalidatedSourcesCount: invalidatedFiles.length, - fastReassembleClassName: _checkIfSingleWidgetReloadApplied(), compileDuration: compileTimer.elapsed, transferDuration: transferTimer.elapsed, ); diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 091bac0f48d0f..aecf35a70b38e 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -8,7 +8,6 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; import 'application_package.dart'; -import 'artifacts.dart'; import 'base/context.dart'; import 'base/dds.dart'; import 'base/file_system.dart'; @@ -394,7 +393,7 @@ class DeviceDiscoverySupportFilter { if (_flutterProject == null) { return true; } - return device.isSupportedForProject(_flutterProject!); + return device.isSupportedForProject(_flutterProject); } } @@ -504,12 +503,13 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { if (_timer == null) { deviceNotifier ??= ItemListNotifier<Device>(); // Make initial population the default, fast polling timeout. - _timer = _initTimer(null); + _timer = _initTimer(null, initialCall: true); } } - Timer _initTimer(Duration? pollingTimeout) { - return Timer(_pollingInterval, () async { + Timer _initTimer(Duration? pollingTimeout, {bool initialCall = false}) { + // Poll for devices immediately on the initial call for faster initial population. + return Timer(initialCall ? Duration.zero : _pollingInterval, () async { try { final List<Device> devices = await pollingGetDevices(timeout: pollingTimeout); deviceNotifier!.updateWithNewList(devices); @@ -740,9 +740,6 @@ abstract class Device { /// Clear the device's logs. void clearLogs(); - /// Optional device-specific artifact overrides. - OverrideArtifacts? get artifactOverrides => null; - /// Start an app package on the current device. /// /// [platformArgs] allows callers to pass platform-specific arguments to the @@ -848,8 +845,10 @@ abstract class Device { ]; } - static Future<void> printDevices(List<Device> devices, Logger logger) async { - (await descriptions(devices)).forEach(logger.printStatus); + static Future<void> printDevices(List<Device> devices, Logger logger, { String prefix = '' }) async { + for (final String line in await descriptions(devices)) { + logger.printStatus('$prefix$line'); + } } static List<String> devicesPlatformTypes(List<Device> devices) { diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 255e938ef8caa..9e3e1b7f0060e 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -51,17 +51,20 @@ abstract class DoctorValidatorsProvider { // [FeatureFlags]. factory DoctorValidatorsProvider.test({ Platform? platform, + Logger? logger, required FeatureFlags featureFlags, }) { return _DefaultDoctorValidatorsProvider( featureFlags: featureFlags, platform: platform ?? FakePlatform(), + logger: logger ?? BufferLogger.test(), ); } /// The singleton instance, pulled from the [AppContext]. static DoctorValidatorsProvider get _instance => context.get<DoctorValidatorsProvider>()!; static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider( + logger: globals.logger, platform: globals.platform, featureFlags: featureFlags, ); @@ -74,12 +77,14 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { _DefaultDoctorValidatorsProvider({ required this.platform, required this.featureFlags, - }); + required Logger logger, + }) : _logger = logger; List<DoctorValidator>? _validators; List<Workflow>? _workflows; final Platform platform; final FeatureFlags featureFlags; + final Logger _logger; late final LinuxWorkflow linuxWorkflow = LinuxWorkflow( platform: platform, @@ -115,6 +120,7 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { userMessages: userMessages, plistParser: globals.plistParser, processManager: globals.processManager, + logger: _logger, ), ...VsCodeValidator.installedValidators(globals.fs, platform, globals.processManager), ]; diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 445716b8712f5..ffd4a990dcf3e 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -41,6 +41,9 @@ class WebDriverService extends DriverService { late ResidentRunner _residentRunner; Uri? _webUri; + @visibleForTesting + Uri? get webUri => _webUri; + /// The result of [ResidentRunner.run]. /// /// This is expected to stay `null` throughout the test, as the application @@ -74,10 +77,12 @@ class WebDriverService extends DriverService { DebuggingOptions.disabled( buildInfo, port: debuggingOptions.port, + hostname: debuggingOptions.hostname, ) : DebuggingOptions.enabled( buildInfo, port: debuggingOptions.port, + hostname: debuggingOptions.hostname, disablePortPublication: debuggingOptions.disablePortPublication, ), stayResident: true, @@ -116,11 +121,16 @@ class WebDriverService extends DriverService { throw ToolExit('Failed to start application'); } - _webUri = _residentRunner.uri; - - if (_webUri == null) { + if (_residentRunner.uri == null) { throw ToolExit('Unable to connect to the app. URL not available.'); } + + if (debuggingOptions.webLaunchUrl != null) { + // It should thow an error if the provided url is invalid so no tryParse + _webUri = Uri.parse(debuggingOptions.webLaunchUrl!); + } else { + _webUri = _residentRunner.uri; + } } @override diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 97be411a541fa..284416992133b 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -44,12 +44,15 @@ abstract class FeatureFlags { /// Whether custom devices are enabled. bool get areCustomDevicesEnabled => false; - /// Whether fast single widget reloads are enabled. - bool get isSingleWidgetReloadEnabled => false; - /// Whether WebAssembly compilation for Flutter Web is enabled. bool get isFlutterWebWasmEnabled => false; + /// Whether animations are used in the command line interface. + bool get isCliAnimationEnabled => true; + + /// Whether native assets compilation and bundling is enabled. + bool get isNativeAssetsEnabled => false; + /// Whether a particular feature is enabled for the current channel. /// /// Prefer using one of the specific getters above instead of this API. @@ -62,12 +65,13 @@ const List<Feature> allFeatures = <Feature>[ flutterLinuxDesktopFeature, flutterMacOSDesktopFeature, flutterWindowsDesktopFeature, - singleWidgetReload, flutterAndroidFeature, flutterIOSFeature, flutterFuchsiaFeature, flutterCustomDevicesFeature, flutterWebWasm, + cliAnimation, + nativeAssets, ]; /// All current Flutter feature flags that can be configured. @@ -126,7 +130,7 @@ const Feature flutterFuchsiaFeature = Feature( ); const Feature flutterCustomDevicesFeature = Feature( - name: 'Early support for custom device types', + name: 'early support for custom device types', configSetting: 'enable-custom-devices', environmentOverride: 'FLUTTER_CUSTOM_DEVICES', master: FeatureChannelSetting( @@ -140,27 +144,31 @@ const Feature flutterCustomDevicesFeature = Feature( ), ); -/// The fast hot reload feature for https://github.com/flutter/flutter/issues/61407. -const Feature singleWidgetReload = Feature( - name: 'Hot reload optimization for changes to class body of a single widget', - configSetting: 'single-widget-reload-optimization', - environmentOverride: 'FLUTTER_SINGLE_WIDGET_RELOAD', +/// Enabling WebAssembly compilation from `flutter build web` +const Feature flutterWebWasm = Feature( + name: 'WebAssembly compilation from flutter build web', + environmentOverride: 'FLUTTER_WEB_WASM', master: FeatureChannelSetting( available: true, enabledByDefault: true, ), - beta: FeatureChannelSetting( - available: true, - ), ); -/// Enabling WebAssembly compilation from `flutter build web` -const Feature flutterWebWasm = Feature( - name: 'WebAssembly compilation from flutter build web', - environmentOverride: 'FLUTTER_WEB_WASM', +/// The [Feature] for CLI animations. +/// +/// The TERM environment variable set to "dumb" turns this off. +const Feature cliAnimation = Feature.fullyEnabled( + name: 'animations in the command line interface', + configSetting: 'cli-animations', +); + +/// Enable native assets compilation and bundling. +const Feature nativeAssets = Feature( + name: 'native assets compilation and bundling', + configSetting: 'enable-native-assets', + environmentOverride: 'FLUTTER_NATIVE_ASSETS', master: FeatureChannelSetting( available: true, - enabledByDefault: true, ), ); @@ -247,9 +255,9 @@ class Feature { ]; // Add channel info for settings only on some channels. if (channels.length == 1) { - buffer.write('\nThis setting applies to only the ${channels.single} channel.'); + buffer.write('\nThis setting applies only to the ${channels.single} channel.'); } else if (channels.length == 2) { - buffer.write('\nThis setting applies to only the ${channels.join(' and ')} channels.'); + buffer.write('\nThis setting applies only to the ${channels.join(' and ')} channels.'); } if (extraHelpText != null) { buffer.write(' $extraHelpText'); diff --git a/packages/flutter_tools/lib/src/flutter_cache.dart b/packages/flutter_tools/lib/src/flutter_cache.dart index 252021cf78e89..dcba2c6da5443 100644 --- a/packages/flutter_tools/lib/src/flutter_cache.dart +++ b/packages/flutter_tools/lib/src/flutter_cache.dart @@ -836,7 +836,11 @@ class IosUsbArtifacts extends CachedArtifact { } @visibleForTesting - Uri get archiveUri => Uri.parse('${cache.storageBaseUrl}/flutter_infra_release/ios-usb-dependencies${cache.useUnsignedMacBinaries ? '/unsigned' : ''}/$name/$version/$name.zip'); + Uri get archiveUri => Uri.parse( + '${cache.realmlessStorageBaseUrl}/flutter_infra_release/' + 'ios-usb-dependencies${cache.useUnsignedMacBinaries ? '/unsigned' : ''}' + '/$name/$version/$name.zip', + ); } // TODO(zanderso): upload debug desktop artifacts to host-debug and diff --git a/packages/flutter_tools/lib/src/flutter_device_manager.dart b/packages/flutter_tools/lib/src/flutter_device_manager.dart index 47411190ca349..dc98b4a7d0dcb 100644 --- a/packages/flutter_tools/lib/src/flutter_device_manager.dart +++ b/packages/flutter_tools/lib/src/flutter_device_manager.dart @@ -87,7 +87,6 @@ class FlutterDeviceManager extends DeviceManager { processManager: processManager, logger: logger, artifacts: artifacts, - operatingSystemUtils: operatingSystemUtils, ), MacOSDevices( processManager: processManager, diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index 672a81855d4bc..1418e90009631 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -45,10 +45,18 @@ class FlutterFeatureFlags implements FeatureFlags { bool get areCustomDevicesEnabled => isEnabled(flutterCustomDevicesFeature); @override - bool get isSingleWidgetReloadEnabled => isEnabled(singleWidgetReload); + bool get isFlutterWebWasmEnabled => isEnabled(flutterWebWasm); @override - bool get isFlutterWebWasmEnabled => isEnabled(flutterWebWasm); + bool get isCliAnimationEnabled { + if (_platform.environment['TERM'] == 'dumb') { + return false; + } + return isEnabled(cliAnimation); + } + + @override + bool get isNativeAssetsEnabled => isEnabled(nativeAssets); @override bool isEnabled(Feature feature) { diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index 27fa5d611a2c8..fd38dd79be11c 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -26,14 +26,20 @@ import 'platform_plugins.dart'; import 'plugins.dart'; import 'project.dart'; -void _renderTemplateToFile(String template, Object? context, File file, TemplateRenderer templateRenderer) { +Future<void> _renderTemplateToFile( + String template, + Object? context, + File file, + TemplateRenderer templateRenderer, +) async { final String renderedTemplate = templateRenderer .renderString(template, context); - file.createSync(recursive: true); - file.writeAsStringSync(renderedTemplate); + await file.create(recursive: true); + await file.writeAsString(renderedTemplate); } -Plugin? _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies, {FileSystem? fileSystem}) { +Future<Plugin?> _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies, + {FileSystem? fileSystem}) async { final FileSystem fs = fileSystem ?? globals.fs; final File pubspecFile = fs.file(packageRoot.resolve('pubspec.yaml')); if (!pubspecFile.existsSync()) { @@ -42,7 +48,7 @@ Plugin? _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependen Object? pubspec; try { - pubspec = loadYaml(pubspecFile.readAsStringSync()); + pubspec = loadYaml(await pubspecFile.readAsString()); } on YamlException catch (err) { globals.printTrace('Failed to parse plugin manifest for $name: $err'); // Do nothing, potentially not a plugin. @@ -85,7 +91,7 @@ Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = t ); for (final Package package in packageConfig.packages) { final Uri packageRoot = package.packageUriRoot.resolve('..'); - final Plugin? plugin = _pluginFromPackage( + final Plugin? plugin = await _pluginFromPackage( package.name, packageRoot, project.manifest.dependencies, @@ -445,7 +451,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> templateContent = _androidPluginRegistryTemplateOldEmbedding; } globals.printTrace('Generating $registryPath'); - _renderTemplateToFile( + await _renderTemplateToFile( templateContent, templateContext, globals.fs.file(registryPath), @@ -774,20 +780,20 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug }; if (project.isModule) { final Directory registryDirectory = project.ios.pluginRegistrantHost; - _renderTemplateToFile( + await _renderTemplateToFile( _pluginRegistrantPodspecTemplate, context, registryDirectory.childFile('FlutterPluginRegistrant.podspec'), globals.templateRenderer, ); } - _renderTemplateToFile( + await _renderTemplateToFile( _objcPluginRegistryHeaderTemplate, context, project.ios.pluginRegistrantHeader, globals.templateRenderer, ); - _renderTemplateToFile( + await _renderTemplateToFile( _objcPluginRegistryImplementationTemplate, context, project.ios.pluginRegistrantImplementation, @@ -829,13 +835,13 @@ Future<void> _writeLinuxPluginFiles(FlutterProject project, List<Plugin> plugins } Future<void> _writeLinuxPluginRegistrant(Directory destination, Map<String, Object> templateContext) async { - _renderTemplateToFile( + await _renderTemplateToFile( _linuxPluginRegistryHeaderTemplate, templateContext, destination.childFile('generated_plugin_registrant.h'), globals.templateRenderer, ); - _renderTemplateToFile( + await _renderTemplateToFile( _linuxPluginRegistryImplementationTemplate, templateContext, destination.childFile('generated_plugin_registrant.cc'), @@ -844,7 +850,7 @@ Future<void> _writeLinuxPluginRegistrant(Directory destination, Map<String, Obje } Future<void> _writePluginCmakefile(File destinationFile, Map<String, Object> templateContext, TemplateRenderer templateRenderer) async { - _renderTemplateToFile( + await _renderTemplateToFile( _pluginCmakefileTemplate, templateContext, destinationFile, @@ -860,7 +866,7 @@ Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> pl 'framework': 'FlutterMacOS', 'methodChannelPlugins': macosMethodChannelPlugins, }; - _renderTemplateToFile( + await _renderTemplateToFile( _swiftPluginRegistryTemplate, context, project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'), @@ -931,13 +937,13 @@ Future<void> writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugin } Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, Object> templateContext, TemplateRenderer templateRenderer) async { - _renderTemplateToFile( + await _renderTemplateToFile( _cppPluginRegistryHeaderTemplate, templateContext, destination.childFile('generated_plugin_registrant.h'), templateRenderer, ); - _renderTemplateToFile( + await _renderTemplateToFile( _cppPluginRegistryImplementationTemplate, templateContext, destination.childFile('generated_plugin_registrant.cc'), @@ -955,7 +961,7 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug final String template = webPlugins.isEmpty ? _noopDartPluginRegistryTemplate : _dartPluginRegistryTemplate; - _renderTemplateToFile( + await _renderTemplateToFile( template, context, pluginFile, @@ -970,7 +976,7 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug /// be created only if missing. /// /// This uses [project.flutterPluginsDependenciesFile], so it should only be -/// run after refreshPluginList has been run since the last plugin change. +/// run after [refreshPluginsList] has been run since the last plugin change. void createPluginSymlinks(FlutterProject project, {bool force = false, @visibleForTesting FeatureFlags? featureFlagsOverride}) { final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags; Map<String, Object?>? platformPlugins; @@ -1029,6 +1035,14 @@ void handleSymlinkException(FileSystemException e, { : 'You must build from a terminal run as administrator.'; throwToolExit('Building with plugins requires symlink support.\n\n$instructions'); } + // ERROR_INVALID_FUNCTION, trying to link across drives, which is not supported + if (e.osError?.errorCode == 1) { + throwToolExit( + 'Creating symlink from $source to $destination failed with ' + 'ERROR_INVALID_FUNCTION. Try moving your Flutter project to the same ' + 'drive as your Flutter SDK.', + ); + } } } @@ -1411,8 +1425,8 @@ Future<void> generateMainDartWithPluginRegistrant( final File newMainDart = rootProject.dartPluginRegistrant; if (resolutions.isEmpty) { try { - if (newMainDart.existsSync()) { - newMainDart.deleteSync(); + if (await newMainDart.exists()) { + await newMainDart.delete(); } } on FileSystemException catch (error) { globals.printWarning( @@ -1428,7 +1442,7 @@ Future<void> generateMainDartWithPluginRegistrant( (templateContext[resolution.platform] as List<Object?>?)?.add(resolution.toMap()); } try { - _renderTemplateToFile( + await _renderTemplateToFile( _dartPluginRegistryForNonWebTemplate, templateContext, newMainDart, diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart index 708f79fd5c72a..c4c1e9146d1af 100644 --- a/packages/flutter_tools/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart @@ -7,6 +7,7 @@ import 'package:yaml/yaml.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/utils.dart'; +import 'features.dart'; import 'project.dart'; import 'template.dart'; import 'version.dart'; @@ -28,6 +29,9 @@ enum FlutterProjectType implements CliEnum { /// components, only Dart. package, + /// This is a Dart package project with external builds for native components. + packageFfi, + /// This is a native plugin project. plugin, @@ -52,6 +56,10 @@ enum FlutterProjectType implements CliEnum { 'Generate a shareable Flutter project containing an API ' 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' 'Linux, macOS, Windows, or any combination of these.', + FlutterProjectType.packageFfi => + 'Generate a shareable Dart/Flutter project containing an API ' + 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' + 'Linux, macOS, and Windows.', FlutterProjectType.module => 'Generate a project to add a Flutter module to an existing Android or iOS application.', }; @@ -64,6 +72,16 @@ enum FlutterProjectType implements CliEnum { } return null; } + + static List<FlutterProjectType> get enabledValues { + return <FlutterProjectType>[ + for (final FlutterProjectType value in values) + if (value == FlutterProjectType.packageFfi) ...<FlutterProjectType>[ + if (featureFlags.isNativeAssetsEnabled) value + ] else + value, + ]; + } } /// Verifies the expected yaml keys are present in the file. diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index 59045aa442359..48eaa42f11b8c 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart @@ -88,7 +88,7 @@ final BotDetector _defaultBotDetector = BotDetector( ); Future<bool> get isRunningOnBot => botDetector.isRunningOnBot; -// Analytics instance for package:unified_analytics for telemetry +// Analytics instance for package:unified_analytics for analytics // reporting for all Flutter and Dart related tooling Analytics get analytics => context.get<Analytics>()!; diff --git a/packages/flutter_tools/lib/src/intellij/intellij_validator.dart b/packages/flutter_tools/lib/src/intellij/intellij_validator.dart index 7f09515dec91d..c6d5e695afea7 100644 --- a/packages/flutter_tools/lib/src/intellij/intellij_validator.dart +++ b/packages/flutter_tools/lib/src/intellij/intellij_validator.dart @@ -7,6 +7,7 @@ import 'package:process/process.dart'; import '../base/file_system.dart'; import '../base/io.dart'; +import '../base/logger.dart'; import '../base/platform.dart'; import '../base/user_messages.dart' hide userMessages; import '../base/version.dart'; @@ -50,6 +51,7 @@ abstract class IntelliJValidator extends DoctorValidator { static Iterable<DoctorValidator> installedValidators({ required FileSystem fileSystem, required Platform platform, + required Logger logger, required UserMessages userMessages, required PlistParser plistParser, required ProcessManager processManager, @@ -77,6 +79,7 @@ abstract class IntelliJValidator extends DoctorValidator { userMessages: userMessages, plistParser: plistParser, processManager: processManager, + logger: logger, ); } return <DoctorValidator>[]; @@ -376,11 +379,13 @@ class IntelliJValidatorOnMac extends IntelliJValidator { 'IntelliJ IDEA.app': _ultimateEditionId, 'IntelliJ IDEA Ultimate.app': _ultimateEditionId, 'IntelliJ IDEA CE.app': _communityEditionId, + 'IntelliJ IDEA Community Edition.app': _communityEditionId, }; static Iterable<DoctorValidator> installed({ required FileSystem fileSystem, required FileSystemUtils fileSystemUtils, + required Logger logger, required UserMessages userMessages, required PlistParser plistParser, required ProcessManager processManager, @@ -482,6 +487,26 @@ class IntelliJValidatorOnMac extends IntelliJValidator { ]), )); } + + // Remove JetBrains Toolbox link apps. These tiny apps just + // link to the full app, will get detected elsewhere in our search. + validators.removeWhere((DoctorValidator validator) { + if (validator is! IntelliJValidatorOnMac) { + return false; + } + final String? identifierKey = plistParser.getValueFromFile<String>( + validator.plistFile, + PlistParser.kCFBundleIdentifierKey, + ); + if (identifierKey == null) { + logger.printTrace('Android Studio/IntelliJ installation at ' + '${validator.installPath} has a null CFBundleIdentifierKey, ' + 'which is a required field.'); + return false; + } + return identifierKey.contains('com.jetbrains.toolbox.linkapp'); + }); + return validators; } diff --git a/packages/flutter_tools/lib/src/ios/application_package.dart b/packages/flutter_tools/lib/src/ios/application_package.dart index 514b7d6798115..5472bb6d1fcd8 100644 --- a/packages/flutter_tools/lib/src/ios/application_package.dart +++ b/packages/flutter_tools/lib/src/ios/application_package.dart @@ -141,7 +141,7 @@ class BuildableIOSApp extends IOSApp { // not a top-level output directory. // Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`. String get archiveBundlePath => globals.fs.path.join(getIosBuildDirectory(), 'archive', - _hostAppBundleName == null ? 'Runner' : globals.fs.path.withoutExtension(_hostAppBundleName!)); + _hostAppBundleName == null ? 'Runner' : globals.fs.path.withoutExtension(_hostAppBundleName)); // The output xcarchive bundle path `build/ios/archive/Runner.xcarchive`. String get archiveBundleOutputPath => @@ -150,7 +150,7 @@ class BuildableIOSApp extends IOSApp { String get builtInfoPlistPathAfterArchive => globals.fs.path.join(archiveBundleOutputPath, 'Products', 'Applications', - _hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!, + _hostAppBundleName ?? 'Runner.app', 'Info.plist'); String get projectAppIconDirName => _projectImageAssetDirName(_appIconAsset); @@ -191,7 +191,7 @@ class BuildableIOSApp extends IOSApp { // Template asset's images are in flutter_template_images package. Future<String> _templateImageAssetDirNameForImages(String asset) async { - final Directory imageTemplate = await templateImageDirectory(null, globals.fs, globals.logger); + final Directory imageTemplate = await templatePathProvider.imageDirectory(null, globals.fs, globals.logger); return globals.fs.path.join(imageTemplate.path, _templateImageAssetDirNameSuffix(asset)); } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index ca6e594885f8a..d07542d436591 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -537,34 +537,13 @@ class IOSDevice extends Device { int installationResult = 1; if (debuggingOptions.debuggingEnabled) { _logger.printTrace('Debugging is enabled, connecting to vmService'); - final DeviceLogReader deviceLogReader = getLogReader( - app: package, - usingCISystem: debuggingOptions.usingCISystem, - ); - - // If the device supports syslog reading, prefer launching the app without - // attaching the debugger to avoid the overhead of the unnecessary extra running process. - if (majorSdkVersion >= IOSDeviceLogReader.minimumUniversalLoggingSdkVersion) { - iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch( - deviceId: id, - bundlePath: bundle.path, - appDeltaDirectory: package.appDeltaDirectory, - launchArguments: launchArguments, - interfaceType: connectionInterface, - uninstallFirst: debuggingOptions.uninstallFirst, - ); - if (deviceLogReader is IOSDeviceLogReader) { - deviceLogReader.debuggerStream = iosDeployDebugger; - } - } - // Don't port foward if debugging with a wireless device. - vmServiceDiscovery = ProtocolDiscovery.vmService( - deviceLogReader, - portForwarder: isWirelesslyConnected ? null : portForwarder, - hostPort: debuggingOptions.hostVmServicePort, - devicePort: debuggingOptions.deviceVmServicePort, + vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery( + package: package, + bundle: bundle, + debuggingOptions: debuggingOptions, + launchArguments: launchArguments, ipv6: ipv6, - logger: _logger, + uninstallFirst: debuggingOptions.uninstallFirst, ); } @@ -590,10 +569,7 @@ class IOSDevice extends Device { installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; } if (installationResult != 0) { - _logger.printError('Could not run ${bundle.path} on $id.'); - _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); - _logger.printError(' open ios/Runner.xcworkspace'); - _logger.printError(''); + _printInstallError(bundle); await dispose(); return LaunchResult.failed(); } @@ -705,6 +681,32 @@ class IOSDevice extends Device { ); } else { localUri = await vmServiceDiscovery?.uri; + // If the `ios-deploy` debugger loses connection before it finds the + // Dart Service VM url, try starting the debugger and launching the + // app again. + if (localUri == null && + debuggingOptions.usingCISystem && + iosDeployDebugger != null && + iosDeployDebugger!.lostConnection) { + _logger.printStatus('Lost connection to device. Trying to connect again...'); + await dispose(); + vmServiceDiscovery = _setupDebuggerAndVmServiceDiscovery( + package: package, + bundle: bundle, + debuggingOptions: debuggingOptions, + launchArguments: launchArguments, + ipv6: ipv6, + uninstallFirst: false, + skipInstall: true, + ); + installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; + if (installationResult != 0) { + _printInstallError(bundle); + await dispose(); + return LaunchResult.failed(); + } + localUri = await vmServiceDiscovery.uri; + } } } timer.cancel(); @@ -736,6 +738,54 @@ class IOSDevice extends Device { } } + void _printInstallError(Directory bundle) { + _logger.printError('Could not run ${bundle.path} on $id.'); + _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); + _logger.printError(' open ios/Runner.xcworkspace'); + _logger.printError(''); + } + + ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({ + required IOSApp package, + required Directory bundle, + required DebuggingOptions debuggingOptions, + required List<String> launchArguments, + required bool ipv6, + required bool uninstallFirst, + bool skipInstall = false, + }) { + final DeviceLogReader deviceLogReader = getLogReader( + app: package, + usingCISystem: debuggingOptions.usingCISystem, + ); + + // If the device supports syslog reading, prefer launching the app without + // attaching the debugger to avoid the overhead of the unnecessary extra running process. + if (majorSdkVersion >= IOSDeviceLogReader.minimumUniversalLoggingSdkVersion) { + iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch( + deviceId: id, + bundlePath: bundle.path, + appDeltaDirectory: package.appDeltaDirectory, + launchArguments: launchArguments, + interfaceType: connectionInterface, + uninstallFirst: uninstallFirst, + skipInstall: skipInstall, + ); + if (deviceLogReader is IOSDeviceLogReader) { + deviceLogReader.debuggerStream = iosDeployDebugger; + } + } + // Don't port foward if debugging with a wireless device. + return ProtocolDiscovery.vmService( + deviceLogReader, + portForwarder: isWirelesslyConnected ? null : portForwarder, + hostPort: debuggingOptions.hostVmServicePort, + devicePort: debuggingOptions.deviceVmServicePort, + ipv6: ipv6, + logger: _logger, + ); + } + /// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to /// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used /// to install the app, launch the app, and start `debugserver`. diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart index 2d1f7d6ffbe53..71cb6dbee63d7 100644 --- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart @@ -129,6 +129,7 @@ class IOSDeploy { required DeviceConnectionInterface interfaceType, Directory? appDeltaDirectory, required bool uninstallFirst, + bool skipInstall = false, }) { appDeltaDirectory?.createSync(recursive: true); // Interactive debug session to support sending the lldb detach command. @@ -148,6 +149,8 @@ class IOSDeploy { ], if (uninstallFirst) '--uninstall', + if (skipInstall) + '--noinstall', '--debug', if (interfaceType != DeviceConnectionInterface.wireless) '--no-wifi', @@ -327,6 +330,14 @@ class IOSDeployDebugger { /// The future should be completed once the backtraces are logged. Completer<void>? _processResumeCompleter; + // Process 525 exited with status = -1 (0xffffffff) lost connection + static final RegExp _lostConnectionPattern = RegExp(r'exited with status = -1 \(0xffffffff\) lost connection'); + + /// Whether ios-deploy received a message matching [_lostConnectionPattern], + /// indicating that it lost connection to the device. + bool get lostConnection => _lostConnection; + bool _lostConnection = false; + /// Launch the app on the device, and attach the debugger. /// /// Returns whether or not the debugger successfully attached. @@ -338,6 +349,8 @@ class IOSDeployDebugger { RegExp lldbRun = RegExp(r'\(lldb\)\s*run'); final Completer<bool> debuggerCompleter = Completer<bool>(); + + bool receivedLogs = false; try { _iosDeployProcess = await _processUtils.start( _launchCommand, @@ -386,8 +399,6 @@ class IOSDeployDebugger { if (lldbRun.hasMatch(line)) { _logger.printTrace(line); _debuggerState = _IOSDeployDebuggerState.launching; - // TODO(vashworth): Remove all debugger state comments when https://github.com/flutter/flutter/issues/126412 is resolved. - _logger.printTrace('Debugger state set to launching.'); return; } // Next line after "run" must be "success", or the attach failed. @@ -396,7 +407,6 @@ class IOSDeployDebugger { _logger.printTrace(line); final bool attachSuccess = line == 'success'; _debuggerState = attachSuccess ? _IOSDeployDebuggerState.attached : _IOSDeployDebuggerState.detached; - _logger.printTrace('Debugger state set to ${attachSuccess ? 'attached' : 'detached'}.'); if (!debuggerCompleter.isCompleted) { debuggerCompleter.complete(attachSuccess); } @@ -425,7 +435,6 @@ class IOSDeployDebugger { // Even though we're not "detached", just stopped, mark as detached so the backtrace // is only show in verbose. _debuggerState = _IOSDeployDebuggerState.detached; - _logger.printTrace('Debugger state set to detached.'); // If we paused the app and are waiting to resume it, complete the completer final Completer<void>? processResumeCompleter = _processResumeCompleter; @@ -450,6 +459,9 @@ class IOSDeployDebugger { // The app exited or crashed, so exit. Continue passing debugging // messages to the log reader until it exits to capture crash dumps. _logger.printTrace(line); + if (line.contains(_lostConnectionPattern)) { + _lostConnection = true; + } exit(); return; } @@ -465,7 +477,6 @@ class IOSDeployDebugger { _logger.printTrace(line); // we marked this detached when we received [_backTraceAll] _debuggerState = _IOSDeployDebuggerState.attached; - _logger.printTrace('Debugger state set to attached.'); return; } @@ -480,6 +491,16 @@ class IOSDeployDebugger { // This will still cause "legit" logged newlines to be doubled... } else if (!_debuggerOutput.isClosed) { _debuggerOutput.add(line); + + // Sometimes the `ios-deploy` process does not return logs from the + // application after attaching, such as the Dart VM url. In CI, + // `idevicesyslog` is used as a fallback to get logs. Print a + // message to indicate whether logs were received from `ios-deploy` + // to help with debugging. + if (!receivedLogs) { + _logger.printTrace('Received logs from ios-deploy.'); + receivedLogs = true; + } } lastLineFromDebugger = line; }); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 541c89443a558..4d43965a72b73 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -73,6 +73,7 @@ class IMobileDevice { /// Create an [IMobileDevice] for testing. factory IMobileDevice.test({ required ProcessManager processManager }) { return IMobileDevice( + // ignore: invalid_use_of_visible_for_testing_member artifacts: Artifacts.test(), cache: Cache.test(processManager: processManager), processManager: processManager, @@ -374,10 +375,10 @@ Future<XcodeBuildResult> buildXcodeProject({ buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); } - final File resultBundleFile = tempDir.childFile(_kResultBundlePath); + final Directory resultBundleDirectory = tempDir.childDirectory(_kResultBundlePath); buildCommands.addAll(<String>[ '-resultBundlePath', - resultBundleFile.absolute.path, + resultBundleDirectory.absolute.path, '-resultBundleVersion', _kResultBundleVersion, ]); @@ -399,7 +400,7 @@ Future<XcodeBuildResult> buildXcodeProject({ final Stopwatch sw = Stopwatch()..start(); initialBuildStatus = globals.logger.startProgress('Running Xcode build...'); - buildResult = await _runBuildWithRetries(buildCommands, app, resultBundleFile); + buildResult = await _runBuildWithRetries(buildCommands, app, resultBundleDirectory); // Notifies listener that no more output is coming. scriptOutputPipeFile?.writeAsStringSync('all done'); @@ -590,14 +591,14 @@ Future<void> removeFinderExtendedAttributes(FileSystemEntity projectDirectory, P } } -Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app, File resultBundleFile) async { +Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app, Directory resultBundleDirectory) async { int buildRetryDelaySeconds = 1; int remainingTries = 8; RunResult? buildResult; while (remainingTries > 0) { - if (resultBundleFile.existsSync()) { - resultBundleFile.deleteSync(recursive: true); + if (resultBundleDirectory.existsSync()) { + resultBundleDirectory.deleteSync(recursive: true); } remainingTries--; buildRetryDelaySeconds *= 2; diff --git a/packages/flutter_tools/lib/src/ios/native_assets.dart b/packages/flutter_tools/lib/src/ios/native_assets.dart new file mode 100644 index 0000000000000..8a18b9eb14765 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/native_assets.dart @@ -0,0 +1,171 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_builder/native_assets_builder.dart' show BuildResult; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; + +import '../macos/native_assets_host.dart'; +import '../native_assets.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file and +/// the Xcode project. +Future<Uri?> dryRunNativeAssetsIOS({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + required FileSystem fileSystem, +}) async { + if (!await nativeBuildRequired(buildRunner)) { + return null; + } + + final Uri buildUri = nativeAssetsBuildUri(projectUri, OS.iOS); + final Iterable<Asset> assetTargetLocations = await dryRunNativeAssetsIOSInternal( + fileSystem, + projectUri, + buildRunner, + ); + final Uri nativeAssetsUri = await writeNativeAssetsYaml( + assetTargetLocations, + buildUri, + fileSystem, + ); + return nativeAssetsUri; +} + +Future<Iterable<Asset>> dryRunNativeAssetsIOSInternal( + FileSystem fileSystem, + Uri projectUri, + NativeAssetsBuildRunner buildRunner, +) async { + const OS targetOS = OS.iOS; + globals.logger.printTrace('Dry running native assets for $targetOS.'); + final List<Asset> nativeAssets = (await buildRunner.dryRun( + linkModePreference: LinkModePreference.dynamic, + targetOS: targetOS, + workingDirectory: projectUri, + includeParentEnvironment: true, + )) + .assets; + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Dry running native assets for $targetOS done.'); + final Iterable<Asset> assetTargetLocations = _assetTargetLocations(nativeAssets).values; + return assetTargetLocations; +} + +/// Builds native assets. +Future<List<Uri>> buildNativeAssetsIOS({ + required NativeAssetsBuildRunner buildRunner, + required List<DarwinArch> darwinArchs, + required EnvironmentType environmentType, + required Uri projectUri, + required BuildMode buildMode, + String? codesignIdentity, + required Uri yamlParentDirectory, + required FileSystem fileSystem, +}) async { + if (!await nativeBuildRequired(buildRunner)) { + await writeNativeAssetsYaml(<Asset>[], yamlParentDirectory, fileSystem); + return <Uri>[]; + } + + final List<Target> targets = darwinArchs.map(_getNativeTarget).toList(); + final native_assets_cli.BuildMode buildModeCli = nativeAssetsBuildMode(buildMode); + + const OS targetOS = OS.iOS; + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + final IOSSdk iosSdk = _getIOSSdk(environmentType); + + globals.logger.printTrace('Building native assets for $targets $buildModeCli.'); + final List<Asset> nativeAssets = <Asset>[]; + final Set<Uri> dependencies = <Uri>{}; + for (final Target target in targets) { + final BuildResult result = await buildRunner.build( + linkModePreference: LinkModePreference.dynamic, + target: target, + targetIOSSdk: iosSdk, + buildMode: buildModeCli, + workingDirectory: projectUri, + includeParentEnvironment: true, + cCompilerConfig: await buildRunner.cCompilerConfig, + ); + nativeAssets.addAll(result.assets); + dependencies.addAll(result.dependencies); + } + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Building native assets for $targets done.'); + final Map<AssetPath, List<Asset>> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets); + await copyNativeAssetsMacOSHost( + buildUri, + fatAssetTargetLocations, + codesignIdentity, + buildMode, + fileSystem, + ); + + final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets); + await writeNativeAssetsYaml( + assetTargetLocations.values, + yamlParentDirectory, + fileSystem, + ); + return dependencies.toList(); +} + +IOSSdk _getIOSSdk(EnvironmentType environmentType) { + switch (environmentType) { + case EnvironmentType.physical: + return IOSSdk.iPhoneOs; + case EnvironmentType.simulator: + return IOSSdk.iPhoneSimulator; + } +} + +/// Extract the [Target] from a [DarwinArch]. +Target _getNativeTarget(DarwinArch darwinArch) { + switch (darwinArch) { + case DarwinArch.armv7: + return Target.iOSArm; + case DarwinArch.arm64: + return Target.iOSArm64; + case DarwinArch.x86_64: + return Target.iOSX64; + } +} + +Map<AssetPath, List<Asset>> _fatAssetTargetLocations(List<Asset> nativeAssets) { + final Map<AssetPath, List<Asset>> result = <AssetPath, List<Asset>>{}; + for (final Asset asset in nativeAssets) { + final AssetPath path = _targetLocationIOS(asset).path; + result[path] ??= <Asset>[]; + result[path]!.add(asset); + } + return result; +} + +Map<Asset, Asset> _assetTargetLocations(List<Asset> nativeAssets) => <Asset, Asset>{ + for (final Asset asset in nativeAssets) + asset: _targetLocationIOS(asset), +}; + +Asset _targetLocationIOS(Asset asset) { + final AssetPath path = asset.path; + switch (path) { + case AssetSystemPath _: + case AssetInExecutable _: + case AssetInProcess _: + return asset; + case AssetAbsolutePath _: + final String fileName = path.uri.pathSegments.last; + return asset.copyWith(path: AssetAbsolutePath(Uri(path: fileName))); + } + throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset'); +} diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart index f6e1a9fe7ccc5..a482957455294 100644 --- a/packages/flutter_tools/lib/src/ios/plist_parser.dart +++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart @@ -30,6 +30,8 @@ class PlistParser { static const String kCFBundleExecutableKey = 'CFBundleExecutable'; static const String kCFBundleVersionKey = 'CFBundleVersion'; static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName'; + static const String kCFBundleNameKey = 'CFBundleName'; + static const String kFLTEnableImpellerKey = 'FLTEnableImpeller'; static const String kMinimumOSVersionKey = 'MinimumOSVersion'; static const String kNSPrincipalClassKey = 'NSPrincipalClass'; @@ -43,8 +45,6 @@ class PlistParser { /// /// If [plistFilePath] points to a non-existent file or a file that's not a /// valid property list file, this will return null. - /// - /// The [plistFilePath] argument must not be null. String? plistXmlContent(String plistFilePath) { if (!_fileSystem.isFileSync(_plutilExecutable)) { throw const FileNotFoundException(_plutilExecutable); @@ -100,8 +100,6 @@ class PlistParser { /// /// If [plistFilePath] points to a non-existent file or a file that's not a /// valid property list file, this will return an empty map. - /// - /// The [plistFilePath] argument must not be null. Map<String, Object> parseFile(String plistFilePath) { if (!_fileSystem.isFileSync(plistFilePath)) { return const <String, Object>{}; @@ -175,8 +173,6 @@ class PlistParser { /// valid property list file, this will return null. /// /// If [key] is not found in the property list, this will return null. - /// - /// The [plistFilePath] and [key] arguments must not be null. T? getValueFromFile<T>(String plistFilePath, String key) { final Map<String, dynamic> parsed = parseFile(plistFilePath); return parsed[key] as T?; diff --git a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart index eeda85a63bf9f..8bf662b4b31ce 100644 --- a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart +++ b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart @@ -182,12 +182,15 @@ Future<List<String>> _xcodeBuildSettingsLines({ final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo; if (localEngineInfo != null) { - final String engineOutPath = localEngineInfo.engineOutPath; + final String engineOutPath = localEngineInfo.targetOutPath; xcodeBuildSettings.add('FLUTTER_ENGINE=${globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath))}'); - final String localEngineName = localEngineInfo.localEngineName; + final String localEngineName = localEngineInfo.localTargetName; xcodeBuildSettings.add('LOCAL_ENGINE=$localEngineName'); + final String localEngineHostName = localEngineInfo.localHostName; + xcodeBuildSettings.add('LOCAL_ENGINE_HOST=$localEngineHostName'); + // Tell Xcode not to build universal binaries for local engines, which are // single-architecture. // diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 011454bc00f7e..bb81534287652 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -412,21 +412,6 @@ class XcodeProjectBuildContext { } } -/// The settings that are relevant for setting up universal links -@immutable -class XcodeUniversalLinkSettings { - const XcodeUniversalLinkSettings({ - this.bundleIdentifier, - this.teamIdentifier, - this.associatedDomains = const <String>[], - }); - - final String? bundleIdentifier; - final String? teamIdentifier; - final List<String> associatedDomains; -} - - /// Information about an Xcode project. /// /// Represents the output of `xcodebuild -list`. diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index 903eae94f6bfc..e9539482ab6db 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -295,7 +295,6 @@ class WebAssetServer implements AssetReader { server, PackageUriMapper(packageConfig), digestProvider, - server.basePath, packageConfig.toPackageUri( globals.fs.file(entrypoint).absolute.uri, ), @@ -347,6 +346,7 @@ class WebAssetServer implements AssetReader { /// /// It should have no leading or trailing slashes. @visibleForTesting + @override String basePath; // handle requests for JavaScript source, dart sources maps, or asset files. diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index a44b803320a01..2c81eb2c91817 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -446,7 +446,6 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). fullRestart: true, reason: reason, overallTimeInMs: elapsed.inMilliseconds, - fastReassemble: false, ).send(); } return OperationResult.ok; diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart index fb1b937ddfe9d..1bcf07361fd65 100644 --- a/packages/flutter_tools/lib/src/linux/build_linux.dart +++ b/packages/flutter_tools/lib/src/linux/build_linux.dart @@ -17,6 +17,7 @@ import '../convert.dart'; import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; +import '../migrations/cmake_native_assets_migration.dart'; // Matches the following error and warning patterns: // - <file path>:<line>:<column>: (fatal) error: <error...> @@ -29,12 +30,13 @@ final RegExp errorMatcher = RegExp(r'(?:(?:.*:\d+:\d+|clang):\s)?(fatal\s)?(?:er Future<void> buildLinux( LinuxProject linuxProject, BuildInfo buildInfo, { - String? target, - SizeAnalyzer? sizeAnalyzer, - bool needCrossBuild = false, - required TargetPlatform targetPlatform, - String targetSysroot = '/', - }) async { + String? target, + SizeAnalyzer? sizeAnalyzer, + bool needCrossBuild = false, + required TargetPlatform targetPlatform, + String targetSysroot = '/', + required Logger logger, +}) async { target ??= 'lib/main.dart'; if (!linuxProject.cmakeFile.existsSync()) { throwToolExit('No Linux desktop project configured. See ' @@ -43,7 +45,8 @@ Future<void> buildLinux( } final List<ProjectMigrator> migrators = <ProjectMigrator>[ - CmakeCustomCommandMigration(linuxProject, globals.logger), + CmakeCustomCommandMigration(linuxProject, logger), + CmakeNativeAssetsMigration(linuxProject, 'linux', logger), ]; final ProjectMigration migration = ProjectMigration(migrators); @@ -55,15 +58,17 @@ Future<void> buildLinux( environmentConfig['FLUTTER_TARGET'] = target; final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo; if (localEngineInfo != null) { - final String engineOutPath = localEngineInfo.engineOutPath; - environmentConfig['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath)); - environmentConfig['LOCAL_ENGINE'] = localEngineInfo.localEngineName; + final String targetOutPath = localEngineInfo.targetOutPath; + // $ENGINE/src/out/foo_bar_baz -> $ENGINE/src + environmentConfig['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(targetOutPath)); + environmentConfig['LOCAL_ENGINE'] = localEngineInfo.localTargetName; + environmentConfig['LOCAL_ENGINE_HOST'] = localEngineInfo.localHostName; } - writeGeneratedCmakeConfig(Cache.flutterRoot!, linuxProject, buildInfo, environmentConfig); + writeGeneratedCmakeConfig(Cache.flutterRoot!, linuxProject, buildInfo, environmentConfig, logger); createPluginSymlinks(linuxProject.parent); - final Status status = globals.logger.startProgress( + final Status status = logger.startProgress( 'Building Linux application...', ); try { @@ -97,13 +102,13 @@ Future<void> buildLinux( .childDirectory('.flutter-devtools'), 'linux-code-size-analysis', 'json', )..writeAsStringSync(jsonEncode(output)); // This message is used as a sentinel in analyze_apk_size_test.dart - globals.printStatus( + logger.printStatus( 'A summary of your Linux bundle analysis can be found at: ${outputFile.path}', ); // DevTools expects a file path relative to the .flutter-devtools/ dir. final String relativeAppSizePath = outputFile.path.split('.flutter-devtools/').last.trim(); - globals.printStatus( + logger.printStatus( '\nTo analyze your app size in Dart DevTools, run the following command:\n' 'dart devtools --appSizeBase=$relativeAppSizePath' ); diff --git a/packages/flutter_tools/lib/src/linux/linux_device.dart b/packages/flutter_tools/lib/src/linux/linux_device.dart index 8e6b141a949ac..7d210ee049da4 100644 --- a/packages/flutter_tools/lib/src/linux/linux_device.dart +++ b/packages/flutter_tools/lib/src/linux/linux_device.dart @@ -25,6 +25,7 @@ class LinuxDevice extends DesktopDevice { required FileSystem fileSystem, required OperatingSystemUtils operatingSystemUtils, }) : _operatingSystemUtils = operatingSystemUtils, + _logger = logger, super( 'linux', platformType: PlatformType.linux, @@ -36,6 +37,7 @@ class LinuxDevice extends DesktopDevice { ); final OperatingSystemUtils _operatingSystemUtils; + final Logger _logger; @override bool isSupported() => true; @@ -66,6 +68,7 @@ class LinuxDevice extends DesktopDevice { buildInfo, target: mainPath, targetPlatform: await targetPlatform, + logger: _logger, ); } diff --git a/packages/flutter_tools/lib/src/linux/native_assets.dart b/packages/flutter_tools/lib/src/linux/native_assets.dart new file mode 100644 index 0000000000000..692c4126ac830 --- /dev/null +++ b/packages/flutter_tools/lib/src/linux/native_assets.dart @@ -0,0 +1,98 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; + +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; +import '../native_assets.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file. +Future<Uri?> dryRunNativeAssetsLinux({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + bool flutterTester = false, + required FileSystem fileSystem, +}) { + return dryRunNativeAssetsSingleArchitecture( + buildRunner: buildRunner, + projectUri: projectUri, + flutterTester: flutterTester, + fileSystem: fileSystem, + os: OS.linux, + ); +} + +Future<Iterable<Asset>> dryRunNativeAssetsLinuxInternal( + FileSystem fileSystem, + Uri projectUri, + bool flutterTester, + NativeAssetsBuildRunner buildRunner, +) { + return dryRunNativeAssetsSingleArchitectureInternal( + fileSystem, + projectUri, + flutterTester, + buildRunner, + OS.linux, + ); +} + +Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> buildNativeAssetsLinux({ + required NativeAssetsBuildRunner buildRunner, + TargetPlatform? targetPlatform, + required Uri projectUri, + required BuildMode buildMode, + bool flutterTester = false, + Uri? yamlParentDirectory, + required FileSystem fileSystem, +}) { + return buildNativeAssetsSingleArchitecture( + buildRunner: buildRunner, + targetPlatform: targetPlatform, + projectUri: projectUri, + buildMode: buildMode, + flutterTester: flutterTester, + yamlParentDirectory: yamlParentDirectory, + fileSystem: fileSystem, + ); +} + +/// Flutter expects `clang++` to be on the path on Linux hosts. +/// +/// Search for the accompanying `clang`, `ar`, and `ld`. +Future<CCompilerConfig> cCompilerConfigLinux() async { + const String kClangPlusPlusBinary = 'clang++'; + const String kClangBinary = 'clang'; + const String kArBinary = 'llvm-ar'; + const String kLdBinary = 'ld.lld'; + + final ProcessResult whichResult = await globals.processManager.run(<String>['which', kClangPlusPlusBinary]); + if (whichResult.exitCode != 0) { + throwToolExit('Failed to find $kClangPlusPlusBinary on PATH.'); + } + File clangPpFile = globals.fs.file((whichResult.stdout as String).trim()); + clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks()); + + final Directory clangDir = clangPpFile.parent; + final Map<String, Uri> binaryPaths = <String, Uri>{}; + for (final String binary in <String>[kClangBinary, kArBinary, kLdBinary]) { + final File binaryFile = clangDir.childFile(binary); + if (!await binaryFile.exists()) { + throwToolExit("Failed to find $binary relative to $clangPpFile: $binaryFile doesn't exist."); + } + binaryPaths[binary] = binaryFile.uri; + } + return CCompilerConfig( + ar: binaryPaths[kArBinary], + cc: binaryPaths[kClangBinary], + ld: binaryPaths[kLdBinary], + ); +} diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 17bc3641a2ebe..206dedbaf1dc3 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart @@ -47,6 +47,9 @@ Future<LocalizationsGenerator> generateLocalizations({ precacheLanguageAndRegionTags(); + // Use \r\n if project's pubspec file contains \r\n. + final bool useCRLF = fileSystem.file('pubspec.yaml').readAsStringSync().contains('\r\n'); + LocalizationsGenerator generator; try { generator = LocalizationsGenerator( @@ -69,23 +72,19 @@ Future<LocalizationsGenerator> generateLocalizations({ useEscaping: options.useEscaping, logger: logger, suppressWarnings: options.suppressWarnings, + useRelaxedSyntax: options.relaxSyntax, ) ..loadResources() - ..writeOutputFiles(isFromYaml: true); + ..writeOutputFiles(isFromYaml: true, useCRLF: useCRLF); } on L10nException catch (e) { throwToolExit(e.message); } - final List<String> outputFileList = generator.outputFileList; - final File? untranslatedMessagesFile = generator.untranslatedMessagesFile; - - // All other post processing. if (options.format) { - final List<String> formatFileList = outputFileList.toList(); - if (untranslatedMessagesFile != null) { - // Don't format the messages file using `dart format`. - formatFileList.remove(untranslatedMessagesFile.absolute.path); - } + // Only format Dart files using `dart format`. + final List<String> formatFileList = generator.outputFileList + .where((String e) => e.endsWith('.dart')) + .toList(growable: false); if (formatFileList.isEmpty) { return generator; } @@ -492,6 +491,7 @@ class LocalizationsGenerator { bool useEscaping = false, required Logger logger, bool suppressWarnings = false, + bool useRelaxedSyntax = false, }) { final Directory? projectDirectory = projectDirFromPath(fileSystem, projectPathString); final Directory inputDirectory = inputDirectoryFromPath(fileSystem, inputPathString, projectDirectory); @@ -515,6 +515,7 @@ class LocalizationsGenerator { useEscaping: useEscaping, logger: logger, suppressWarnings: suppressWarnings, + useRelaxedSyntax: useRelaxedSyntax, ); } @@ -539,6 +540,7 @@ class LocalizationsGenerator { required this.logger, this.useEscaping = false, this.suppressWarnings = false, + this.useRelaxedSyntax = false, }); final FileSystem _fs; @@ -615,6 +617,9 @@ class LocalizationsGenerator { /// from calling [_generateMethod]. bool hadErrors = false; + /// Whether to use relaxed syntax. + bool useRelaxedSyntax = false; + /// The list of all arb path strings in [inputDirectory]. List<String> get arbPathStrings { return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList(); @@ -906,7 +911,13 @@ class LocalizationsGenerator { } // The call to .toList() is absolutely necessary. Otherwise, it is an iterator and will call Message's constructor again. _allMessages = _templateBundle.resourceIds.map((String id) => Message( - _templateBundle, _allBundles, id, areResourceAttributesRequired, useEscaping: useEscaping, logger: logger, + _templateBundle, + _allBundles, + id, + areResourceAttributesRequired, + useEscaping: useEscaping, + logger: logger, + useRelaxedSyntax: useRelaxedSyntax, )).toList(); hadErrors = _allMessages.any((Message message) => message.hadErrors); if (inputsAndOutputsListFile != null) { @@ -1310,7 +1321,7 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " } } - List<String> writeOutputFiles({ bool isFromYaml = false }) { + List<String> writeOutputFiles({ bool isFromYaml = false, bool useCRLF = false }) { // First, generate the string contents of all necessary files. final String generatedLocalizationsFile = _generateCode(); @@ -1328,7 +1339,9 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " syntheticPackageDirectory.createSync(recursive: true); final File flutterGenPubspec = syntheticPackageDirectory.childFile('pubspec.yaml'); if (!flutterGenPubspec.existsSync()) { - flutterGenPubspec.writeAsStringSync(emptyPubspecTemplate); + flutterGenPubspec.writeAsStringSync( + useCRLF ? emptyPubspecTemplate.replaceAll('\n', '\r\n') : emptyPubspecTemplate + ); } } @@ -1347,11 +1360,13 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " // Generate the required files for localizations. _languageFileMap.forEach((File file, String contents) { - file.writeAsStringSync(contents); + file.writeAsStringSync(useCRLF ? contents.replaceAll('\n', '\r\n') : contents); _outputFileList.add(file.absolute.path); }); - baseOutputFile.writeAsStringSync(generatedLocalizationsFile); + baseOutputFile.writeAsStringSync( + useCRLF ? generatedLocalizationsFile.replaceAll('\n', '\r\n') : generatedLocalizationsFile + ); final File? messagesFile = untranslatedMessagesFile; if (messagesFile != null) { _generateUntranslatedMessagesFile(logger, messagesFile); @@ -1387,12 +1402,12 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", " if (!inputsAndOutputsListFileLocal.existsSync()) { inputsAndOutputsListFileLocal.createSync(recursive: true); } - + final String filesListContent = json.encode(<String, Object> { + 'inputs': _inputFileList, + 'outputs': _outputFileList, + }); inputsAndOutputsListFileLocal.writeAsStringSync( - json.encode(<String, Object> { - 'inputs': _inputFileList, - 'outputs': _outputFileList, - }), + useCRLF ? filesListContent.replaceAll('\n', '\r\n') : filesListContent, ); } diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart index a978e43794343..d17988dd1e39b 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart @@ -336,6 +336,7 @@ class Message { this.resourceId, bool isResourceAttributeRequired, { + this.useRelaxedSyntax = false, this.useEscaping = false, this.logger, } @@ -352,13 +353,18 @@ class Message { filenames[bundle.locale] = bundle.file.basename; final String? translation = bundle.translationFor(resourceId); messages[bundle.locale] = translation; + List<String>? validPlaceholders; + if (useRelaxedSyntax) { + validPlaceholders = placeholders.entries.map((MapEntry<String, Placeholder> e) => e.key).toList(); + } try { parsedMessages[bundle.locale] = translation == null ? null : Parser( resourceId, bundle.file.basename, translation, useEscaping: useEscaping, - logger: logger + placeholders: validPlaceholders, + logger: logger, ).parse(); } on L10nParserException catch (error) { logger?.printError(error.toString()); @@ -378,6 +384,7 @@ class Message { final Map<LocaleInfo, Node?> parsedMessages; final Map<String, Placeholder> placeholders; final bool useEscaping; + final bool useRelaxedSyntax; final Logger? logger; bool hadErrors = false; @@ -559,13 +566,18 @@ class Message { } } -// Represents the contents of one ARB file. +/// Represents the contents of one ARB file. class AppResourceBundle { + /// Assuming that the caller has verified that the file exists and is readable. factory AppResourceBundle(File file) { - // Assuming that the caller has verified that the file exists and is readable. - Map<String, Object?> resources; + final Map<String, Object?> resources; try { - resources = json.decode(file.readAsStringSync()) as Map<String, Object?>; + final String content = file.readAsStringSync().trim(); + if (content.isEmpty) { + resources = <String, Object?>{}; + } else { + resources = json.decode(content) as Map<String, Object?>; + } } on FormatException catch (e) { throw L10nException( 'The arb file ${file.path} has the following formatting issue: \n' @@ -650,20 +662,26 @@ class AppResourceBundleCollection { final RegExp filenameRE = RegExp(r'(\w+)\.arb$'); final Map<LocaleInfo, AppResourceBundle> localeToBundle = <LocaleInfo, AppResourceBundle>{}; final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{}; - final List<File> files = directory.listSync().whereType<File>().toList()..sort(sortFilesByPath); + // We require the list of files to be sorted so that + // "languageToLocales[bundle.locale.languageCode]" is not null + // by the time we handle locales with country codes. + final List<File> files = directory + .listSync() + .whereType<File>() + .where((File e) => filenameRE.hasMatch(e.path)) + .toList() + ..sort(sortFilesByPath); for (final File file in files) { - if (filenameRE.hasMatch(file.path)) { - final AppResourceBundle bundle = AppResourceBundle(file); - if (localeToBundle[bundle.locale] != null) { - throw L10nException( - "Multiple arb files with the same '${bundle.locale}' locale detected. \n" - 'Ensure that there is exactly one arb file for each locale.' - ); - } - localeToBundle[bundle.locale] = bundle; - languageToLocales[bundle.locale.languageCode] ??= <LocaleInfo>[]; - languageToLocales[bundle.locale.languageCode]!.add(bundle.locale); + final AppResourceBundle bundle = AppResourceBundle(file); + if (localeToBundle[bundle.locale] != null) { + throw L10nException( + "Multiple arb files with the same '${bundle.locale}' locale detected. \n" + 'Ensure that there is exactly one arb file for each locale.' + ); } + localeToBundle[bundle.locale] = bundle; + languageToLocales[bundle.locale.languageCode] ??= <LocaleInfo>[]; + languageToLocales[bundle.locale.languageCode]!.add(bundle.locale); } languageToLocales.forEach((String language, List<LocaleInfo> listOfCorrespondingLocales) { diff --git a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart index 329f425a9bc23..84bfa86251629 100644 --- a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart +++ b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart @@ -354,6 +354,7 @@ class LocalizationOptions { bool? format, bool? useEscaping, bool? suppressWarnings, + bool? relaxSyntax, }) : templateArbFile = templateArbFile ?? 'app_en.arb', outputLocalizationFile = outputLocalizationFile ?? 'app_localizations.dart', outputClass = outputClass ?? 'AppLocalizations', @@ -363,7 +364,8 @@ class LocalizationOptions { nullableGetter = nullableGetter ?? true, format = format ?? false, useEscaping = useEscaping ?? false, - suppressWarnings = suppressWarnings ?? false; + suppressWarnings = suppressWarnings ?? false, + relaxSyntax = relaxSyntax ?? false; /// The `--arb-dir` argument. /// @@ -455,6 +457,16 @@ class LocalizationOptions { /// /// Whether or not to suppress warnings. final bool suppressWarnings; + + /// The `relax-syntax` argument. + /// + /// Whether or not to relax the syntax. When specified, the syntax will be + /// relaxed so that the special character "{" is treated as a string if it is + /// not followed by a valid placeholder and "}" is treated as a string if it + /// does not close any previous "{" that is treated as a special character. + /// This was added in for backward compatibility and is not recommended + /// as it may mask errors. + final bool relaxSyntax; } /// Parse the localizations configuration options from [file]. @@ -498,6 +510,7 @@ LocalizationOptions parseLocalizationsOptionsFromYAML({ format: _tryReadBool(yamlNode, 'format', logger), useEscaping: _tryReadBool(yamlNode, 'use-escaping', logger), suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings', logger), + relaxSyntax: _tryReadBool(yamlNode, 'relax-syntax', logger), ); } diff --git a/packages/flutter_tools/lib/src/localizations/message_parser.dart b/packages/flutter_tools/lib/src/localizations/message_parser.dart index 49a04fff4858d..6591d9da23660 100644 --- a/packages/flutter_tools/lib/src/localizations/message_parser.dart +++ b/packages/flutter_tools/lib/src/localizations/message_parser.dart @@ -198,7 +198,8 @@ class Parser { this.messageString, { this.useEscaping = false, - this.logger + this.logger, + this.placeholders, } ); @@ -207,6 +208,7 @@ class Parser { final String filename; final bool useEscaping; final Logger? logger; + final List<String>? placeholders; static String indentForError(int position) { return '${List<String>.filled(position, ' ').join()}^'; @@ -216,12 +218,16 @@ class Parser { // every instance of "{" and "}" toggles the isString boolean and every // instance of "'" toggles the isEscaped boolean (and treats a double // single quote "''" as a single quote "'"). When !isString and !isEscaped - // delimit tokens by whitespace and special characters. + // delimit tokens by whitespace and special characters. When placeholders + // is passed, relax the syntax so that "{" and "}" can be used as strings in + // certain cases. List<Node> lexIntoTokens() { + final bool useRelaxedLexer = placeholders != null; final List<Node> tokens = <Node>[]; bool isString = true; // Index specifying where to match from int startIndex = 0; + int depth = 0; // At every iteration, we should be able to match a new token until we // reach the end of the string. If for some reason we don't match a @@ -267,9 +273,28 @@ class Parser { } match = brace.matchAsPrefix(messageString, startIndex); if (match != null) { + final String matchedBrace = match.group(0)!; + if (useRelaxedLexer) { + final Match? whitespaceMatch = whitespace.matchAsPrefix(messageString, match.end); + final int endOfWhitespace = whitespaceMatch?.group(0) == null ? match.end : whitespaceMatch!.end; + final Match? identifierMatch = alphanumeric.matchAsPrefix(messageString, endOfWhitespace); + // If we match a "}" and the depth is 0, treat it as a string. + // If we match a "{" and the next token is not a valid placeholder, treat it as a string. + if (matchedBrace == '}' && depth == 0) { + tokens.add(Node.string(startIndex, matchedBrace)); + startIndex = match.end; + continue; + } + if (matchedBrace == '{' && (identifierMatch == null || !placeholders!.contains(identifierMatch.group(0)))) { + tokens.add(Node.string(startIndex, matchedBrace)); + startIndex = match.end; + continue; + } + } tokens.add(Node.brace(startIndex, match.group(0)!)); isString = false; startIndex = match.end; + depth += 1; continue; } // Theoretically, we only reach this point because of unmatched single quotes because @@ -299,9 +324,15 @@ class Parser { if (match == null) { match = brace.matchAsPrefix(messageString, startIndex); if (match != null) { - tokens.add(Node.brace(startIndex, match.group(0)!)); + final String matchedBrace = match.group(0)!; + tokens.add(Node.brace(startIndex, matchedBrace)); isString = true; startIndex = match.end; + if (matchedBrace == '{') { + depth += 1; + } else { + depth -= 1; + } continue; } // This should only happen when there are special characters we are unable to match. diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index e675b50c5efa8..c4e9b86e41ade 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -48,6 +48,8 @@ const String outOfDatePluginsPodfileConsequence = ''' const String cocoaPodsInstallInstructions = 'see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.'; +const String cocoaPodsUpdateInstructions = 'see https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods for instructions.'; + const String podfileIosMigrationInstructions = ''' rm ios/Podfile'''; diff --git a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart index 56b16dbfd5b19..93fccd62e5abb 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart @@ -29,31 +29,28 @@ class CocoaPodsValidator extends DoctorValidator { .evaluateCocoaPodsInstallation; ValidationType status = ValidationType.success; - if (cocoaPodsStatus == CocoaPodsStatus.recommended) { - messages.add(ValidationMessage(_userMessages.cocoaPodsVersion((await _cocoaPods.cocoaPodsVersionText).toString()))); - } else { - if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) { + switch (cocoaPodsStatus) { + case CocoaPodsStatus.recommended: + messages.add(ValidationMessage(_userMessages.cocoaPodsVersion((await _cocoaPods.cocoaPodsVersionText).toString()))); + case CocoaPodsStatus.notInstalled: status = ValidationType.missing; messages.add(ValidationMessage.error( _userMessages.cocoaPodsMissing(noCocoaPodsConsequence, cocoaPodsInstallInstructions))); - - } else if (cocoaPodsStatus == CocoaPodsStatus.brokenInstall) { + case CocoaPodsStatus.brokenInstall: status = ValidationType.missing; messages.add(ValidationMessage.error( _userMessages.cocoaPodsBrokenInstall(brokenCocoaPodsConsequence, cocoaPodsInstallInstructions))); - - } else if (cocoaPodsStatus == CocoaPodsStatus.unknownVersion) { + case CocoaPodsStatus.unknownVersion: status = ValidationType.partial; messages.add(ValidationMessage.hint( _userMessages.cocoaPodsUnknownVersion(unknownCocoaPodsConsequence, cocoaPodsInstallInstructions))); - } else { + case CocoaPodsStatus.belowMinimumVersion: + case CocoaPodsStatus.belowRecommendedVersion: status = ValidationType.partial; final String currentVersionText = (await _cocoaPods.cocoaPodsVersionText).toString(); messages.add(ValidationMessage.hint( - _userMessages.cocoaPodsOutdated(currentVersionText, cocoaPodsRecommendedVersion.toString(), noCocoaPodsConsequence, cocoaPodsInstallInstructions))); - } + _userMessages.cocoaPodsOutdated(currentVersionText, cocoaPodsRecommendedVersion.toString(), noCocoaPodsConsequence, cocoaPodsUpdateInstructions))); } - return ValidationResult(status, messages); } } diff --git a/packages/flutter_tools/lib/src/macos/macos_device.dart b/packages/flutter_tools/lib/src/macos/macos_device.dart index 478c8ac27c125..43ada0af083c1 100644 --- a/packages/flutter_tools/lib/src/macos/macos_device.dart +++ b/packages/flutter_tools/lib/src/macos/macos_device.dart @@ -47,9 +47,6 @@ class MacOSDevice extends DesktopDevice { @override String get name => 'macOS'; - @override - bool get supportsImpeller => true; - @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.darwin; diff --git a/packages/flutter_tools/lib/src/macos/native_assets.dart b/packages/flutter_tools/lib/src/macos/native_assets.dart new file mode 100644 index 0000000000000..5e79097f2ca03 --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/native_assets.dart @@ -0,0 +1,162 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_builder/native_assets_builder.dart' show BuildResult; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; +import '../native_assets.dart'; +import 'native_assets_host.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file and +/// the Xcode project. +Future<Uri?> dryRunNativeAssetsMacOS({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + bool flutterTester = false, + required FileSystem fileSystem, +}) async { + if (!await nativeBuildRequired(buildRunner)) { + return null; + } + + final Uri buildUri = nativeAssetsBuildUri(projectUri, OS.macOS); + final Iterable<Asset> nativeAssetPaths = await dryRunNativeAssetsMacOSInternal(fileSystem, projectUri, flutterTester, buildRunner); + final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri, fileSystem); + return nativeAssetsUri; +} + +Future<Iterable<Asset>> dryRunNativeAssetsMacOSInternal( + FileSystem fileSystem, + Uri projectUri, + bool flutterTester, + NativeAssetsBuildRunner buildRunner, +) async { + const OS targetOS = OS.macOS; + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + + globals.logger.printTrace('Dry running native assets for $targetOS.'); + final List<Asset> nativeAssets = (await buildRunner.dryRun( + linkModePreference: LinkModePreference.dynamic, + targetOS: targetOS, + workingDirectory: projectUri, + includeParentEnvironment: true, + )) + .assets; + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Dry running native assets for $targetOS done.'); + final Uri? absolutePath = flutterTester ? buildUri : null; + final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath); + final Iterable<Asset> nativeAssetPaths = assetTargetLocations.values; + return nativeAssetPaths; +} + +/// Builds native assets. +/// +/// If [darwinArchs] is omitted, the current target architecture is used. +/// +/// If [flutterTester] is true, absolute paths are emitted in the native +/// assets mapping. This can be used for JIT mode without sandbox on the host. +/// This is used in `flutter test` and `flutter run -d flutter-tester`. +Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> buildNativeAssetsMacOS({ + required NativeAssetsBuildRunner buildRunner, + List<DarwinArch>? darwinArchs, + required Uri projectUri, + required BuildMode buildMode, + bool flutterTester = false, + String? codesignIdentity, + Uri? yamlParentDirectory, + required FileSystem fileSystem, +}) async { + const OS targetOS = OS.macOS; + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + if (!await nativeBuildRequired(buildRunner)) { + final Uri nativeAssetsYaml = await writeNativeAssetsYaml(<Asset>[], yamlParentDirectory ?? buildUri, fileSystem); + return (nativeAssetsYaml, <Uri>[]); + } + + final List<Target> targets = darwinArchs != null ? darwinArchs.map(_getNativeTarget).toList() : <Target>[Target.current]; + final native_assets_cli.BuildMode buildModeCli = nativeAssetsBuildMode(buildMode); + + globals.logger.printTrace('Building native assets for $targets $buildModeCli.'); + final List<Asset> nativeAssets = <Asset>[]; + final Set<Uri> dependencies = <Uri>{}; + for (final Target target in targets) { + final BuildResult result = await buildRunner.build( + linkModePreference: LinkModePreference.dynamic, + target: target, + buildMode: buildModeCli, + workingDirectory: projectUri, + includeParentEnvironment: true, + cCompilerConfig: await buildRunner.cCompilerConfig, + ); + nativeAssets.addAll(result.assets); + dependencies.addAll(result.dependencies); + } + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Building native assets for $targets done.'); + final Uri? absolutePath = flutterTester ? buildUri : null; + final Map<Asset, Asset> assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath); + final Map<AssetPath, List<Asset>> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets, absolutePath); + await copyNativeAssetsMacOSHost(buildUri, fatAssetTargetLocations, codesignIdentity, buildMode, fileSystem); + final Uri nativeAssetsUri = await writeNativeAssetsYaml(assetTargetLocations.values, yamlParentDirectory ?? buildUri, fileSystem); + return (nativeAssetsUri, dependencies.toList()); +} + +/// Extract the [Target] from a [DarwinArch]. +Target _getNativeTarget(DarwinArch darwinArch) { + switch (darwinArch) { + case DarwinArch.arm64: + return Target.macOSArm64; + case DarwinArch.x86_64: + return Target.macOSX64; + case DarwinArch.armv7: + throw Exception('Unknown DarwinArch: $darwinArch.'); + } +} + +Map<AssetPath, List<Asset>> _fatAssetTargetLocations(List<Asset> nativeAssets, Uri? absolutePath) { + final Map<AssetPath, List<Asset>> result = <AssetPath, List<Asset>>{}; + for (final Asset asset in nativeAssets) { + final AssetPath path = _targetLocationMacOS(asset, absolutePath).path; + result[path] ??= <Asset>[]; + result[path]!.add(asset); + } + return result; +} + +Map<Asset, Asset> _assetTargetLocations(List<Asset> nativeAssets, Uri? absolutePath) => <Asset, Asset>{ + for (final Asset asset in nativeAssets) + asset: _targetLocationMacOS(asset, absolutePath), +}; + +Asset _targetLocationMacOS(Asset asset, Uri? absolutePath) { + final AssetPath path = asset.path; + switch (path) { + case AssetSystemPath _: + case AssetInExecutable _: + case AssetInProcess _: + return asset; + case AssetAbsolutePath _: + final String fileName = path.uri.pathSegments.last; + Uri uri; + if (absolutePath != null) { + // Flutter tester needs full host paths. + uri = absolutePath.resolve(fileName); + } else { + // Flutter Desktop needs "absolute" paths inside the app. + // "relative" in the context of native assets would be relative to the + // kernel or aot snapshot. + uri = Uri(path: fileName); + } + return asset.copyWith(path: AssetAbsolutePath(uri)); + } + throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset'); +} diff --git a/packages/flutter_tools/lib/src/macos/native_assets_host.dart b/packages/flutter_tools/lib/src/macos/native_assets_host.dart new file mode 100644 index 0000000000000..107ac9045c18f --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/native_assets_host.dart @@ -0,0 +1,141 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Shared logic between iOS and macOS implementations of native assets. + +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; + +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../build_info.dart'; +import '../convert.dart'; +import '../globals.dart' as globals; + +/// The target location for native assets on macOS. +/// +/// Because we need to have a multi-architecture solution for +/// `flutter run --release`, we use `lipo` to combine all target architectures +/// into a single file. +/// +/// We need to set the install name so that it matches what the place it will +/// be bundled in the final app. +/// +/// Code signing is also done here, so that we don't have to worry about it +/// in xcode_backend.dart and macos_assemble.sh. +Future<void> copyNativeAssetsMacOSHost( + Uri buildUri, + Map<AssetPath, List<Asset>> assetTargetLocations, + String? codesignIdentity, + BuildMode buildMode, + FileSystem fileSystem, +) async { + if (assetTargetLocations.isNotEmpty) { + globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.'); + final Directory buildDir = fileSystem.directory(buildUri.toFilePath()); + if (!buildDir.existsSync()) { + buildDir.createSync(recursive: true); + } + for (final MapEntry<AssetPath, List<Asset>> assetMapping in assetTargetLocations.entries) { + final Uri target = (assetMapping.key as AssetAbsolutePath).uri; + final List<Uri> sources = <Uri>[for (final Asset source in assetMapping.value) (source.path as AssetAbsolutePath).uri]; + final Uri targetUri = buildUri.resolveUri(target); + final String targetFullPath = targetUri.toFilePath(); + await lipoDylibs(targetFullPath, sources); + await setInstallNameDylib(targetUri); + await codesignDylib(codesignIdentity, buildMode, targetFullPath); + } + globals.logger.printTrace('Copying native assets done.'); + } +} + +/// Combines dylibs from [sources] into a fat binary at [targetFullPath]. +/// +/// The dylibs must have different architectures. E.g. a dylib targeting +/// arm64 ios simulator cannot be combined with a dylib targeting arm64 +/// ios device or macos arm64. +Future<void> lipoDylibs(String targetFullPath, List<Uri> sources) async { + final ProcessResult lipoResult = await globals.processManager.run( + <String>[ + 'lipo', + '-create', + '-output', + targetFullPath, + for (final Uri source in sources) source.toFilePath(), + ], + ); + if (lipoResult.exitCode != 0) { + throwToolExit('Failed to create universal binary:\n${lipoResult.stderr}'); + } + globals.logger.printTrace(lipoResult.stdout as String); + globals.logger.printTrace(lipoResult.stderr as String); +} + +/// Sets the install name in a dylib with a Mach-O format. +/// +/// On macOS and iOS, opening a dylib at runtime fails if the path inside the +/// dylib itself does not correspond to the path that the file is at. Therefore, +/// native assets copied into their final location also need their install name +/// updated with the `install_name_tool`. +Future<void> setInstallNameDylib(Uri targetUri) async { + final String fileName = targetUri.pathSegments.last; + final ProcessResult installNameResult = await globals.processManager.run( + <String>[ + 'install_name_tool', + '-id', + '@executable_path/Frameworks/$fileName', + targetUri.toFilePath(), + ], + ); + if (installNameResult.exitCode != 0) { + throwToolExit('Failed to change the install name of $targetUri:\n${installNameResult.stderr}'); + } +} + +Future<void> codesignDylib( + String? codesignIdentity, + BuildMode buildMode, + String targetFullPath, +) async { + if (codesignIdentity == null || codesignIdentity.isEmpty) { + codesignIdentity = '-'; + } + final List<String> codesignCommand = <String>[ + 'codesign', + '--force', + '--sign', + codesignIdentity, + if (buildMode != BuildMode.release) ...<String>[ + // Mimic Xcode's timestamp codesigning behavior on non-release binaries. + '--timestamp=none', + ], + targetFullPath, + ]; + globals.logger.printTrace(codesignCommand.join(' ')); + final ProcessResult codesignResult = await globals.processManager.run(codesignCommand); + if (codesignResult.exitCode != 0) { + throwToolExit('Failed to code sign binary:\n${codesignResult.stderr}'); + } + globals.logger.printTrace(codesignResult.stdout as String); + globals.logger.printTrace(codesignResult.stderr as String); +} + +/// Flutter expects `xcrun` to be on the path on macOS hosts. +/// +/// Use the `clang`, `ar`, and `ld` that would be used if run with `xcrun`. +Future<CCompilerConfig> cCompilerConfigMacOS() async { + final ProcessResult xcrunResult = await globals.processManager.run(<String>['xcrun', 'clang', '--version']); + if (xcrunResult.exitCode != 0) { + throwToolExit('Failed to find clang with xcrun:\n${xcrunResult.stderr}'); + } + final String installPath = LineSplitter.split(xcrunResult.stdout as String) + .firstWhere((String s) => s.startsWith('InstalledDir: ')) + .split(' ') + .last; + return CCompilerConfig( + cc: Uri.file('$installPath/clang'), + ar: Uri.file('$installPath/ar'), + ld: Uri.file('$installPath/ld'), + ); +} diff --git a/packages/flutter_tools/lib/src/migrations/cmake_native_assets_migration.dart b/packages/flutter_tools/lib/src/migrations/cmake_native_assets_migration.dart new file mode 100644 index 0000000000000..c7f21351afbbd --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/cmake_native_assets_migration.dart @@ -0,0 +1,65 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../base/file_system.dart'; +import '../base/project_migrator.dart'; +import '../cmake_project.dart'; + +/// Adds the snippet to the CMake file that copies the native assets. +/// +/// ```cmake +/// # Copy the native assets provided by the build.dart from all packages. +/// set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +/// install(DIRECTORY "${NATIVE_ASSETS_DIR}" +/// DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" +/// COMPONENT Runtime) +/// ``` +class CmakeNativeAssetsMigration extends ProjectMigrator { + CmakeNativeAssetsMigration(CmakeBasedProject project, this.os, super.logger) + : _cmakeFile = project.managedCmakeFile; + + final File _cmakeFile; + final String os; + + @override + void migrate() { + if (!_cmakeFile.existsSync()) { + logger.printTrace('CMake project not found, skipping install() NATIVE_ASSETS_DIR migration.'); + return; + } + + final String originalProjectContents = _cmakeFile.readAsStringSync(); + + if (originalProjectContents.contains('set(NATIVE_ASSETS_DIR')) { + // Command is already present. + return; + } + + final String copyNativeAssetsCommand = ''' + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "\${PROJECT_BUILD_DIR}native_assets/$os/") +install(DIRECTORY "\${NATIVE_ASSETS_DIR}" + DESTINATION "\${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +'''; + + // Insert the new command after the bundled libraries loop. + const String bundleLibrariesCommandEnd = r''' +endforeach(bundled_library) +'''; + + String newProjectContents = originalProjectContents; + + newProjectContents = originalProjectContents.replaceFirst( + bundleLibrariesCommandEnd, + '$bundleLibrariesCommandEnd$copyNativeAssetsCommand', + ); + + if (originalProjectContents != newProjectContents) { + logger.printStatus('CMake missing install() NATIVE_ASSETS_DIR command, updating.'); + _cmakeFile.writeAsStringSync(newProjectContents); + } + } +} diff --git a/packages/flutter_tools/lib/src/native_assets.dart b/packages/flutter_tools/lib/src/native_assets.dart new file mode 100644 index 0000000000000..38afb4e6e60d8 --- /dev/null +++ b/packages/flutter_tools/lib/src/native_assets.dart @@ -0,0 +1,657 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Logic for native assets shared between all host OSes. + +import 'package:logging/logging.dart' as logging; +import 'package:native_assets_builder/native_assets_builder.dart' hide NativeAssetsBuildRunner; +import 'package:native_assets_builder/native_assets_builder.dart' as native_assets_builder show NativeAssetsBuildRunner; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:package_config/package_config_types.dart'; + +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/logger.dart'; +import 'base/platform.dart'; +import 'build_info.dart' as build_info; +import 'cache.dart'; +import 'features.dart'; +import 'globals.dart' as globals; +import 'ios/native_assets.dart'; +import 'linux/native_assets.dart'; +import 'macos/native_assets.dart'; +import 'macos/native_assets_host.dart'; +import 'resident_runner.dart'; +import 'windows/native_assets.dart'; + +/// Programmatic API to be used by Dart launchers to invoke native builds. +/// +/// It enables mocking `package:native_assets_builder` package. +/// It also enables mocking native toolchain discovery via [cCompilerConfig]. +abstract class NativeAssetsBuildRunner { + /// Whether the project has a `.dart_tools/package_config.json`. + /// + /// If there is no package config, [packagesWithNativeAssets], [build], and + /// [dryRun] must not be invoked. + Future<bool> hasPackageConfig(); + + /// All packages in the transitive dependencies that have a `build.dart`. + Future<List<Package>> packagesWithNativeAssets(); + + /// Runs all [packagesWithNativeAssets] `build.dart` in dry run. + Future<DryRunResult> dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOS, + required Uri workingDirectory, + }); + + /// Runs all [packagesWithNativeAssets] `build.dart`. + Future<BuildResult> build({ + required bool includeParentEnvironment, + required BuildMode buildMode, + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + CCompilerConfig? cCompilerConfig, + int? targetAndroidNdkApi, + IOSSdk? targetIOSSdk, + }); + + /// The C compiler config to use for compilation. + Future<CCompilerConfig> get cCompilerConfig; +} + +/// Uses `package:native_assets_builder` for its implementation. +class NativeAssetsBuildRunnerImpl implements NativeAssetsBuildRunner { + NativeAssetsBuildRunnerImpl( + this.projectUri, + this.packageConfig, + this.fileSystem, + this.logger, + ); + + final Uri projectUri; + final PackageConfig packageConfig; + final FileSystem fileSystem; + final Logger logger; + + late final logging.Logger _logger = logging.Logger('') + ..onRecord.listen((logging.LogRecord record) { + final int levelValue = record.level.value; + final String message = record.message; + if (levelValue >= logging.Level.SEVERE.value) { + logger.printError(message); + } else if (levelValue >= logging.Level.WARNING.value) { + logger.printWarning(message); + } else if (levelValue >= logging.Level.INFO.value) { + logger.printTrace(message); + } else { + logger.printTrace(message); + } + }); + + late final Uri _dartExecutable = fileSystem.directory(Cache.flutterRoot).uri.resolve('bin/dart'); + + late final native_assets_builder.NativeAssetsBuildRunner _buildRunner = native_assets_builder.NativeAssetsBuildRunner( + logger: _logger, + dartExecutable: _dartExecutable, + ); + + @override + Future<bool> hasPackageConfig() { + final File packageConfigJson = + fileSystem.directory(projectUri.toFilePath()).childDirectory('.dart_tool').childFile('package_config.json'); + return packageConfigJson.exists(); + } + + @override + Future<List<Package>> packagesWithNativeAssets() async { + final PackageLayout packageLayout = PackageLayout.fromPackageConfig( + packageConfig, + projectUri.resolve('.dart_tool/package_config.json'), + ); + return packageLayout.packagesWithNativeAssets; + } + + @override + Future<DryRunResult> dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOS, + required Uri workingDirectory, + }) { + final PackageLayout packageLayout = PackageLayout.fromPackageConfig( + packageConfig, + projectUri.resolve('.dart_tool/package_config.json'), + ); + return _buildRunner.dryRun( + includeParentEnvironment: includeParentEnvironment, + linkModePreference: linkModePreference, + targetOs: targetOS, + workingDirectory: workingDirectory, + packageLayout: packageLayout, + ); + } + + @override + Future<BuildResult> build({ + required bool includeParentEnvironment, + required BuildMode buildMode, + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + CCompilerConfig? cCompilerConfig, + int? targetAndroidNdkApi, + IOSSdk? targetIOSSdk, + }) { + final PackageLayout packageLayout = PackageLayout.fromPackageConfig( + packageConfig, + projectUri.resolve('.dart_tool/package_config.json'), + ); + return _buildRunner.build( + buildMode: buildMode, + cCompilerConfig: cCompilerConfig, + includeParentEnvironment: includeParentEnvironment, + linkModePreference: linkModePreference, + target: target, + targetAndroidNdkApi: targetAndroidNdkApi, + targetIOSSdk: targetIOSSdk, + workingDirectory: workingDirectory, + packageLayout: packageLayout, + ); + } + + @override + late final Future<CCompilerConfig> cCompilerConfig = () { + if (globals.platform.isMacOS || globals.platform.isIOS) { + return cCompilerConfigMacOS(); + } + if (globals.platform.isLinux) { + return cCompilerConfigLinux(); + } + if (globals.platform.isWindows) { + return cCompilerConfigWindows(); + } + throwToolExit( + 'Native assets feature not yet implemented for Android.', + ); + }(); +} + +/// Write [assets] to `native_assets.yaml` in [yamlParentDirectory]. +Future<Uri> writeNativeAssetsYaml( + Iterable<Asset> assets, + Uri yamlParentDirectory, + FileSystem fileSystem, +) async { + globals.logger.printTrace('Writing native_assets.yaml.'); + final String nativeAssetsDartContents = assets.toNativeAssetsFile(); + final Directory parentDirectory = fileSystem.directory(yamlParentDirectory); + if (!await parentDirectory.exists()) { + await parentDirectory.create(recursive: true); + } + final File nativeAssetsFile = parentDirectory.childFile('native_assets.yaml'); + await nativeAssetsFile.writeAsString(nativeAssetsDartContents); + globals.logger.printTrace('Writing ${nativeAssetsFile.path} done.'); + return nativeAssetsFile.uri; +} + +/// Select the native asset build mode for a given Flutter build mode. +BuildMode nativeAssetsBuildMode(build_info.BuildMode buildMode) { + switch (buildMode) { + case build_info.BuildMode.debug: + return BuildMode.debug; + case build_info.BuildMode.jitRelease: + case build_info.BuildMode.profile: + case build_info.BuildMode.release: + return BuildMode.release; + } +} + +/// Checks whether this project does not yet have a package config file. +/// +/// A project has no package config when `pub get` has not yet been run. +/// +/// Native asset builds cannot be run without a package config. If there is +/// no package config, leave a logging trace about that. +Future<bool> _hasNoPackageConfig(NativeAssetsBuildRunner buildRunner) async { + final bool packageConfigExists = await buildRunner.hasPackageConfig(); + if (!packageConfigExists) { + globals.logger.printTrace('No package config found. Skipping native assets compilation.'); + } + return !packageConfigExists; +} + +Future<bool> nativeBuildRequired(NativeAssetsBuildRunner buildRunner) async { + if (await _hasNoPackageConfig(buildRunner)) { + return false; + } + final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets(); + if (packagesWithNativeAssets.isEmpty) { + return false; + } + + if (!featureFlags.isNativeAssetsEnabled) { + final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' '); + throwToolExit( + 'Package(s) $packageNames require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ); + } + return true; +} + +/// Ensures that either this project has no native assets, or that native assets +/// are supported on that operating system. +/// +/// Exits the tool if the above condition is not satisfied. +Future<void> ensureNoNativeAssetsOrOsIsSupported( + Uri workingDirectory, + String os, + FileSystem fileSystem, + NativeAssetsBuildRunner buildRunner, +) async { + if (await _hasNoPackageConfig(buildRunner)) { + return; + } + final List<Package> packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets(); + if (packagesWithNativeAssets.isEmpty) { + return; + } + final String packageNames = packagesWithNativeAssets.map((Package p) => p.name).join(' '); + throwToolExit( + 'Package(s) $packageNames require the native assets feature. ' + 'This feature has not yet been implemented for `$os`. ' + 'For more info see https://github.com/flutter/flutter/issues/129757.', + ); +} + +/// Ensure all native assets have a linkmode declared to be dynamic loading. +/// +/// In JIT, the link mode must always be dynamic linking. +/// In AOT, the static linking has not yet been implemented in Dart: +/// https://github.com/dart-lang/sdk/issues/49418. +/// +/// Therefore, ensure all `build.dart` scripts return only dynamic libraries. +void ensureNoLinkModeStatic(List<Asset> nativeAssets) { + final Iterable<Asset> staticAssets = nativeAssets.whereLinkMode(LinkMode.static); + if (staticAssets.isNotEmpty) { + final String assetIds = staticAssets.map((Asset a) => a.id).toSet().join(', '); + throwToolExit( + 'Native asset(s) $assetIds have their link mode set to static, ' + 'but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ); + } +} + +/// This should be the same for different archs, debug/release, etc. +/// It should work for all macOS. +Uri nativeAssetsBuildUri(Uri projectUri, OS os) { + final String buildDir = build_info.getBuildDirectory(); + return projectUri.resolve('$buildDir/native_assets/$os/'); +} + +/// Gets the native asset id to dylib mapping to embed in the kernel file. +/// +/// Run hot compiles a kernel file that is pushed to the device after hot +/// restart. We need to embed the native assets mapping in order to access +/// native assets after hot restart. +Future<Uri?> dryRunNativeAssets({ + required Uri projectUri, + required FileSystem fileSystem, + required NativeAssetsBuildRunner buildRunner, + required List<FlutterDevice> flutterDevices, +}) async { + if (flutterDevices.length != 1) { + return dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: flutterDevices.map((FlutterDevice d) => d.targetPlatform).nonNulls, + buildRunner: buildRunner, + ); + } + final FlutterDevice flutterDevice = flutterDevices.single; + final build_info.TargetPlatform targetPlatform = flutterDevice.targetPlatform!; + + final Uri? nativeAssetsYaml; + switch (targetPlatform) { + case build_info.TargetPlatform.darwin: + nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.ios: + nativeAssetsYaml = await dryRunNativeAssetsIOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.tester: + if (const LocalPlatform().isMacOS) { + nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + flutterTester: true, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + } else if (const LocalPlatform().isLinux) { + nativeAssetsYaml = await dryRunNativeAssetsLinux( + projectUri: projectUri, + flutterTester: true, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + } else if (const LocalPlatform().isWindows) { + nativeAssetsYaml = await dryRunNativeAssetsWindows( + projectUri: projectUri, + flutterTester: true, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + } else { + await nativeBuildRequired(buildRunner); + nativeAssetsYaml = null; + } + case build_info.TargetPlatform.linux_arm64: + case build_info.TargetPlatform.linux_x64: + nativeAssetsYaml = await dryRunNativeAssetsLinux( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.windows_x64: + nativeAssetsYaml = await dryRunNativeAssetsWindows( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: buildRunner, + ); + case build_info.TargetPlatform.android_arm: + case build_info.TargetPlatform.android_arm64: + case build_info.TargetPlatform.android_x64: + case build_info.TargetPlatform.android_x86: + case build_info.TargetPlatform.android: + case build_info.TargetPlatform.fuchsia_arm64: + case build_info.TargetPlatform.fuchsia_x64: + case build_info.TargetPlatform.web_javascript: + await ensureNoNativeAssetsOrOsIsSupported( + projectUri, + targetPlatform.toString(), + fileSystem, + buildRunner, + ); + nativeAssetsYaml = null; + } + return nativeAssetsYaml; +} + +/// Dry run the native builds for multiple OSes. +/// +/// Needed for `flutter run -d all`. +Future<Uri?> dryRunNativeAssetsMultipeOSes({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + required FileSystem fileSystem, + required Iterable<build_info.TargetPlatform> targetPlatforms, +}) async { + if (await nativeBuildRequired(buildRunner)) { + return null; + } + + final Uri buildUri = buildUriMultiple(projectUri); + final Iterable<Asset> nativeAssetPaths = <Asset>[ + if (targetPlatforms.contains(build_info.TargetPlatform.darwin) || + (targetPlatforms.contains(build_info.TargetPlatform.tester) && OS.current == OS.macOS)) + ...await dryRunNativeAssetsMacOSInternal( + fileSystem, + projectUri, + false, + buildRunner, + ), + if (targetPlatforms.contains(build_info.TargetPlatform.linux_arm64) || + targetPlatforms.contains(build_info.TargetPlatform.linux_x64) || + (targetPlatforms.contains(build_info.TargetPlatform.tester) && OS.current == OS.linux)) + ...await dryRunNativeAssetsLinuxInternal( + fileSystem, + projectUri, + false, + buildRunner, + ), + if (targetPlatforms.contains(build_info.TargetPlatform.windows_x64) || + (targetPlatforms.contains(build_info.TargetPlatform.tester) && OS.current == OS.windows)) + ...await dryRunNativeAssetsWindowsInternal( + fileSystem, + projectUri, + false, + buildRunner, + ), + if (targetPlatforms.contains(build_info.TargetPlatform.ios)) + ...await dryRunNativeAssetsIOSInternal( + fileSystem, + projectUri, + buildRunner, + ) + ]; + final Uri nativeAssetsUri = await writeNativeAssetsYaml(nativeAssetPaths, buildUri, fileSystem); + return nativeAssetsUri; +} + +/// With `flutter run -d all` we need a place to store the native assets +/// mapping for multiple OSes combined. +Uri buildUriMultiple(Uri projectUri) { + final String buildDir = build_info.getBuildDirectory(); + return projectUri.resolve('$buildDir/native_assets/multiple/'); +} + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file. +Future<Uri?> dryRunNativeAssetsSingleArchitecture({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + bool flutterTester = false, + required FileSystem fileSystem, + required OS os, +}) async { + if (!await nativeBuildRequired(buildRunner)) { + return null; + } + + final Uri buildUri = nativeAssetsBuildUri(projectUri, os); + final Iterable<Asset> nativeAssetPaths = await dryRunNativeAssetsSingleArchitectureInternal( + fileSystem, + projectUri, + flutterTester, + buildRunner, + os, + ); + final Uri nativeAssetsUri = await writeNativeAssetsYaml( + nativeAssetPaths, + buildUri, + fileSystem, + ); + return nativeAssetsUri; +} + +Future<Iterable<Asset>> dryRunNativeAssetsSingleArchitectureInternal( + FileSystem fileSystem, + Uri projectUri, + bool flutterTester, + NativeAssetsBuildRunner buildRunner, + OS targetOS, +) async { + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + + globals.logger.printTrace('Dry running native assets for $targetOS.'); + final List<Asset> nativeAssets = (await buildRunner.dryRun( + linkModePreference: LinkModePreference.dynamic, + targetOS: targetOS, + workingDirectory: projectUri, + includeParentEnvironment: true, + )) + .assets; + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Dry running native assets for $targetOS done.'); + final Uri? absolutePath = flutterTester ? buildUri : null; + final Map<Asset, Asset> assetTargetLocations = _assetTargetLocationsSingleArchitecture( + nativeAssets, + absolutePath, + ); + final Iterable<Asset> nativeAssetPaths = assetTargetLocations.values; + return nativeAssetPaths; +} + +/// Builds native assets. +/// +/// If [targetPlatform] is omitted, the current target architecture is used. +/// +/// If [flutterTester] is true, absolute paths are emitted in the native +/// assets mapping. This can be used for JIT mode without sandbox on the host. +/// This is used in `flutter test` and `flutter run -d flutter-tester`. +Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> buildNativeAssetsSingleArchitecture({ + required NativeAssetsBuildRunner buildRunner, + build_info.TargetPlatform? targetPlatform, + required Uri projectUri, + required build_info.BuildMode buildMode, + bool flutterTester = false, + Uri? yamlParentDirectory, + required FileSystem fileSystem, +}) async { + final Target target = targetPlatform != null ? _getNativeTarget(targetPlatform) : Target.current; + final OS targetOS = target.os; + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + final Directory buildDir = fileSystem.directory(buildUri); + if (!await buildDir.exists()) { + // CMake requires the folder to exist to do copying. + await buildDir.create(recursive: true); + } + if (!await nativeBuildRequired(buildRunner)) { + final Uri nativeAssetsYaml = await writeNativeAssetsYaml( + <Asset>[], + yamlParentDirectory ?? buildUri, + fileSystem, + ); + return (nativeAssetsYaml, <Uri>[]); + } + + final BuildMode buildModeCli = nativeAssetsBuildMode(buildMode); + + globals.logger.printTrace('Building native assets for $target $buildModeCli.'); + final BuildResult result = await buildRunner.build( + linkModePreference: LinkModePreference.dynamic, + target: target, + buildMode: buildModeCli, + workingDirectory: projectUri, + includeParentEnvironment: true, + cCompilerConfig: await buildRunner.cCompilerConfig, + ); + final List<Asset> nativeAssets = result.assets; + final Set<Uri> dependencies = result.dependencies.toSet(); + ensureNoLinkModeStatic(nativeAssets); + globals.logger.printTrace('Building native assets for $target done.'); + final Uri? absolutePath = flutterTester ? buildUri : null; + final Map<Asset, Asset> assetTargetLocations = _assetTargetLocationsSingleArchitecture(nativeAssets, absolutePath); + await _copyNativeAssetsSingleArchitecture( + buildUri, + assetTargetLocations, + buildMode, + fileSystem, + ); + final Uri nativeAssetsUri = await writeNativeAssetsYaml( + assetTargetLocations.values, + yamlParentDirectory ?? buildUri, + fileSystem, + ); + return (nativeAssetsUri, dependencies.toList()); +} + +Map<Asset, Asset> _assetTargetLocationsSingleArchitecture( + List<Asset> nativeAssets, + Uri? absolutePath, +) { + return <Asset, Asset>{ + for (final Asset asset in nativeAssets) + asset: _targetLocationSingleArchitecture( + asset, + absolutePath, + ), + }; +} + +Asset _targetLocationSingleArchitecture(Asset asset, Uri? absolutePath) { + final AssetPath path = asset.path; + switch (path) { + case AssetSystemPath _: + case AssetInExecutable _: + case AssetInProcess _: + return asset; + case AssetAbsolutePath _: + final String fileName = path.uri.pathSegments.last; + Uri uri; + if (absolutePath != null) { + // Flutter tester needs full host paths. + uri = absolutePath.resolve(fileName); + } else { + // Flutter Desktop needs "absolute" paths inside the app. + // "relative" in the context of native assets would be relative to the + // kernel or aot snapshot. + uri = Uri(path: fileName); + } + return asset.copyWith(path: AssetAbsolutePath(uri)); + } + throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset'); +} + +/// Extract the [Target] from a [TargetPlatform]. +/// +/// Does not cover MacOS, iOS, and Android as these pass the architecture +/// in other enums. +Target _getNativeTarget(build_info.TargetPlatform targetPlatform) { + switch (targetPlatform) { + case build_info.TargetPlatform.linux_x64: + return Target.linuxX64; + case build_info.TargetPlatform.linux_arm64: + return Target.linuxArm64; + case build_info.TargetPlatform.windows_x64: + return Target.windowsX64; + case build_info.TargetPlatform.android: + case build_info.TargetPlatform.ios: + case build_info.TargetPlatform.darwin: + case build_info.TargetPlatform.fuchsia_arm64: + case build_info.TargetPlatform.fuchsia_x64: + case build_info.TargetPlatform.tester: + case build_info.TargetPlatform.web_javascript: + case build_info.TargetPlatform.android_arm: + case build_info.TargetPlatform.android_arm64: + case build_info.TargetPlatform.android_x64: + case build_info.TargetPlatform.android_x86: + throw Exception('Unknown targetPlatform: $targetPlatform.'); + } +} + +Future<void> _copyNativeAssetsSingleArchitecture( + Uri buildUri, + Map<Asset, Asset> assetTargetLocations, + build_info.BuildMode buildMode, + FileSystem fileSystem, +) async { + if (assetTargetLocations.isNotEmpty) { + globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.'); + final Directory buildDir = fileSystem.directory(buildUri.toFilePath()); + if (!buildDir.existsSync()) { + buildDir.createSync(recursive: true); + } + for (final MapEntry<Asset, Asset> assetMapping in assetTargetLocations.entries) { + final Uri source = (assetMapping.key.path as AssetAbsolutePath).uri; + final Uri target = (assetMapping.value.path as AssetAbsolutePath).uri; + final Uri targetUri = buildUri.resolveUri(target); + final String targetFullPath = targetUri.toFilePath(); + await fileSystem.file(source).copy(targetFullPath); + } + globals.logger.printTrace('Copying native assets done.'); + } +} diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 38a63ff55df1b..befd5793e7d52 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -7,7 +7,6 @@ import 'package:xml/xml.dart'; import 'package:yaml/yaml.dart'; import '../src/convert.dart'; -import 'android/android_app_link_settings.dart'; import 'android/android_builder.dart'; import 'android/gradle_utils.dart' as gradle; import 'base/common.dart'; @@ -122,6 +121,9 @@ class FlutterProject { /// The location of this project. final Directory directory; + /// The location of the build folder. + Directory get buildDirectory => directory.childDirectory('build'); + /// The manifest of this project. final FlutterManifest manifest; @@ -486,20 +488,15 @@ class AndroidProject extends FlutterProjectPlatform { return androidBuilder!.getBuildVariants(project: parent); } - /// Returns app link related project settings for a given build variant. + /// Outputs app link related settings into a json file. /// - /// Use [getBuildVariants] to get all of the available build variants. - Future<AndroidAppLinkSettings> getAppLinksSettings({required String variant}) async { + /// The file is stored in + /// `<project>/build/app/app-link-settings-<variant>.json`. + Future<void> outputsAppLinkSettings({required String variant}) async { if (!existsSync() || androidBuilder == null) { - return const AndroidAppLinkSettings( - applicationId: '', - domains: <String>[], - ); + return; } - return AndroidAppLinkSettings( - applicationId: await androidBuilder!.getApplicationIdForVariant(variant, project: parent), - domains: await androidBuilder!.getAppLinkDomainsForVariant(variant, project: parent), - ); + await androidBuilder!.outputsAppLinkSettings(variant, project: parent); } bool _computeSupportedVersion() { @@ -509,30 +506,45 @@ class AndroidProject extends FlutterProjectPlatform { if (plugin.existsSync()) { return false; } - final File appGradle = hostAppGradleRoot.childFile( - fileSystem.path.join('app', 'build.gradle')); - if (!appGradle.existsSync()) { - return false; - } - for (final String line in appGradle.readAsLinesSync()) { - final bool fileBasedApply = line.contains(RegExp(r'apply from: .*/flutter.gradle')); - final bool declarativeApply = line.contains('dev.flutter.flutter-gradle-plugin'); - final bool managed = line.contains("def flutterPluginVersion = 'managed'"); - if (fileBasedApply || declarativeApply || managed) { - return true; + try { + for (final String line in appGradleFile.readAsLinesSync()) { + // This syntax corresponds to applying the Flutter Gradle Plugin with a + // script. + // See https://docs.gradle.org/current/userguide/plugins.html#sec:script_plugins. + final bool fileBasedApply = line.contains(RegExp(r'apply from: .*/flutter.gradle')); + + // This syntax corresponds to applying the Flutter Gradle Plugin using + // the declarative "plugins {}" block after including it in the + // pluginManagement block of the settings.gradle file. + // See https://docs.gradle.org/current/userguide/composite_builds.html#included_plugin_builds, + // as well as the settings.gradle and build.gradle templates. + final bool declarativeApply = line.contains('dev.flutter.flutter-gradle-plugin'); + + // This case allows for flutter run/build to work for modules. It does + // not guarantee the Flutter Gradle Plugin is applied. + final bool managed = line.contains("def flutterPluginVersion = 'managed'"); + if (fileBasedApply || declarativeApply || managed) { + return true; + } } + } on FileSystemException { + return false; } return false; } /// True, if the app project is using Kotlin. bool get isKotlin { - final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); - final bool imperativeMatch = firstMatchInFile(gradleFile, _imperativeKotlinPluginPattern) != null; - final bool declarativeMatch = firstMatchInFile(gradleFile, _declarativeKotlinPluginPattern) != null; + final bool imperativeMatch = firstMatchInFile(appGradleFile, _imperativeKotlinPluginPattern) != null; + final bool declarativeMatch = firstMatchInFile(appGradleFile, _declarativeKotlinPluginPattern) != null; return imperativeMatch || declarativeMatch; } + /// Gets the module-level build.gradle file. + /// See https://developer.android.com/build#module-level. + File get appGradleFile => hostAppGradleRoot.childDirectory('app') + .childFile('build.gradle'); + File get appManifestFile { if (isUsingGradle) { return hostAppGradleRoot @@ -561,7 +573,7 @@ class AndroidProject extends FlutterProjectPlatform { /// /// This is expected to be called from /// flutter_tools/lib/src/project_validator.dart. - Future<ProjectValidatorResult> validateJavaGradleAgpVersions() async { + Future<ProjectValidatorResult> validateJavaAndGradleAgpVersions() async { // Constructing ProjectValidatorResult happens here and not in // flutter_tools/lib/src/project_validator.dart because of the additional // Complexity of variable status values and error string formatting. @@ -587,7 +599,7 @@ class AndroidProject extends FlutterProjectPlatform { hostAppGradleRoot, globals.logger, globals.processManager); final String? agpVersion = gradle.getAgpVersion(hostAppGradleRoot, globals.logger); - final String? javaVersion = _versionToParsableString(globals.java?.version); + final String? javaVersion = versionToParsableString(globals.java?.version); // Assume valid configuration. String description = validJavaGradleAgpString; @@ -595,7 +607,7 @@ class AndroidProject extends FlutterProjectPlatform { final bool compatibleGradleAgp = gradle.validateGradleAndAgp(globals.logger, gradleV: gradleVersion, agpV: agpVersion); - final bool compatibleJavaGradle = gradle.validateJavaGradle(globals.logger, + final bool compatibleJavaGradle = gradle.validateJavaAndGradle(globals.logger, javaV: javaVersion, gradleV: gradleVersion); // Begin description formatting. @@ -627,21 +639,18 @@ $javaGradleCompatUrl } String? get applicationId { - final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); - return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); + return firstMatchInFile(appGradleFile, _applicationIdPattern)?.group(1); } /// Get the namespace for newer Android projects, /// which replaces the `package` attribute in the Manifest.xml. String? get namespace { - final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); - - if (!gradleFile.existsSync()) { + try { + // firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern. + return _androidNamespacePattern.firstMatch(appGradleFile.readAsStringSync())?.group(1); + } on FileSystemException { return null; } - - // firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern. - return _androidNamespacePattern.firstMatch(gradleFile.readAsStringSync())?.group(1); } String? get group { @@ -651,7 +660,7 @@ $javaGradleCompatUrl /// The build directory where the Android artifacts are placed. Directory get buildDirectory { - return parent.directory.childDirectory('build'); + return parent.buildDirectory; } Future<void> ensureReadyForPlatformSpecificTooling({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) async { @@ -713,9 +722,9 @@ $javaGradleCompatUrl 'androidIdentifier': androidIdentifier, 'androidX': usesAndroidX, 'agpVersion': gradle.templateAndroidGradlePluginVersion, + 'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule, 'kotlinVersion': gradle.templateKotlinGradlePluginVersion, 'gradleVersion': gradle.templateDefaultGradleVersion, - 'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule, 'compileSdkVersion': gradle.compileSdkVersion, 'minSdkVersion': gradle.minSdkVersion, 'ndkVersion': gradle.ndkVersion, @@ -914,7 +923,7 @@ class CompatibilityResult { } /// Converts a [Version] to a string that can be parsed by [Version.parse]. -String? _versionToParsableString(Version? version) { +String? versionToParsableString(Version? version) { if (version == null) { return null; } diff --git a/packages/flutter_tools/lib/src/project_validator.dart b/packages/flutter_tools/lib/src/project_validator.dart index 57c3f8dd4fe3a..8e23c8fc52013 100644 --- a/packages/flutter_tools/lib/src/project_validator.dart +++ b/packages/flutter_tools/lib/src/project_validator.dart @@ -227,7 +227,7 @@ class GeneralInfoProjectValidator extends ProjectValidator{ result.add(_materialDesignResult(flutterManifest)); result.add(_pluginValidatorResult(flutterManifest)); } - result.add(await project.android.validateJavaGradleAgpVersions()); + result.add(await project.android.validateJavaAndGradleAgpVersions()); return result; } diff --git a/packages/flutter_tools/lib/src/proxied_devices/devices.dart b/packages/flutter_tools/lib/src/proxied_devices/devices.dart index 332b30b0e50f2..93df50e03ff43 100644 --- a/packages/flutter_tools/lib/src/proxied_devices/devices.dart +++ b/packages/flutter_tools/lib/src/proxied_devices/devices.dart @@ -36,14 +36,15 @@ T _cast<T>(Object? object) { /// /// If [deltaFileTransfer] is true, the proxy will use an rsync-like algorithm that /// only transfers the changed part of the application package for deployment. -class ProxiedDevices extends DeviceDiscovery { +class ProxiedDevices extends PollingDeviceDiscovery { ProxiedDevices(this.connection, { bool deltaFileTransfer = true, bool enableDdsProxy = false, required Logger logger, }) : _deltaFileTransfer = deltaFileTransfer, _enableDdsProxy = enableDdsProxy, - _logger = logger; + _logger = logger, + super('Proxied devices'); /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; @@ -88,6 +89,9 @@ class ProxiedDevices extends DeviceDiscovery { return filter.filterDevices(devices); } + @override + Future<List<Device>> pollingGetDevices({Duration? timeout}) => discoverDevices(timeout: timeout); + @override List<String> get wellKnownIds => const <String>[]; diff --git a/packages/flutter_tools/lib/src/reporting/crash_reporting.dart b/packages/flutter_tools/lib/src/reporting/crash_reporting.dart index a8fd8da753ac7..df68535ae59f7 100644 --- a/packages/flutter_tools/lib/src/reporting/crash_reporting.dart +++ b/packages/flutter_tools/lib/src/reporting/crash_reporting.dart @@ -69,7 +69,7 @@ class CrashReporter { /// Prints instructions for filing a bug about the crash. Future<void> informUser(CrashDetails details, File crashFile) async { - _logger.printError('A crash report has been written to ${crashFile.path}.'); + _logger.printError('A crash report has been written to ${crashFile.path}'); _logger.printStatus('This crash may already be reported. Check GitHub for similar crashes.', emphasis: true); final String similarIssuesURL = GitHubTemplateCreator.toolCrashSimilarIssuesURL(details.error.toString()); diff --git a/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart b/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart index 9a31fcd57562d..4ee8c9e3429d1 100644 --- a/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart +++ b/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart @@ -58,7 +58,6 @@ class CustomDimensions { this.commandRunAndroidEmbeddingVersion, this.commandPackagesAndroidEmbeddingVersion, this.nullSafety, - this.fastReassemble, this.nullSafeMigratedLibraries, this.nullSafeTotalLibraries, this.hotEventCompileTimeInMs, @@ -118,17 +117,17 @@ class CustomDimensions { final String? commandRunAndroidEmbeddingVersion; // cd45 final String? commandPackagesAndroidEmbeddingVersion; // cd46 final bool? nullSafety; // cd47 - final bool? fastReassemble; // cd48 + // cd48 was fastReassemble but that feature was removed final int? nullSafeMigratedLibraries; // cd49 final int? nullSafeTotalLibraries; // cd50 - final int? hotEventCompileTimeInMs; // cd 51 - final int? hotEventFindInvalidatedTimeInMs; // cd 52 - final int? hotEventScannedSourcesCount; // cd 53 - final int? hotEventReassembleTimeInMs; // cd 54 - final int? hotEventReloadVMTimeInMs; // cd 55 - final bool? commandRunEnableImpeller; // cd 56 - final String? commandRunIOSInterfaceType; // cd 57 - final bool? commandRunIsTest; // cd 58 + final int? hotEventCompileTimeInMs; // cd51 + final int? hotEventFindInvalidatedTimeInMs; // cd52 + final int? hotEventScannedSourcesCount; // cd53 + final int? hotEventReassembleTimeInMs; // cd54 + final int? hotEventReloadVMTimeInMs; // cd55 + final bool? commandRunEnableImpeller; // cd56 + final String? commandRunIOSInterfaceType; // cd57 + final bool? commandRunIsTest; // cd58 /// Convert to a map that will be used to upload to the analytics backend. Map<String, String> toMap() => <String, String>{ @@ -179,7 +178,6 @@ class CustomDimensions { if (commandRunAndroidEmbeddingVersion != null) CustomDimensionsEnum.commandRunAndroidEmbeddingVersion.cdKey: commandRunAndroidEmbeddingVersion.toString(), if (commandPackagesAndroidEmbeddingVersion != null) CustomDimensionsEnum.commandPackagesAndroidEmbeddingVersion.cdKey: commandPackagesAndroidEmbeddingVersion.toString(), if (nullSafety != null) CustomDimensionsEnum.nullSafety.cdKey: nullSafety.toString(), - if (fastReassemble != null) CustomDimensionsEnum.fastReassemble.cdKey: fastReassemble.toString(), if (nullSafeMigratedLibraries != null) CustomDimensionsEnum.nullSafeMigratedLibraries.cdKey: nullSafeMigratedLibraries.toString(), if (nullSafeTotalLibraries != null) CustomDimensionsEnum.nullSafeTotalLibraries.cdKey: nullSafeTotalLibraries.toString(), if (hotEventCompileTimeInMs != null) CustomDimensionsEnum.hotEventCompileTimeInMs.cdKey: hotEventCompileTimeInMs.toString(), @@ -247,7 +245,6 @@ class CustomDimensions { commandRunAndroidEmbeddingVersion: other.commandRunAndroidEmbeddingVersion ?? commandRunAndroidEmbeddingVersion, commandPackagesAndroidEmbeddingVersion: other.commandPackagesAndroidEmbeddingVersion ?? commandPackagesAndroidEmbeddingVersion, nullSafety: other.nullSafety ?? nullSafety, - fastReassemble: other.fastReassemble ?? fastReassemble, nullSafeMigratedLibraries: other.nullSafeMigratedLibraries ?? nullSafeMigratedLibraries, nullSafeTotalLibraries: other.nullSafeTotalLibraries ?? nullSafeTotalLibraries, hotEventCompileTimeInMs: other.hotEventCompileTimeInMs ?? hotEventCompileTimeInMs, @@ -309,7 +306,6 @@ class CustomDimensions { commandRunAndroidEmbeddingVersion: _extractString(map, CustomDimensionsEnum.commandRunAndroidEmbeddingVersion), commandPackagesAndroidEmbeddingVersion: _extractString(map, CustomDimensionsEnum.commandPackagesAndroidEmbeddingVersion), nullSafety: _extractBool(map, CustomDimensionsEnum.nullSafety), - fastReassemble: _extractBool(map, CustomDimensionsEnum.fastReassemble), nullSafeMigratedLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeMigratedLibraries), nullSafeTotalLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeTotalLibraries), hotEventCompileTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventCompileTimeInMs), @@ -397,7 +393,7 @@ enum CustomDimensionsEnum { commandRunAndroidEmbeddingVersion, // cd45 commandPackagesAndroidEmbeddingVersion, // cd46 nullSafety, // cd47 - fastReassemble, // cd48 + obsolete1, // cd48 (was fastReassemble) nullSafeMigratedLibraries, // cd49 nullSafeTotalLibraries, // cd50 hotEventCompileTimeInMs, // cd51 diff --git a/packages/flutter_tools/lib/src/reporting/events.dart b/packages/flutter_tools/lib/src/reporting/events.dart index 5ffcb2d584f3b..ec3933ed5d48f 100644 --- a/packages/flutter_tools/lib/src/reporting/events.dart +++ b/packages/flutter_tools/lib/src/reporting/events.dart @@ -39,7 +39,6 @@ class HotEvent extends UsageEvent { required this.sdkName, required this.emulator, required this.fullRestart, - required this.fastReassemble, this.reason, this.finalLibraryCount, this.syncedLibraryCount, @@ -54,14 +53,15 @@ class HotEvent extends UsageEvent { this.scannedSourcesCount, this.reassembleTimeInMs, this.reloadVMTimeInMs, - }) : super('hot', parameter, flutterUsage: globals.flutterUsage); + // TODO(fujino): make this required + Usage? usage, + }) : super('hot', parameter, flutterUsage: usage ?? globals.flutterUsage); final String? reason; final String targetPlatform; final String sdkName; final bool emulator; final bool fullRestart; - final bool fastReassemble; final int? finalLibraryCount; final int? syncedLibraryCount; final int? syncedClassesCount; @@ -92,7 +92,6 @@ class HotEvent extends UsageEvent { hotEventInvalidatedSourcesCount: invalidatedSourcesCount, hotEventTransferTimeInMs: transferTimeInMs, hotEventOverallTimeInMs: overallTimeInMs, - fastReassemble: fastReassemble, hotEventCompileTimeInMs: compileTimeInMs, hotEventFindInvalidatedTimeInMs: findInvalidatedTimeInMs, hotEventScannedSourcesCount: scannedSourcesCount, diff --git a/packages/flutter_tools/lib/src/reporting/first_run.dart b/packages/flutter_tools/lib/src/reporting/first_run.dart index 2d39fe3f53e68..0e2b0caab7ff1 100644 --- a/packages/flutter_tools/lib/src/reporting/first_run.dart +++ b/packages/flutter_tools/lib/src/reporting/first_run.dart @@ -25,8 +25,7 @@ const String _kFlutterFirstRunMessage = ''' ║ Flutter tool. ║ ║ ║ ║ By downloading the Flutter SDK, you agree to the Google Terms of Service. ║ - ║ Note: The Google Privacy Policy describes how data is handled in this ║ - ║ service. ║ + ║ The Google Privacy Policy describes how data is handled in this service. ║ ║ ║ ║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and ║ ║ crash reports to Google. ║ @@ -36,6 +35,8 @@ const String _kFlutterFirstRunMessage = ''' ║ ║ ║ See Google's privacy policy: ║ ║ https://policies.google.com/privacy ║ + ║ ║ + ║ To disable animations in this tool, use 'flutter config --no-animations'. ║ ╚════════════════════════════════════════════════════════════════════════════╝ '''; diff --git a/packages/flutter_tools/lib/src/resident_devtools_handler.dart b/packages/flutter_tools/lib/src/resident_devtools_handler.dart index 3b7521480fa83..5eac9320ac090 100644 --- a/packages/flutter_tools/lib/src/resident_devtools_handler.dart +++ b/packages/flutter_tools/lib/src/resident_devtools_handler.dart @@ -79,19 +79,19 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { return; } if (devToolsServerAddress != null) { - _devToolsLauncher!.devToolsUrl = devToolsServerAddress; + _devToolsLauncher.devToolsUrl = devToolsServerAddress; } else { - await _devToolsLauncher!.serve(); + await _devToolsLauncher.serve(); _served = true; } - await _devToolsLauncher!.ready; + await _devToolsLauncher.ready; // Do not attempt to print debugger list if the connection has failed or if we're shutting down. - if (_devToolsLauncher!.activeDevToolsServer == null || _shutdown) { + if (_devToolsLauncher.activeDevToolsServer == null || _shutdown) { assert(!_readyToAnnounce); return; } - final Uri? devToolsUrl = _devToolsLauncher!.devToolsUrl; + final Uri? devToolsUrl = _devToolsLauncher.devToolsUrl; if (devToolsUrl != null) { for (final FlutterDevice? device in flutterDevices) { if (device == null) { @@ -130,7 +130,7 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { } _readyToAnnounce = true; - assert(_devToolsLauncher!.activeDevToolsServer != null); + assert(_devToolsLauncher.activeDevToolsServer != null); if (_residentRunner.reportedDebuggers) { // Since the DevTools only just became available, we haven't had a chance to // report their URLs yet. Do so now. @@ -148,9 +148,9 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) { return false; } - if (_devToolsLauncher!.devToolsUrl == null) { + if (_devToolsLauncher.devToolsUrl == null) { _logger.startProgress('Waiting for Flutter DevTools to be served...'); - unawaited(_devToolsLauncher!.ready.then((_) { + unawaited(_devToolsLauncher.ready.then((_) { _launchDevToolsForDevices(flutterDevices); })); } else { @@ -294,7 +294,7 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { } _shutdown = true; _readyToAnnounce = false; - await _devToolsLauncher!.close(); + await _devToolsLauncher.close(); } } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 1d55e0d5673dc..1b63932ce60c7 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -34,7 +34,6 @@ import 'compile.dart'; import 'convert.dart'; import 'devfs.dart'; import 'device.dart'; -import 'features.dart'; import 'globals.dart' as globals; import 'ios/application_package.dart'; import 'ios/devices.dart'; @@ -68,6 +67,7 @@ class FlutterDevice { targetModel: targetModel, dartDefines: buildInfo.dartDefines, packagesPath: buildInfo.packagesPath, + frontendServerStarterPath: buildInfo.frontendServerStarterPath, extraFrontEndOptions: buildInfo.extraFrontEndOptions, artifacts: globals.artifacts!, processManager: globals.processManager, @@ -156,6 +156,7 @@ class FlutterDevice { ), assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate, targetModel: TargetModel.dartdevc, + frontendServerStarterPath: buildInfo.frontendServerStarterPath, extraFrontEndOptions: extraFrontEndOptions, platformDill: globals.fs.file(platformDillPath).absolute.uri.toString(), dartDefines: buildInfo.dartDefines, @@ -169,11 +170,8 @@ class FlutterDevice { platform: platform, ); } else { - // The flutter-widget-cache feature only applies to run mode. List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions; extraFrontEndOptions = <String>[ - if (featureFlags.isSingleWidgetReloadEnabled) - '--flutter-widget-cache', '--enable-experiment=alternative-invalidation-strategy', ...extraFrontEndOptions, ]; @@ -189,6 +187,7 @@ class FlutterDevice { fileSystemScheme: buildInfo.fileSystemScheme, targetModel: targetModel, dartDefines: buildInfo.dartDefines, + frontendServerStarterPath: buildInfo.frontendServerStarterPath, extraFrontEndOptions: extraFrontEndOptions, initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, @@ -1018,17 +1017,17 @@ abstract class ResidentHandlers { } Future<bool> _takeVmServiceScreenshot(FlutterDevice device, File outputFile) async { - final bool isWebDevice = device.targetPlatform == TargetPlatform.web_javascript; + if (device.targetPlatform != TargetPlatform.web_javascript) { + return false; + } assert(supportsServiceProtocol); return _toggleDebugBanner(device, () async { - final vm_service.Response? response = isWebDevice - ? await device.vmService!.callMethodWrapper('ext.dwds.screenshot') - : await device.vmService!.screenshot(); + final vm_service.Response? response = await device.vmService!.callMethodWrapper('ext.dwds.screenshot'); if (response == null) { throw Exception('Failed to take screenshot'); } - final String data = response.json![isWebDevice ? 'data' : 'screenshot'] as String; + final String data = response.json!['data'] as String; outputFile.writeAsBytesSync(base64.decode(data)); }); } @@ -1694,7 +1693,7 @@ class TerminalHandler { _addSignalHandler(io.ProcessSignal.sigusr2, _handleSignal); if (_pidFile != null) { _logger.printTrace('Writing pid to: $_pidFile'); - _actualPidFile = _processInfo.writePidFile(_pidFile!); + _actualPidFile = _processInfo.writePidFile(_pidFile); } } } @@ -1881,19 +1880,17 @@ class DebugConnectionInfo { /// These values must match what is available in /// `packages/flutter/lib/src/foundation/binding.dart`. String nextPlatform(String currentPlatform) { - switch (currentPlatform) { - case 'android': - return 'iOS'; - case 'iOS': - return 'fuchsia'; - case 'fuchsia': - return 'macOS'; - case 'macOS': - return 'android'; - default: - assert(false); // Invalid current platform. - return 'android'; - } + const List<String> platforms = <String>[ + 'android', + 'iOS', + 'windows', + 'macOS', + 'linux', + 'fuchsia', + ]; + final int index = platforms.indexOf(currentPlatform); + assert(index >= 0, 'unknown platform "$currentPlatform"'); + return platforms[(index + 1) % platforms.length]; } /// A launcher for the devtools debugger and analysis tool. diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 7d8c77ac02b26..6e829ed18c73e 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -20,8 +20,8 @@ import 'convert.dart'; import 'dart/package_map.dart'; import 'devfs.dart'; import 'device.dart'; -import 'features.dart'; import 'globals.dart' as globals; +import 'native_assets.dart'; import 'project.dart'; import 'reporting/reporting.dart'; import 'resident_runner.dart'; @@ -91,11 +91,13 @@ class HotRunner extends ResidentRunner { this.multidexEnabled = false, super.devtoolsHandler, StopwatchFactory stopwatchFactory = const StopwatchFactory(), - ReloadSourcesHelper reloadSourcesHelper = _defaultReloadSourcesHelper, + ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper, ReassembleHelper reassembleHelper = _defaultReassembleHelper, + NativeAssetsBuildRunner? buildRunner, }) : _stopwatchFactory = stopwatchFactory, _reloadSourcesHelper = reloadSourcesHelper, _reassembleHelper = reassembleHelper, + _buildRunner = buildRunner, super( hotMode: true, ); @@ -133,6 +135,8 @@ class HotRunner extends ResidentRunner { String? _sdkName; bool? _emulator; + NativeAssetsBuildRunner? _buildRunner; + Future<void> _calculateTargetPlatform() async { if (_targetPlatform != null) { return; @@ -361,6 +365,20 @@ class HotRunner extends ResidentRunner { }) async { await _calculateTargetPlatform(); + final Uri projectUri = Uri.directory(projectRootPath); + _buildRunner ??= NativeAssetsBuildRunnerImpl( + projectUri, + debuggingOptions.buildInfo.packageConfig, + fileSystem, + globals.logger, + ); + final Uri? nativeAssetsYaml = await dryRunNativeAssets( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: _buildRunner!, + flutterDevices: flutterDevices, + ); + final Stopwatch appStartedTimer = Stopwatch()..start(); final File mainFile = globals.fs.file(mainPath); firstBuildTime = DateTime.now(); @@ -392,6 +410,7 @@ class HotRunner extends ResidentRunner { packageConfig: debuggingOptions.buildInfo.packageConfig, projectRootPath: FlutterProject.current().directory.absolute.path, fs: globals.fs, + nativeAssetsYaml: nativeAssetsYaml, ).then((CompilerOutput? output) { compileTimer.stop(); totalCompileTime += compileTimer.elapsed; @@ -415,7 +434,6 @@ class HotRunner extends ResidentRunner { sdkName: _sdkName!, emulator: _emulator!, fullRestart: false, - fastReassemble: false, overallTimeInMs: appStartedTimer.elapsed.inMilliseconds, compileTimeInMs: totalCompileTime.inMilliseconds, transferTimeInMs: totalLaunchAppTime.inMilliseconds, @@ -802,7 +820,6 @@ class HotRunner extends ResidentRunner { emulator: emulator!, fullRestart: true, reason: reason, - fastReassemble: false, overallTimeInMs: restartTimer.elapsed.inMilliseconds, syncedBytes: result.updateFSReport?.syncedBytes, invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount, @@ -828,7 +845,6 @@ class HotRunner extends ResidentRunner { emulator: emulator!, fullRestart: true, reason: reason, - fastReassemble: false, ).send(); } status?.cancel(); @@ -878,7 +894,6 @@ class HotRunner extends ResidentRunner { emulator: emulator!, fullRestart: false, reason: reason, - fastReassemble: false, ).send(); } else { HotEvent('exception', @@ -887,7 +902,6 @@ class HotRunner extends ResidentRunner { emulator: emulator!, fullRestart: false, reason: reason, - fastReassemble: false, ).send(); } return OperationResult(errorCode, errorMessage, fatal: true); @@ -950,6 +964,7 @@ class HotRunner extends ResidentRunner { sdkName, emulator, reason, + globals.flutterUsage, ); if (result.code != 0) { return result; @@ -970,7 +985,6 @@ class HotRunner extends ResidentRunner { viewCache, onSlow, reloadMessage, - updatedDevFS.fastReassembleClassName, ); shouldReportReloadTime = reassembleResult.shouldReportReloadTime; if (reassembleResult.reassembleViews.isEmpty) { @@ -1004,7 +1018,6 @@ class HotRunner extends ResidentRunner { syncedBytes: updatedDevFS.syncedBytes, invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds, - fastReassemble: featureFlags.isSingleWidgetReloadEnabled && updatedDevFS.fastReassembleClassName != null, compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds, findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds, scannedSourcesCount: updatedDevFS.scannedSourcesCount, @@ -1172,9 +1185,11 @@ typedef ReloadSourcesHelper = Future<OperationResult> Function( String? sdkName, bool? emulator, String? reason, + Usage usage, ); -Future<OperationResult> _defaultReloadSourcesHelper( +@visibleForTesting +Future<OperationResult> defaultReloadSourcesHelper( HotRunner hotRunner, List<FlutterDevice?> flutterDevices, bool? pause, @@ -1183,10 +1198,11 @@ Future<OperationResult> _defaultReloadSourcesHelper( String? sdkName, bool? emulator, String? reason, + Usage usage, ) async { final Stopwatch vmReloadTimer = Stopwatch()..start(); const String entryPath = 'main.dart.incremental.dill'; - final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[]; + final List<Future<DeviceReloadReport?>> allReportsFutures = <Future<DeviceReloadReport?>>[]; for (final FlutterDevice? device in flutterDevices) { final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources( @@ -1194,10 +1210,13 @@ Future<OperationResult> _defaultReloadSourcesHelper( entryPath, pause: pause, ); - allReportsFutures.add(Future.wait(reportFutures).then( + allReportsFutures.add(Future.wait(reportFutures).then<DeviceReloadReport?>( (List<vm_service.ReloadReport> reports) async { // TODO(aam): Investigate why we are validating only first reload report, // which seems to be current behavior + if (reports.isEmpty) { + return null; + } final vm_service.ReloadReport firstReport = reports.first; // Don't print errors because they will be printed further down when // `validateReloadReport` is called again. @@ -1208,9 +1227,9 @@ Future<OperationResult> _defaultReloadSourcesHelper( }, )); } - final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures); - final vm_service.ReloadReport reloadReport = reports.first.reports[0]; - if (!HotRunner.validateReloadReport(reloadReport)) { + final Iterable<DeviceReloadReport> reports = (await Future.wait(allReportsFutures)).whereType<DeviceReloadReport>(); + final vm_service.ReloadReport? reloadReport = reports.isEmpty ? null : reports.first.reports[0]; + if (reloadReport == null || !HotRunner.validateReloadReport(reloadReport)) { // Reload failed. HotEvent('reload-reject', targetPlatform: targetPlatform!, @@ -1218,11 +1237,14 @@ Future<OperationResult> _defaultReloadSourcesHelper( emulator: emulator!, fullRestart: false, reason: reason, - fastReassemble: false, + usage: usage, ).send(); // Reset devFS lastCompileTime to ensure the file will still be marked // as dirty on subsequent reloads. _resetDevFSCompileTime(flutterDevices); + if (reloadReport == null) { + return OperationResult(1, 'No Dart isolates found'); + } final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}'); } @@ -1277,7 +1299,6 @@ typedef ReassembleHelper = Future<ReassembleResult> Function( Map<FlutterDevice?, List<FlutterView>> viewCache, void Function(String message)? onSlow, String reloadMessage, - String? fastReassembleClassName, ); Future<ReassembleResult> _defaultReassembleHelper( @@ -1285,7 +1306,6 @@ Future<ReassembleResult> _defaultReassembleHelper( Map<FlutterDevice?, List<FlutterView>> viewCache, void Function(String message)? onSlow, String reloadMessage, - String? fastReassembleClassName, ) async { // Check if any isolates are paused and reassemble those that aren't. final Map<FlutterView, FlutterVmService?> reassembleViews = <FlutterView, FlutterVmService?>{}; @@ -1314,17 +1334,9 @@ Future<ReassembleResult> _defaultReassembleHelper( reassembleViews[view] = device.vmService; // If the tool identified a change in a single widget, do a fast instead // of a full reassemble. - Future<void> reassembleWork; - if (fastReassembleClassName != null) { - reassembleWork = device.vmService!.flutterFastReassemble( - isolateId: view.uiIsolate!.id!, - className: fastReassembleClassName, - ); - } else { - reassembleWork = device.vmService!.flutterReassemble( - isolateId: view.uiIsolate!.id!, - ); - } + final Future<void> reassembleWork = device.vmService!.flutterReassemble( + isolateId: view.uiIsolate!.id!, + ); reassembleFutures.add(reassembleWork.then( (Object? obj) => obj, onError: (Object error, StackTrace stackTrace) { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index b7e624b4e2589..1d99c9a29e450 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -34,6 +34,31 @@ import 'target_devices.dart'; export '../cache.dart' show DevelopmentArtifact; +abstract class DotEnvRegex { + // Dot env multi-line block value regex + static final RegExp multiLineBlock = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*"""\s*(.*)$'); + + // Dot env full line value regex (eg FOO=bar) + // Entire line will be matched including key and value + static final RegExp keyValue = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)?$'); + + // Dot env value wrapped in double quotes regex (eg FOO="bar") + // Value between double quotes will be matched (eg only bar in "bar") + static final RegExp doubleQuotedValue = RegExp(r'^"(.*)"\s*(\#\s*.*)?$'); + + // Dot env value wrapped in single quotes regex (eg FOO='bar') + // Value between single quotes will be matched (eg only bar in 'bar') + static final RegExp singleQuotedValue = RegExp(r"^'(.*)'\s*(\#\s*.*)?$"); + + // Dot env value wrapped in back quotes regex (eg FOO=`bar`) + // Value between back quotes will be matched (eg only bar in `bar`) + static final RegExp backQuotedValue = RegExp(r'^`(.*)`\s*(\#\s*.*)?$'); + + // Dot env value without quotes regex (eg FOO=bar) + // Value without quotes will be matched (eg full value after the equals sign) + static final RegExp unquotedValue = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$'); +} + enum ExitStatus { success, warning, @@ -87,6 +112,7 @@ class FlutterCommandResult { /// Common flutter command line options. abstract final class FlutterOptions { + static const String kFrontendServerStarterPath = 'frontend-server-starter-path'; static const String kExtraFrontEndOptions = 'extra-front-end-options'; static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options'; static const String kEnableExperiment = 'enable-experiment'; @@ -461,7 +487,7 @@ abstract class FlutterCommand extends Command<void> { } ddsEnabled = !boolArg('disable-dds'); // TODO(ianh): enable the following code once google3 is migrated away from --disable-dds (and add test to flutter_command_test.dart) - if (false) { // ignore: dead_code + if (false) { // ignore: dead_code, literal_only_boolean_expressions if (ddsEnabled) { globals.printWarning('${globals.logger.terminal .warningMark} The "--no-disable-dds" argument is deprecated and redundant, and should be omitted.'); @@ -621,7 +647,8 @@ abstract class FlutterCommand extends Command<void> { help: 'The path of a .json or .env file containing key-value pairs that will be available as environment variables.\n' 'These can be accessed using the String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment constructors.\n' - 'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.', + 'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.\n' + 'Entries from "--${FlutterOptions.kDartDefinesOption}" with identical keys take precedence over entries from these files.', valueHelp: 'use-define-config.json|.env', splitCommas: false, ); @@ -823,6 +850,18 @@ abstract class FlutterCommand extends Command<void> { ); } + void usesFrontendServerStarterPathOption({required bool verboseHelp}) { + argParser.addOption( + FlutterOptions.kFrontendServerStarterPath, + help: 'When this value is provided, the frontend server will be started ' + 'in JIT mode from the specified file, instead of from the AOT ' + 'snapshot shipped with the Dart SDK. The specified file can either ' + 'be a Dart source file, or an AppJIT snapshot. This option does ' + 'not affect web builds.', + hide: !verboseHelp, + ); + } + /// Enables support for the hidden options --extra-front-end-options and /// --extra-gen-snapshot-options. void usesExtraDartFlagOptions({ required bool verboseHelp }) { @@ -1208,11 +1247,25 @@ abstract class FlutterCommand extends Command<void> { } } + final String? flavor = argParser.options.containsKey('flavor') ? stringArg('flavor') : null; + if (flavor != null) { + if (globals.platform.environment['FLUTTER_APP_FLAVOR'] != null) { + throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.'); + } + if (dartDefines.any((String define) => define.startsWith('FLUTTER_APP_FLAVOR'))) { + throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be ' + 'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}'); + } + dartDefines.add('FLUTTER_APP_FLAVOR=$flavor'); + } + return BuildInfo(buildMode, - argParser.options.containsKey('flavor') - ? stringArg('flavor') - : null, + flavor, trackWidgetCreation: trackWidgetCreation, + frontendServerStarterPath: argParser.options + .containsKey(FlutterOptions.kFrontendServerStarterPath) + ? stringArg(FlutterOptions.kFrontendServerStarterPath) + : null, extraFrontEndOptions: extraFrontEndOptions.isNotEmpty ? extraFrontEndOptions : null, @@ -1326,14 +1379,14 @@ abstract class FlutterCommand extends Command<void> { List<String> extractDartDefines({required Map<String, Object?> defineConfigJsonMap}) { final List<String> dartDefines = <String>[]; - if (argParser.options.containsKey(FlutterOptions.kDartDefinesOption)) { - dartDefines.addAll(stringsArg(FlutterOptions.kDartDefinesOption)); - } - defineConfigJsonMap.forEach((String key, Object? value) { dartDefines.add('$key=$value'); }); + if (argParser.options.containsKey(FlutterOptions.kDartDefinesOption)) { + dartDefines.addAll(stringsArg(FlutterOptions.kDartDefinesOption)); + } + return dartDefines; } @@ -1347,9 +1400,8 @@ abstract class FlutterCommand extends Command<void> { for (final String path in configFilePaths) { if (!globals.fs.isFileSync(path)) { - throwToolExit('Json config define file "--${FlutterOptions - .kDartDefineFromFileOption}=$path" is not a file, ' - 'please fix first!'); + throwToolExit('Did not find the file passed to "--${FlutterOptions + .kDartDefineFromFileOption}". Path: $path'); } final String configRaw = globals.fs.file(path).readAsStringSync(); @@ -1390,45 +1442,39 @@ abstract class FlutterCommand extends Command<void> { /// /// Returns a record of key and value as strings. MapEntry<String, String> _parseProperty(String line) { - final RegExp blockRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*"""\s*(.*)$'); - if (blockRegExp.hasMatch(line)) { + if (DotEnvRegex.multiLineBlock.hasMatch(line)) { throwToolExit('Multi-line value is not supported: $line'); } - final RegExp propertyRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)?$'); - final Match? match = propertyRegExp.firstMatch(line); - if (match == null) { + final Match? keyValueMatch = DotEnvRegex.keyValue.firstMatch(line); + if (keyValueMatch == null) { throwToolExit('Unable to parse file provided for ' '--${FlutterOptions.kDartDefineFromFileOption}.\n' 'Invalid property line: $line'); } - final String key = match.group(1)!; - final String value = match.group(2) ?? ''; + final String key = keyValueMatch.group(1)!; + final String value = keyValueMatch.group(2) ?? ''; // Remove wrapping quotes and trailing line comment. - final RegExp doubleQuoteValueRegExp = RegExp(r'^"(.*)"\s*(\#\s*.*)?$'); - final Match? doubleQuoteValue = doubleQuoteValueRegExp.firstMatch(value); - if (doubleQuoteValue != null) { - return MapEntry<String, String>(key, doubleQuoteValue.group(1)!); + final Match? doubleQuotedValueMatch = DotEnvRegex.doubleQuotedValue.firstMatch(value); + if (doubleQuotedValueMatch != null) { + return MapEntry<String, String>(key, doubleQuotedValueMatch.group(1)!); } - final RegExp quoteValueRegExp = RegExp(r"^'(.*)'\s*(\#\s*.*)?$"); - final Match? quoteValue = quoteValueRegExp.firstMatch(value); - if (quoteValue != null) { - return MapEntry<String, String>(key, quoteValue.group(1)!); + final Match? singleQuotedValueMatch = DotEnvRegex.singleQuotedValue.firstMatch(value); + if (singleQuotedValueMatch != null) { + return MapEntry<String, String>(key, singleQuotedValueMatch.group(1)!); } - final RegExp backQuoteValueRegExp = RegExp(r'^`(.*)`\s*(\#\s*.*)?$'); - final Match? backQuoteValue = backQuoteValueRegExp.firstMatch(value); - if (backQuoteValue != null) { - return MapEntry<String, String>(key, backQuoteValue.group(1)!); + final Match? backQuotedValueMatch = DotEnvRegex.backQuotedValue.firstMatch(value); + if (backQuotedValueMatch != null) { + return MapEntry<String, String>(key, backQuotedValueMatch.group(1)!); } - final RegExp noQuoteValueRegExp = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$'); - final Match? noQuoteValue = noQuoteValueRegExp.firstMatch(value); - if (noQuoteValue != null) { - return MapEntry<String, String>(key, noQuoteValue.group(1)!); + final Match? unquotedValueMatch = DotEnvRegex.unquotedValue.firstMatch(value); + if (unquotedValueMatch != null) { + return MapEntry<String, String>(key, unquotedValueMatch.group(1)!); } return MapEntry<String, String>(key, value); diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 5d6d78639fb66..25e0ddb12d195 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -26,10 +26,11 @@ abstract final class FlutterGlobalOptions { static const String kColorFlag = 'color'; static const String kContinuousIntegrationFlag = 'ci'; static const String kDeviceIdOption = 'device-id'; - static const String kDisableTelemetryFlag = 'disable-telemetry'; - static const String kEnableTelemetryFlag = 'enable-telemetry'; + static const String kDisableAnalyticsFlag = 'disable-analytics'; + static const String kEnableAnalyticsFlag = 'enable-analytics'; static const String kLocalEngineOption = 'local-engine'; static const String kLocalEngineSrcPathOption = 'local-engine-src-path'; + static const String kLocalEngineHostOption = 'local-engine-host'; static const String kLocalWebSDKOption = 'local-web-sdk'; static const String kMachineFlag = 'machine'; static const String kPackagesOption = 'packages'; @@ -100,17 +101,17 @@ class FlutterCommandRunner extends CommandRunner<void> { defaultsTo: true, hide: !verboseHelp, help: 'Allow Flutter to check for updates when this command runs.'); - argParser.addFlag(FlutterGlobalOptions.kSuppressAnalyticsFlag, + argParser.addFlag(FlutterGlobalOptions.kEnableAnalyticsFlag, negatable: false, - help: 'Suppress analytics reporting for the current CLI invocation.'); - argParser.addFlag(FlutterGlobalOptions.kDisableTelemetryFlag, + help: 'Enable telemetry reporting each time a flutter or dart ' + 'command runs.'); + argParser.addFlag(FlutterGlobalOptions.kDisableAnalyticsFlag, negatable: false, help: 'Disable telemetry reporting each time a flutter or dart ' 'command runs, until it is re-enabled.'); - argParser.addFlag(FlutterGlobalOptions.kEnableTelemetryFlag, + argParser.addFlag(FlutterGlobalOptions.kSuppressAnalyticsFlag, negatable: false, - help: 'Enable telemetry reporting each time a flutter or dart ' - 'command runs.'); + help: 'Suppress analytics reporting for the current CLI invocation.'); argParser.addOption(FlutterGlobalOptions.kPackagesOption, hide: !verboseHelp, help: 'Path to your "package_config.json" file.'); @@ -131,6 +132,13 @@ class FlutterCommandRunner extends CommandRunner<void> { 'Use this to select a specific version of the engine if you have built multiple engine targets.\n' 'This path is relative to "--local-engine-src-path" (see above).'); + argParser.addOption(FlutterGlobalOptions.kLocalEngineHostOption, + hide: !verboseHelp, + help: 'The host operating system for which engine artifacts should be selected, if you are building Flutter locally.\n' + 'This is only used when "--local-engine" is also specified.\n' + 'By default, the host is determined automatically, but you may need to specify this if you are building on one ' + 'platform (e.g. MacOS ARM64) but intend to run Flutter on another (e.g. Android).'); + argParser.addOption(FlutterGlobalOptions.kLocalWebSDKOption, hide: !verboseHelp, help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n' @@ -229,8 +237,8 @@ class FlutterCommandRunner extends CommandRunner<void> { // If the flag for enabling or disabling telemetry is passed in, // we will return out - if (topLevelResults.wasParsed(FlutterGlobalOptions.kDisableTelemetryFlag) || - topLevelResults.wasParsed(FlutterGlobalOptions.kEnableTelemetryFlag)) { + if (topLevelResults.wasParsed(FlutterGlobalOptions.kDisableAnalyticsFlag) || + topLevelResults.wasParsed(FlutterGlobalOptions.kEnableAnalyticsFlag)) { return; } @@ -273,6 +281,7 @@ class FlutterCommandRunner extends CommandRunner<void> { final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath( engineSourcePath: topLevelResults[FlutterGlobalOptions.kLocalEngineSrcPathOption] as String?, localEngine: topLevelResults[FlutterGlobalOptions.kLocalEngineOption] as String?, + localHostEngine: topLevelResults[FlutterGlobalOptions.kLocalEngineHostOption] as String?, localWebSdk: topLevelResults[FlutterGlobalOptions.kLocalWebSDKOption] as String?, packagePath: topLevelResults[FlutterGlobalOptions.kPackagesOption] as String?, ); diff --git a/packages/flutter_tools/lib/src/runner/local_engine.dart b/packages/flutter_tools/lib/src/runner/local_engine.dart index e5911f2c2df0a..24c0b1b353739 100644 --- a/packages/flutter_tools/lib/src/runner/local_engine.dart +++ b/packages/flutter_tools/lib/src/runner/local_engine.dart @@ -13,7 +13,7 @@ import '../base/user_messages.dart' hide userMessages; import '../cache.dart'; import '../dart/package_map.dart'; -/// A strategy for locating the out/ directory of a local engine build. +/// A strategy for locating the `out/` directory of a local engine build. /// /// The flutter tool can be run with the output files of one or more engine builds /// replacing the cached artifacts. Typically this is done by setting the @@ -25,7 +25,7 @@ import '../dart/package_map.dart'; /// For scenarios where the engine is not adjacent to flutter, the /// `--local-engine-src-path` can be provided to give an exact path. /// -/// For more information on local engines, see CONTRIBUTING.md. +/// For more information on local engines, see README.md. class LocalEngineLocator { LocalEngineLocator({ required Platform platform, @@ -46,7 +46,13 @@ class LocalEngineLocator { final UserMessages _userMessages; /// Returns the engine build path of a local engine if one is located, otherwise `null`. - Future<EngineBuildPaths?> findEnginePath({String? engineSourcePath, String? localEngine, String? localWebSdk, String? packagePath}) async { + Future<EngineBuildPaths?> findEnginePath({ + String? engineSourcePath, + String? localEngine, + String? localHostEngine, + String? localWebSdk, + String? packagePath, + }) async { engineSourcePath ??= _platform.environment[kFlutterEngineEnvironmentVariableName]; if (engineSourcePath == null && localEngine == null && localWebSdk == null && packagePath == null) { return null; @@ -81,7 +87,12 @@ class LocalEngineLocator { if (engineSourcePath != null) { _logger.printTrace('Local engine source at $engineSourcePath'); - return _findEngineBuildPath(localEngine, localWebSdk, engineSourcePath); + return _findEngineBuildPath( + engineSourcePath: engineSourcePath, + localEngine: localEngine, + localWebSdk: localWebSdk, + localHostEngine: localHostEngine, + ); } if (localEngine != null || localWebSdk != null) { throwToolExit( @@ -156,27 +167,12 @@ class LocalEngineLocator { return engineSourcePath; } - // Determine the host engine directory associated with the local engine: - // Strip '_sim' since there are no host simulator builds. - String _getHostEngineBasename(String localEngineBasename) { - if (localEngineBasename.startsWith('web_') || - localEngineBasename.startsWith('wasm_') || - localEngineBasename.startsWith('host_')) { - // Don't modify the web or host local engine's basename. - return localEngineBasename; - } - - String tmpBasename = localEngineBasename.replaceFirst('_sim', ''); - tmpBasename = tmpBasename.substring(tmpBasename.indexOf('_') + 1); - // Strip suffix for various archs. - const List<String> suffixes = <String>['_arm', '_arm64', '_x86', '_x64']; - for (final String suffix in suffixes) { - tmpBasename = tmpBasename.replaceFirst(RegExp('$suffix\$'), ''); - } - return 'host_$tmpBasename'; - } - - EngineBuildPaths _findEngineBuildPath(String? localEngine, String? localWebSdk, String enginePath) { + EngineBuildPaths _findEngineBuildPath({ + required String engineSourcePath, + String? localEngine, + String? localWebSdk, + String? localHostEngine, + }) { if (localEngine == null && localWebSdk == null) { throwToolExit(_userMessages.runnerLocalEngineOrWebSdkRequired, exitCode: 2); } @@ -184,15 +180,16 @@ class LocalEngineLocator { String? engineBuildPath; String? engineHostBuildPath; if (localEngine != null) { - engineBuildPath = _fileSystem.path.normalize(_fileSystem.path.join(enginePath, 'out', localEngine)); + engineBuildPath = _fileSystem.path.normalize(_fileSystem.path.join(engineSourcePath, 'out', localEngine)); if (!_fileSystem.isDirectorySync(engineBuildPath)) { throwToolExit(_userMessages.runnerNoEngineBuild(engineBuildPath), exitCode: 2); } - final String basename = _fileSystem.path.basename(engineBuildPath); - final String hostBasename = _getHostEngineBasename(basename); + if (localHostEngine == null) { + throwToolExit(_userMessages.runnerLocalEngineRequiresHostEngine); + } engineHostBuildPath = _fileSystem.path.normalize( - _fileSystem.path.join(_fileSystem.path.dirname(engineBuildPath), hostBasename), + _fileSystem.path.join(_fileSystem.path.dirname(engineBuildPath), localHostEngine), ); if (!_fileSystem.isDirectorySync(engineHostBuildPath)) { throwToolExit(_userMessages.runnerNoEngineBuild(engineHostBuildPath), exitCode: 2); @@ -201,7 +198,7 @@ class LocalEngineLocator { String? webSdkPath; if (localWebSdk != null) { - webSdkPath = _fileSystem.path.normalize(_fileSystem.path.join(enginePath, 'out', localWebSdk)); + webSdkPath = _fileSystem.path.normalize(_fileSystem.path.join(engineSourcePath, 'out', localWebSdk)); if (!_fileSystem.isDirectorySync(webSdkPath)) { throwToolExit(_userMessages.runnerNoWebSdk(webSdkPath), exitCode: 2); } diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index 19a8c948a7382..a6bc9291ca42c 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart @@ -7,6 +7,7 @@ import 'package:package_config/package_config.dart'; import 'package:package_config/package_config_types.dart'; import 'base/common.dart'; +import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/template.dart'; @@ -19,6 +20,38 @@ import 'dart/package_map.dart'; /// https://kotlinlang.org/docs/keyword-reference.html const List<String> kReservedKotlinKeywords = <String>['when', 'in', 'is']; +/// Provides the path where templates used by flutter_tools are stored. +class TemplatePathProvider { + const TemplatePathProvider(); + + /// Returns the directory containing the 'name' template directory. + Directory directoryInPackage(String name, FileSystem fileSystem) { + final String templatesDir = fileSystem.path.join(Cache.flutterRoot!, + 'packages', 'flutter_tools', 'templates'); + return fileSystem.directory(fileSystem.path.join(templatesDir, name)); + } + + /// Returns the directory containing the 'name' template directory in + /// flutter_template_images, to resolve image placeholder against. + /// if 'name' is null, return the parent template directory. + Future<Directory> imageDirectory(String? name, FileSystem fileSystem, Logger logger) async { + final String toolPackagePath = fileSystem.path.join( + Cache.flutterRoot!, 'packages', 'flutter_tools'); + final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json'); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + fileSystem.file(packageFilePath), + logger: logger, + ); + final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; + final Directory templateDirectory = fileSystem.directory(imagePackageLibDir) + .parent + .childDirectory('templates'); + return name == null ? templateDirectory : templateDirectory.childDirectory(name); + } +} + +TemplatePathProvider get templatePathProvider => context.get<TemplatePathProvider>() ?? const TemplatePathProvider(); + /// Expands templates in a directory to a destination. All files that must /// undergo template expansion should end with the '.tmpl' extension. All files /// that should be replaced with the corresponding image from @@ -100,8 +133,8 @@ class Template { required TemplateRenderer templateRenderer, }) async { // All named templates are placed in the 'templates' directory - final Directory templateDir = _templateDirectoryInPackage(name, fileSystem); - final Directory imageDir = await templateImageDirectory(name, fileSystem, logger); + final Directory templateDir = templatePathProvider.directoryInPackage(name, fileSystem); + final Directory imageDir = await templatePathProvider.imageDirectory(name, fileSystem, logger); return Template._( <Directory>[templateDir], <Directory>[imageDir], @@ -122,12 +155,12 @@ class Template { return Template._( <Directory>[ for (final String name in names) - _templateDirectoryInPackage(name, fileSystem), + templatePathProvider.directoryInPackage(name, fileSystem), ], <Directory>[ for (final String name in names) - if ((await templateImageDirectory(name, fileSystem, logger)).existsSync()) - await templateImageDirectory(name, fileSystem, logger), + if ((await templatePathProvider.imageDirectory(name, fileSystem, logger)).existsSync()) + await templatePathProvider.imageDirectory(name, fileSystem, logger), ], fileSystem: fileSystem, logger: logger, @@ -328,7 +361,13 @@ class Template { context['androidIdentifier'] = _escapeKotlinKeywords(androidIdentifier); } - final String renderedContents = _templateRenderer.renderString(templateContents, context); + // Use a copy of the context, + // since the original is used in rendering other templates. + final Map<String, Object?> localContext = finalDestinationFile.path.endsWith('.yaml') + ? _createEscapedContextCopy(context) + : context; + + final String renderedContents = _templateRenderer.renderString(templateContents, localContext); finalDestinationFile.writeAsStringSync(renderedContents); @@ -344,28 +383,19 @@ class Template { } } -Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) { - final String templatesDir = fileSystem.path.join(Cache.flutterRoot!, - 'packages', 'flutter_tools', 'templates'); - return fileSystem.directory(fileSystem.path.join(templatesDir, name)); -} +/// Create a copy of the given [context], escaping its values when necessary. +/// +/// Returns the copied context. +Map<String, Object?> _createEscapedContextCopy(Map<String, Object?> context) { + final Map<String, Object?> localContext = Map<String, Object?>.of(context); + + final String? description = localContext['description'] as String?; + + if (description != null && description.isNotEmpty) { + localContext['description'] = escapeYamlString(description); + } -/// Returns the directory containing the 'name' template directory in -/// flutter_template_images, to resolve image placeholder against. -/// if 'name' is null, return the parent template directory. -Future<Directory> templateImageDirectory(String? name, FileSystem fileSystem, Logger logger) async { - final String toolPackagePath = fileSystem.path.join( - Cache.flutterRoot!, 'packages', 'flutter_tools'); - final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json'); - final PackageConfig packageConfig = await loadPackageConfigWithLogging( - fileSystem.file(packageFilePath), - logger: logger, - ); - final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; - final Directory templateDirectory = fileSystem.directory(imagePackageLibDir) - .parent - .childDirectory('templates'); - return name == null ? templateDirectory : templateDirectory.childDirectory(name); + return localContext; } String _escapeKotlinKeywords(String androidIdentifier) { diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart index 1881fb6473fb0..48a5da2596df5 100644 --- a/packages/flutter_tools/lib/src/test/coverage_collector.dart +++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart @@ -187,8 +187,13 @@ class CoverageCollector extends TestWatcher { if (formatter == null) { final coverage.Resolver usedResolver = resolver ?? this.resolver ?? await CoverageCollector.getResolver(packagesPath); final String packagePath = globals.fs.currentDirectory.path; - final List<String> reportOn = coverageDirectory == null - ? <String>[globals.fs.path.join(packagePath, 'lib')] + // find paths for libraryNames so we can include them to report + final List<String>? libraryPaths = libraryNames + ?.map((String e) => usedResolver.resolve('package:$e')) + .whereType<String>() + .toList(); + final List<String>? reportOn = coverageDirectory == null + ? libraryPaths : <String>[coverageDirectory.path]; formatter = (Map<String, coverage.HitMap> hitmap) => hitmap .formatLcov(usedResolver, reportOn: reportOn, basePath: packagePath); diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart index ec99e517b066a..6400b7a059304 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -441,6 +441,14 @@ class FlutterWebPlatform extends PlatformPlugin { if (_closed) { throw StateError('Load called on a closed FlutterWebPlatform'); } + + final String pathFromTest = _fileSystem.path.relative(path, from: _fileSystem.path.join(_root, 'test')); + final Uri suiteUrl = url.resolveUri(_fileSystem.path.toUri('${_fileSystem.path.withoutExtension(pathFromTest)}.html')); + final String relativePath = _fileSystem.path.relative(_fileSystem.path.normalize(path), from: _fileSystem.currentDirectory.path); + if (_logger.isVerbose) { + _logger.printTrace('Loading test suite $relativePath.'); + } + final PoolResource lockResource = await _suiteLock.request(); final Runtime browser = platform.runtime; @@ -455,17 +463,23 @@ class FlutterWebPlatform extends PlatformPlugin { throw StateError('Load called on a closed FlutterWebPlatform'); } - final String pathFromTest = _fileSystem.path.relative(path, from: _fileSystem.path.join(_root, 'test')); - final Uri suiteUrl = url.resolveUri(_fileSystem.path.toUri('${_fileSystem.path.withoutExtension(pathFromTest)}.html')); - final String relativePath = _fileSystem.path.relative(_fileSystem.path.normalize(path), from: _fileSystem.currentDirectory.path); + if (_logger.isVerbose) { + _logger.printTrace('Running test suite $relativePath.'); + } + final RunnerSuite suite = await _browserManager!.load(relativePath, suiteUrl, suiteConfig, message, onDone: () async { await _browserManager!.close(); _browserManager = null; lockResource.release(); + if (_logger.isVerbose) { + _logger.printTrace('Test suite $relativePath finished.'); + } }); + if (_closed) { throw StateError('Load called on a closed FlutterWebPlatform'); } + return suite; } diff --git a/packages/flutter_tools/lib/src/test/test_compiler.dart b/packages/flutter_tools/lib/src/test/test_compiler.dart index 37db867caa076..1433a7150c260 100644 --- a/packages/flutter_tools/lib/src/test/test_compiler.dart +++ b/packages/flutter_tools/lib/src/test/test_compiler.dart @@ -10,12 +10,17 @@ import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; +import '../base/platform.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../compile.dart'; import '../flutter_plugins.dart'; import '../globals.dart' as globals; +import '../linux/native_assets.dart'; +import '../macos/native_assets.dart'; +import '../native_assets.dart'; import '../project.dart'; +import '../windows/native_assets.dart'; import 'test_time_recorder.dart'; /// A request to the [TestCompiler] for recompilation. @@ -118,6 +123,7 @@ class TestCompiler { initializeFromDill: testFilePath, dartDefines: buildInfo.dartDefines, packagesPath: buildInfo.packagesPath, + frontendServerStarterPath: buildInfo.frontendServerStarterPath, extraFrontEndOptions: buildInfo.extraFrontEndOptions, platform: globals.platform, testCompilation: true, @@ -163,6 +169,51 @@ class TestCompiler { invalidatedRegistrantFiles.add(flutterProject!.dartPluginRegistrant.absolute.uri); } + Uri? nativeAssetsYaml; + if (!buildInfo.buildNativeAssets) { + nativeAssetsYaml = null; + } else { + final Uri projectUri = FlutterProject.current().directory.uri; + final NativeAssetsBuildRunner buildRunner = NativeAssetsBuildRunnerImpl( + projectUri, + buildInfo.packageConfig, + globals.fs, + globals.logger, + ); + if (globals.platform.isMacOS) { + (nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + buildMode: buildInfo.mode, + projectUri: projectUri, + flutterTester: true, + fileSystem: globals.fs, + buildRunner: buildRunner, + ); + } else if (globals.platform.isLinux) { + (nativeAssetsYaml, _) = await buildNativeAssetsLinux( + buildMode: buildInfo.mode, + projectUri: projectUri, + flutterTester: true, + fileSystem: globals.fs, + buildRunner: buildRunner, + ); + } else if (globals.platform.isWindows) { + (nativeAssetsYaml, _) = await buildNativeAssetsWindows( + buildMode: buildInfo.mode, + projectUri: projectUri, + flutterTester: true, + fileSystem: globals.fs, + buildRunner: buildRunner, + ); + } else { + await ensureNoNativeAssetsOrOsIsSupported( + projectUri, + const LocalPlatform().operatingSystem, + globals.fs, + buildRunner, + ); + } + } + final CompilerOutput? compilerOutput = await compiler!.recompile( request.mainUri, <Uri>[request.mainUri, ...invalidatedRegistrantFiles], @@ -171,6 +222,7 @@ class TestCompiler { projectRootPath: flutterProject?.directory.absolute.path, checkDartPluginRegistry: true, fs: globals.fs, + nativeAssetsYaml: nativeAssetsYaml, ); final String? outputPath = compilerOutput?.outputFilename; diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart index 5b9f6fe1a5d0b..241b132a75ab1 100644 --- a/packages/flutter_tools/lib/src/tester/flutter_tester.dart +++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart @@ -11,7 +11,6 @@ import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; -import '../base/os.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../bundle_builder.dart'; @@ -50,13 +49,11 @@ class FlutterTesterDevice extends Device { required Logger logger, required FileSystem fileSystem, required Artifacts artifacts, - required OperatingSystemUtils operatingSystemUtils, }) : _processManager = processManager, _flutterVersion = flutterVersion, _logger = logger, _fileSystem = fileSystem, - _artifacts = artifacts, - _operatingSystemUtils = operatingSystemUtils, + _artifacts = artifacts, super( platformType: null, category: null, @@ -68,7 +65,6 @@ class FlutterTesterDevice extends Device { final Logger _logger; final FileSystem _fileSystem; final Artifacts _artifacts; - final OperatingSystemUtils _operatingSystemUtils; Process? _process; final DevicePortForwarder _portForwarder = const NoOpDevicePortForwarder(); @@ -157,7 +153,7 @@ class FlutterTesterDevice extends Device { buildInfo: buildInfo, mainPath: mainPath, applicationKernelFilePath: applicationKernelFilePath, - platform: getTargetPlatformForName(getNameForHostPlatform(_operatingSystemUtils.hostPlatform)), + platform: TargetPlatform.tester, assetDirPath: assetDirectory.path, ); @@ -258,15 +254,13 @@ class FlutterTesterDevices extends PollingDeviceDiscovery { required ProcessManager processManager, required Logger logger, required FlutterVersion flutterVersion, - required OperatingSystemUtils operatingSystemUtils, }) : _testerDevice = FlutterTesterDevice( kTesterDeviceId, fileSystem: fileSystem, artifacts: artifacts, processManager: processManager, logger: logger, - flutterVersion: flutterVersion, - operatingSystemUtils: operatingSystemUtils, + flutterVersion: flutterVersion, ), super('Flutter tester'); diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 0702b35e7ec51..c2af8566ddb8d 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -279,6 +279,8 @@ abstract class FlutterVersion { localFrameworkCommitDate = DateTime.parse(_gitCommitDate(workingDirectory: flutterRoot)); } on VersionCheckError { return; + } on FormatException { + return; } final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate(); @@ -532,7 +534,13 @@ class _FlutterVersionFromFile extends FlutterVersion { final String devToolsVersion; @override - void ensureVersionFile() {} + void ensureVersionFile() { + _ensureLegacyVersionFile( + fs: fs, + flutterRoot: flutterRoot, + frameworkVersion: frameworkVersion, + ); + } } class _FlutterVersionGit extends FlutterVersion { @@ -599,10 +607,11 @@ class _FlutterVersionGit extends FlutterVersion { @override void ensureVersionFile() { - final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version')); - if (!legacyVersionFile.existsSync()) { - legacyVersionFile.writeAsStringSync(frameworkVersion); - } + _ensureLegacyVersionFile( + fs: fs, + flutterRoot: flutterRoot, + frameworkVersion: frameworkVersion, + ); const JsonEncoder encoder = JsonEncoder.withIndent(' '); final File newVersionFile = FlutterVersion.getVersionFile(fs, flutterRoot); @@ -612,6 +621,17 @@ class _FlutterVersionGit extends FlutterVersion { } } +void _ensureLegacyVersionFile({ + required FileSystem fs, + required String flutterRoot, + required String frameworkVersion, +}) { + final File legacyVersionFile = fs.file(fs.path.join(flutterRoot, 'version')); + if (!legacyVersionFile.existsSync()) { + legacyVersionFile.writeAsStringSync(frameworkVersion); + } +} + /// Checks if the provided [version] is tracking a standard remote. /// /// A "standard remote" is one having the same url as(in order of precedence): @@ -689,6 +709,7 @@ class VersionUpstreamValidator { static final List<String> _standardRemotes = <String>[ 'https://github.com/flutter/flutter.git', 'git@github.com:flutter/flutter.git', + 'ssh://git@github.com/flutter/flutter.git', ]; // Strips ".git" suffix from a given string, preferably an url. diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 6da8494d24de6..60874eccd4d78 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:meta/meta.dart' show visibleForTesting; import 'package:vm_service/vm_service.dart' as vm_service; -import 'android/android_app_link_settings.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/io.dart' as io; @@ -17,7 +16,6 @@ import 'cache.dart'; import 'convert.dart'; import 'device.dart'; import 'globals.dart' as globals; -import 'ios/xcodeproj.dart'; import 'project.dart'; import 'version.dart'; @@ -30,7 +28,6 @@ const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks'; const String kRunInViewMethod = '_flutter.runInView'; const String kListViewsMethod = '_flutter.listViews'; const String kScreenshotSkpMethod = '_flutter.screenshotSkp'; -const String kScreenshotMethod = '_flutter.screenshot'; const String kRenderFrameWithRasterStatsMethod = '_flutter.renderFrameWithRasterStats'; const String kReloadAssetFonts = '_flutter.reloadAssetFonts'; @@ -42,10 +39,6 @@ const String kFlutterVersionServiceName = 'flutterVersion'; const String kCompileExpressionServiceName = 'compileExpression'; const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo'; const String kFlutterGetSkSLServiceName = 'flutterGetSkSL'; -const String kFlutterGetIOSBuildOptionsServiceName = 'flutterGetIOSBuildOptions'; -const String kFlutterGetAndroidBuildVariantsServiceName = 'flutterGetAndroidBuildVariants'; -const String kFlutterGetIOSUniversalLinkSettingsServiceName = 'flutterGetIOSUniversalLinkSettings'; -const String kFlutterGetAndroidAppLinkSettingsName = 'flutterGetAndroidAppLinkSettings'; /// The error response code from an unrecoverable compilation failure. const int kIsolateReloadBarred = 1005; @@ -318,77 +311,6 @@ Future<vm_service.VmService> setUpVmService({ registrationRequests.add(vmService.registerService(kFlutterGetSkSLServiceName, kFlutterToolAlias)); } - if (flutterProject != null) { - vmService.registerServiceCallback(kFlutterGetIOSBuildOptionsServiceName, (Map<String, Object?> params) async { - final XcodeProjectInfo? info = await flutterProject.ios.projectInfo(); - if (info == null) { - return <String, Object>{ - 'result': <String, Object>{ - kResultType: kResultTypeSuccess, - }, - }; - } - return <String, Object>{ - 'result': <String, Object>{ - kResultType: kResultTypeSuccess, - 'targets': info.targets, - 'schemes': info.schemes, - 'buildConfigurations': info.buildConfigurations, - }, - }; - }); - registrationRequests.add( - vmService.registerService(kFlutterGetIOSBuildOptionsServiceName, kFlutterToolAlias), - ); - - vmService.registerServiceCallback(kFlutterGetAndroidBuildVariantsServiceName, (Map<String, Object?> params) async { - final List<String> options = await flutterProject.android.getBuildVariants(); - return <String, Object>{ - 'result': <String, Object>{ - kResultType: kResultTypeSuccess, - 'variants': options, - }, - }; - }); - registrationRequests.add( - vmService.registerService(kFlutterGetAndroidBuildVariantsServiceName, kFlutterToolAlias), - ); - - vmService.registerServiceCallback(kFlutterGetIOSUniversalLinkSettingsServiceName, (Map<String, Object?> params) async { - final XcodeUniversalLinkSettings settings = await flutterProject.ios.universalLinkSettings( - configuration: params['configuration']! as String, - scheme: params['scheme']! as String, - target: params['target']! as String, - ); - return <String, Object>{ - 'result': <String, Object>{ - kResultType: kResultTypeSuccess, - 'bundleIdentifier': settings.bundleIdentifier ?? '', - 'teamIdentifier': settings.teamIdentifier ?? '', - 'associatedDomains': settings.associatedDomains, - }, - }; - }); - registrationRequests.add( - vmService.registerService(kFlutterGetIOSUniversalLinkSettingsServiceName, kFlutterToolAlias), - ); - - vmService.registerServiceCallback(kFlutterGetAndroidAppLinkSettingsName, (Map<String, Object?> params) async { - final String variant = params['variant']! as String; - final AndroidAppLinkSettings settings = await flutterProject.android.getAppLinksSettings(variant: variant); - return <String, Object>{ - 'result': <String, Object>{ - kResultType: kResultTypeSuccess, - 'applicationId': settings.applicationId, - 'domains': settings.domains, - }, - }; - }); - registrationRequests.add( - vmService.registerService(kFlutterGetAndroidAppLinkSettingsName, kFlutterToolAlias), - ); - } - if (printStructuredErrorLogMethod != null) { vmService.onExtensionEvent.listen(printStructuredErrorLogMethod); registrationRequests.add(vmService @@ -809,19 +731,6 @@ class FlutterVmService { ); } - Future<Map<String, Object?>?> flutterFastReassemble({ - required String isolateId, - required String className, - }) { - return invokeFlutterExtensionRpcRaw( - 'ext.flutter.fastReassemble', - isolateId: isolateId, - args: <String, Object>{ - 'className': className, - }, - ); - } - Future<bool> flutterAlreadyPaintedFirstUsefulFrame({ required String isolateId, }) async { @@ -1144,10 +1053,6 @@ class FlutterVmService { ); } - Future<vm_service.Response?> screenshot() { - return _checkedCallServiceExtension(kScreenshotMethod); - } - Future<vm_service.Response?> screenshotSkp() { return _checkedCallServiceExtension(kScreenshotSkpMethod); } diff --git a/packages/flutter_tools/lib/src/web/bootstrap.dart b/packages/flutter_tools/lib/src/web/bootstrap.dart index de08019febe0c..24e924e755fa1 100644 --- a/packages/flutter_tools/lib/src/web/bootstrap.dart +++ b/packages/flutter_tools/lib/src/web/bootstrap.dart @@ -229,10 +229,10 @@ String generateTestEntrypoint({ Future<void> main() async { ui_web.debugEmulateFlutterTesterEnvironment = true; - await ui.webOnlyInitializePlatform(); + await ui_web.bootstrapEngine(); webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}')); - (ui.window as dynamic).debugOverrideDevicePixelRatio(3.0); - (ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800); + ui_web.debugOverrideDevicePixelRatio(3.0); + ui.window.debugPhysicalSizeOverride = const ui.Size(2400, 1800); internalBootstrapBrowserTest(() { return ${testConfigPath != null ? "() => test_config.testExecutable(test.main)" : "test.main"}; @@ -247,25 +247,16 @@ String generateTestEntrypoint({ StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); StreamChannel postMessageChannel() { - var controller = StreamChannelController(sync: true); - window.onMessage.firstWhere((message) { - return message.origin == window.location.origin && message.data == "port"; - }).then((message) { - var port = message.ports.first; - var portSubscription = port.onMessage.listen((message) { - controller.local.sink.add(message.data); - }); - controller.local.stream.listen((data) { - port.postMessage({"data": data}); - }, onDone: () { - port.postMessage({"event": "done"}); - portSubscription.cancel(); - }); + var controller = StreamChannelController<Object?>(sync: true); + var channel = MessageChannel(); + window.parent!.postMessage('port', window.location.origin, [channel.port2]); + + var portSubscription = channel.port1.onMessage.listen((message) { + controller.local.sink.add(message.data); }); - context['parent'].callMethod('postMessage', [ - JsObject.jsify({"href": window.location.href, "ready": true}), - window.location.origin, - ]); + controller.local.stream + .listen(channel.port1.postMessage, onDone: portSubscription.cancel); + return controller.foreign; } '''; diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index 22068d5e3ebde..f35a6634a2d6f 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -171,14 +171,23 @@ class ChromiumLauncher { throwToolExit('Only one instance of chrome can be started.'); } + if (_logger.isVerbose) { + _logger.printTrace('Launching Chromium (url = $url, headless = $headless, skipCheck = $skipCheck, debugPort = $debugPort)'); + } + final String chromeExecutable = _browserFinder(_platform, _fileSystem); - if (_logger.isVerbose && !_platform.isWindows) { - // The "--version" argument is not supported on Windows. - final ProcessResult versionResult = await _processManager.run(<String>[chromeExecutable, '--version']); - _logger.printTrace('Using ${versionResult.stdout}'); + if (_logger.isVerbose) { + _logger.printTrace('Will use Chromium executable at $chromeExecutable'); + + if (!_platform.isWindows) { + // The "--version" argument is not supported on Windows. + final ProcessResult versionResult = await _processManager.run(<String>[chromeExecutable, '--version']); + _logger.printTrace('Using ${versionResult.stdout}'); + } } + final Directory userDataDir = _fileSystem.systemTempDirectory .createTempSync('flutter_tools_chrome_device.'); @@ -216,10 +225,10 @@ class ChromiumLauncher { url, ]; - final Process? process = await _spawnChromiumProcess(args, chromeExecutable); + final Process process = await _spawnChromiumProcess(args, chromeExecutable); // When the process exits, copy the user settings back to the provided data-dir. - if (process != null && cacheDir != null) { + if (cacheDir != null) { unawaited(process.exitCode.whenComplete(() { _cacheUserSessionInformation(userDataDir, cacheDir); // cleanup temp dir @@ -236,10 +245,11 @@ class ChromiumLauncher { url: url, process: process, chromiumLauncher: this, + logger: _logger, ), skipCheck); } - Future<Process?> _spawnChromiumProcess(List<String> args, String chromeExecutable) async { + Future<Process> _spawnChromiumProcess(List<String> args, String chromeExecutable) async { if (_operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm64) { final ProcessResult result = _processManager.runSync(<String>['file', chromeExecutable]); // Check if ARM Chrome is installed. @@ -460,25 +470,58 @@ class Chromium { this.debugPort, this.chromeConnection, { this.url, - Process? process, + required Process process, required ChromiumLauncher chromiumLauncher, + required Logger logger, }) : _process = process, - _chromiumLauncher = chromiumLauncher; + _chromiumLauncher = chromiumLauncher, + _logger = logger; final String? url; final int debugPort; - final Process? _process; + final Process _process; final ChromeConnection chromeConnection; final ChromiumLauncher _chromiumLauncher; + final Logger _logger; + + /// Resolves to browser's main process' exit code, when the browser exits. + Future<int> get onExit async => _process.exitCode; - Future<int?> get onExit async => _process?.exitCode; + /// The main Chromium process that represents this instance of Chromium. + /// + /// Killing this process should result in the browser exiting. + @visibleForTesting + Process get process => _process; + /// Closes all connections to the browser and asks the browser to exit. Future<void> close() async { + if (_logger.isVerbose) { + _logger.printTrace('Shutting down Chromium.'); + } if (_chromiumLauncher.hasChromeInstance) { _chromiumLauncher.currentCompleter = Completer<Chromium>(); } chromeConnection.close(); - _process?.kill(); - await _process?.exitCode; + + // Try to exit Chromium nicely using SIGTERM, before exiting it rudely using + // SIGKILL. Wait no longer than 5 seconds for Chromium to exit before + // falling back to SIGKILL, and then to a warning message. + ProcessSignal.sigterm.kill(_process); + await _process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () { + _logger.printWarning( + 'Failed to exit Chromium (pid: ${_process.pid}) using SIGTERM. Will try ' + 'sending SIGKILL instead.' + ); + ProcessSignal.sigkill.kill(_process); + return _process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () async { + _logger.printWarning( + 'Failed to exit Chromium (pid: ${_process.pid}) using SIGKILL. Giving ' + 'up. Will continue, assuming Chromium has exited successfully, but ' + 'it is possible that this left a dangling Chromium process running ' + 'on the system.' + ); + return 0; + }); + }); } } diff --git a/packages/flutter_tools/lib/src/web/file_generators/js/flutter.js b/packages/flutter_tools/lib/src/web/file_generators/js/flutter.js index d3efa7fd80d9d..4fd0e51f345ee 100644 --- a/packages/flutter_tools/lib/src/web/file_generators/js/flutter.js +++ b/packages/flutter_tools/lib/src/web/file_generators/js/flutter.js @@ -144,7 +144,7 @@ _flutter.loader = null; const serviceWorkerActivation = navigator.serviceWorker .register(url) - .then(this._getNewServiceWorker) + .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion)) .then(this._waitForServiceWorkerActivation); // Timeout race promise @@ -156,53 +156,47 @@ _flutter.loader = null; } /** - * Returns the latest service worker for the given `serviceWorkerRegistrationPromise`. + * Returns the latest service worker for the given `serviceWorkerRegistration`. * * This might return the current service worker, if there's no new service worker * awaiting to be installed/updated. * - * @param {Promise<ServiceWorkerRegistration>} serviceWorkerRegistrationPromise + * @param {ServiceWorkerRegistration} serviceWorkerRegistration + * @param {String} serviceWorkerVersion * @returns {Promise<ServiceWorker>} */ - async _getNewServiceWorker(serviceWorkerRegistrationPromise) { - const reg = await serviceWorkerRegistrationPromise; - - if (!reg.active && (reg.installing || reg.waiting)) { + async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) { + if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) { // No active web worker and we have installed or are installing // one for the first time. Simply wait for it to activate. console.debug("Installing/Activating first service worker."); - return reg.installing || reg.waiting; - } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) { + return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting; + } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) { // When the app updates the serviceWorkerVersion changes, so we // need to ask the service worker to update. - return reg.update().then((newReg) => { - console.debug("Updating service worker."); - return newReg.installing || newReg.waiting || newReg.active; - }); + const newRegistration = await serviceWorkerRegistration.update(); + console.debug("Updating service worker."); + return newRegistration.installing || newRegistration.waiting || newRegistration.active; } else { console.debug("Loading from existing service worker."); - return reg.active; + return serviceWorkerRegistration.active; } } /** - * Returns a Promise that resolves when the `latestServiceWorker` changes its + * Returns a Promise that resolves when the `serviceWorker` changes its * state to "activated". * - * @param {Promise<ServiceWorker>} latestServiceWorkerPromise + * @param {ServiceWorker} serviceWorker * @returns {Promise<void>} */ - async _waitForServiceWorkerActivation(latestServiceWorkerPromise) { - const serviceWorker = await latestServiceWorkerPromise; - + async _waitForServiceWorkerActivation(serviceWorker) { if (!serviceWorker || serviceWorker.state == "activated") { if (!serviceWorker) { - return Promise.reject( - new Error("Cannot activate a null service worker!") - ); + throw new Error("Cannot activate a null service worker!"); } else { console.debug("Service worker already active."); - return Promise.resolve(); + return; } } return new Promise((resolve, _) => { diff --git a/packages/flutter_tools/lib/src/web/file_generators/wasm_bootstrap.dart b/packages/flutter_tools/lib/src/web/file_generators/wasm_bootstrap.dart index c2ee17b496025..0d1a755c0f2c2 100644 --- a/packages/flutter_tools/lib/src/web/file_generators/wasm_bootstrap.dart +++ b/packages/flutter_tools/lib/src/web/file_generators/wasm_bootstrap.dart @@ -46,7 +46,7 @@ String generateImports(bool isSkwasm) { const skwasmInstance = await skwasm(); window._flutter_skwasmInstance = skwasmInstance; resolve({ - 'skwasm': skwasmInstance.asm, + 'skwasm': skwasmInstance.asm ?? skwasmInstance.wasmExports, 'ffi': { 'memory': skwasmInstance.wasmMemory, } diff --git a/packages/flutter_tools/lib/src/windows/application_package.dart b/packages/flutter_tools/lib/src/windows/application_package.dart index bd38ece9ccc0b..a5b3cb6c80ac2 100644 --- a/packages/flutter_tools/lib/src/windows/application_package.dart +++ b/packages/flutter_tools/lib/src/windows/application_package.dart @@ -111,7 +111,7 @@ class BuildableWindowsApp extends WindowsApp { String executable(BuildMode buildMode) { final String? binaryName = getCmakeExecutableName(project); return globals.fs.path.join( - getWindowsBuildDirectory(), + getWindowsBuildDirectory(TargetPlatform.windows_x64), 'runner', sentenceCase(buildMode.cliName), '$binaryName.exe', diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index 6351b66c7ad6c..dddc0d424d2bf 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -18,6 +18,8 @@ import '../convert.dart'; import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; +import '../migrations/cmake_native_assets_migration.dart'; +import 'migrations/build_architecture_migration.dart'; import 'migrations/show_window_migration.dart'; import 'migrations/version_migration.dart'; import 'visual_studio.dart'; @@ -52,10 +54,18 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { 'to learn about adding Windows support to a project.'); } + // TODO(pbo-linaro): Add support for windows-arm64 platform, https://github.com/flutter/flutter/issues/129807 + const TargetPlatform targetPlatform = TargetPlatform.windows_x64; + final Directory buildDirectory = globals.fs.directory( + getWindowsBuildDirectory(targetPlatform) + ); + final List<ProjectMigrator> migrators = <ProjectMigrator>[ CmakeCustomCommandMigration(windowsProject, globals.logger), + CmakeNativeAssetsMigration(windowsProject, 'windows', globals.logger), VersionMigration(windowsProject, globals.logger), ShowWindowMigration(windowsProject, globals.logger), + BuildArchitectureMigration(windowsProject, buildDirectory, globals.logger), ]; final ProjectMigration migration = ProjectMigration(migrators); @@ -79,7 +89,6 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { } final String buildModeName = buildInfo.mode.cliName; - final Directory buildDirectory = globals.fs.directory(getWindowsBuildDirectory()); final Status status = globals.logger.startProgress( 'Building Windows application...', ); @@ -89,6 +98,7 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { generator: cmakeGenerator, buildDir: buildDirectory, sourceDir: windowsProject.cmakeFile.parent, + targetPlatform: targetPlatform, ); if (visualStudio.displayVersion == '17.1.0') { _fixBrokenCmakeGeneration(buildDirectory); @@ -124,7 +134,11 @@ Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { aotSnapshot: codeSizeFile, // This analysis is only supported for release builds. outputDirectory: globals.fs.directory( - globals.fs.path.join(getWindowsBuildDirectory(), 'runner', 'Release'), + globals.fs.path.join( + buildDirectory.path, + 'runner', + 'Release' + ), ), precompilerTrace: precompilerTrace, type: 'windows', @@ -153,11 +167,18 @@ Future<void> _runCmakeGeneration({ required String generator, required Directory buildDir, required Directory sourceDir, + required TargetPlatform targetPlatform, }) async { + if (targetPlatform != TargetPlatform.windows_x64) { + throwToolExit('Windows build supports only x64 target architecture'); + } + final Stopwatch sw = Stopwatch()..start(); await buildDir.create(recursive: true); int result; + const String arch = 'x64'; + const String flutterTargetPlatform = 'windows-x64'; try { result = await globals.processUtils.stream( <String>[ @@ -168,6 +189,9 @@ Future<void> _runCmakeGeneration({ buildDir.path, '-G', generator, + '-A', + arch, + '-DFLUTTER_TARGET_PLATFORM=$flutterTargetPlatform', ], trace: true, ); @@ -190,7 +214,16 @@ Future<void> _runBuild( // MSBuild sends all output to stdout, including build errors. This surfaces // known error patterns. - final RegExp errorMatcher = RegExp(r':\s*(?:warning|(?:fatal )?error).*?:'); + final RegExp errorMatcher = RegExp( + <String>[ + // Known error messages + r'(:\s*(?:warning|(?:fatal )?error).*?:)', + r'Error detected in pubspec\.yaml:', + + // Known secondary error lines for pubspec.yaml + r'No file or variants found for asset:', + ].join('|'), + ); int result; try { @@ -238,11 +271,13 @@ void _writeGeneratedFlutterConfig( }; final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo; if (localEngineInfo != null) { - final String engineOutPath = localEngineInfo.engineOutPath; - environment['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(engineOutPath)); - environment['LOCAL_ENGINE'] = localEngineInfo.localEngineName; + final String targetOutPath = localEngineInfo.targetOutPath; + // Get the engine source root $ENGINE/src/out/foo_bar_baz -> $ENGINE/src + environment['FLUTTER_ENGINE'] = globals.fs.path.dirname(globals.fs.path.dirname(targetOutPath)); + environment['LOCAL_ENGINE'] = localEngineInfo.localTargetName; + environment['LOCAL_ENGINE_HOST'] = localEngineInfo.localHostName; } - writeGeneratedCmakeConfig(Cache.flutterRoot!, windowsProject, buildInfo, environment); + writeGeneratedCmakeConfig(Cache.flutterRoot!, windowsProject, buildInfo, environment, globals.logger); } // Works around the Visual Studio 17.1.0 CMake bug described in diff --git a/packages/flutter_tools/lib/src/windows/migrations/build_architecture_migration.dart b/packages/flutter_tools/lib/src/windows/migrations/build_architecture_migration.dart new file mode 100644 index 0000000000000..442ade5c3ace8 --- /dev/null +++ b/packages/flutter_tools/lib/src/windows/migrations/build_architecture_migration.dart @@ -0,0 +1,126 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../base/file_system.dart'; +import '../../base/project_migrator.dart'; +import '../../cmake_project.dart'; +import 'utils.dart'; + +const String _cmakeFileToolBackendBefore = r''' +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $<CONFIG> + VERBATIM +) +'''; + +const String _cmakeFileToolBackendAfter = r''' +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $<CONFIG> + VERBATIM +) +'''; + +const String _cmakeFileTargetPlatformBefore = r''' +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +'''; + +const String _cmakeFileTargetPlatformAfter = r''' +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +'''; + +/// Migrates Windows build to target specific architecture. +/// In more, it deletes old runner folder +class BuildArchitectureMigration extends ProjectMigrator { + BuildArchitectureMigration( + WindowsProject project, + Directory buildDirectory, + super.logger + ) + : _cmakeFile = project.managedCmakeFile, + _buildDirectory = buildDirectory; + + final File _cmakeFile; + final Directory _buildDirectory; + + @override + void migrate() { + final Directory oldRunnerDirectory = _buildDirectory + .parent + .childDirectory('runner'); + if (oldRunnerDirectory.existsSync()) { + logger.printTrace(''' +Deleting previous build folder ${oldRunnerDirectory.path}. +New binaries can be found in ${_buildDirectory.childDirectory('runner').path}. +'''); + try { + oldRunnerDirectory.deleteSync(recursive: true); + } on FileSystemException catch (error) { + logger.printError( + 'Failed to remove ${oldRunnerDirectory.path}: $error. ' + 'A program may still be using a file in the directory or the directory itself. ' + 'To find and stop such a program, see: ' + 'https://superuser.com/questions/1333118/cant-delete-empty-folder-because-it-is-used' + ); + } + } + + // Skip this migration if the affected file does not exist. This indicates + // the app has done non-trivial changes to its runner and this migration + // might not work as expected if applied. + if (!_cmakeFile.existsSync()) { + logger.printTrace(''' +windows/flutter/CMakeLists.txt file not found, skipping build architecture migration. + +This indicates non-trivial changes have been made to the "windows" folder. +If needed, you can reset it by deleting the "windows" folder and then using the +"flutter create --platforms=windows ." command. +'''); + return; + } + + // Migrate the windows/flutter/CMakeLists.txt file. + final String originalCmakeContents = _cmakeFile.readAsStringSync(); + final String cmakeContentsWithTargetPlatform = replaceFirst( + originalCmakeContents, + _cmakeFileTargetPlatformBefore, + _cmakeFileTargetPlatformAfter, + ); + final String newCmakeContents = replaceFirst( + cmakeContentsWithTargetPlatform, + _cmakeFileToolBackendBefore, + _cmakeFileToolBackendAfter, + ); + if (originalCmakeContents != newCmakeContents) { + logger.printStatus('windows/flutter/CMakeLists.txt does not use FLUTTER_TARGET_PLATFORM, updating.'); + _cmakeFile.writeAsStringSync(newCmakeContents); + } + } +} diff --git a/packages/flutter_tools/lib/src/windows/native_assets.dart b/packages/flutter_tools/lib/src/windows/native_assets.dart new file mode 100644 index 0000000000000..c5f5835d0084f --- /dev/null +++ b/packages/flutter_tools/lib/src/windows/native_assets.dart @@ -0,0 +1,91 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../globals.dart' as globals; +import '../native_assets.dart'; +import 'visual_studio.dart'; + +/// Dry run the native builds. +/// +/// This does not build native assets, it only simulates what the final paths +/// of all assets will be so that this can be embedded in the kernel file. +Future<Uri?> dryRunNativeAssetsWindows({ + required NativeAssetsBuildRunner buildRunner, + required Uri projectUri, + bool flutterTester = false, + required FileSystem fileSystem, +}) { + return dryRunNativeAssetsSingleArchitecture( + buildRunner: buildRunner, + projectUri: projectUri, + flutterTester: flutterTester, + fileSystem: fileSystem, + os: OS.windows, + ); +} + +Future<Iterable<Asset>> dryRunNativeAssetsWindowsInternal( + FileSystem fileSystem, + Uri projectUri, + bool flutterTester, + NativeAssetsBuildRunner buildRunner, +) { + return dryRunNativeAssetsSingleArchitectureInternal( + fileSystem, + projectUri, + flutterTester, + buildRunner, + OS.windows, + ); +} + +Future<(Uri? nativeAssetsYaml, List<Uri> dependencies)> + buildNativeAssetsWindows({ + required NativeAssetsBuildRunner buildRunner, + TargetPlatform? targetPlatform, + required Uri projectUri, + required BuildMode buildMode, + bool flutterTester = false, + Uri? yamlParentDirectory, + required FileSystem fileSystem, +}) { + return buildNativeAssetsSingleArchitecture( + buildRunner: buildRunner, + targetPlatform: targetPlatform, + projectUri: projectUri, + buildMode: buildMode, + flutterTester: flutterTester, + yamlParentDirectory: yamlParentDirectory, + fileSystem: fileSystem, + ); +} + + +Future<CCompilerConfig> cCompilerConfigWindows() async { + final VisualStudio visualStudio = VisualStudio( + fileSystem: globals.fs, + platform: globals.platform, + logger: globals.logger, + processManager: globals.processManager, + ); + + return CCompilerConfig( + cc: _toOptionalFileUri(visualStudio.clPath), + ld: _toOptionalFileUri(visualStudio.linkPath), + ar: _toOptionalFileUri(visualStudio.libPath), + envScript: _toOptionalFileUri(visualStudio.vcvarsPath), + envScriptArgs: <String>[], + ); +} + +Uri? _toOptionalFileUri(String? string) { + if (string == null) { + return null; + } + return Uri.file(string); +} diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart index c1c6cf9f35a3c..5251450a81b4e 100644 --- a/packages/flutter_tools/lib/src/windows/visual_studio.dart +++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart @@ -81,7 +81,7 @@ class VisualStudio { if (_bestVisualStudioDetails == null) { return false; } - return _bestVisualStudioDetails!.isComplete ?? true; + return _bestVisualStudioDetails.isComplete ?? true; } /// True if Visual Studio is launchable. @@ -91,7 +91,7 @@ class VisualStudio { if (_bestVisualStudioDetails == null) { return false; } - return _bestVisualStudioDetails!.isLaunchable ?? true; + return _bestVisualStudioDetails.isLaunchable ?? true; } /// True if the Visual Studio installation is a pre-release version. @@ -184,6 +184,60 @@ class VisualStudio { } } + /// The path to cl.exe, or null if no Visual Studio installation has + /// the components necessary to build. + String? get clPath { + return _getMsvcBinPath('cl.exe'); + } + + /// The path to lib.exe, or null if no Visual Studio installation has + /// the components necessary to build. + String? get libPath { + return _getMsvcBinPath('lib.exe'); + } + + /// The path to link.exe, or null if no Visual Studio installation has + /// the components necessary to build. + String? get linkPath { + return _getMsvcBinPath('link.exe'); + } + + String? _getMsvcBinPath(String executable) { + final VswhereDetails? details = _bestVisualStudioDetails; + if (details == null || !details.isUsable || details.installationPath == null || details.msvcVersion == null) { + return null; + } + + return _fileSystem.path.joinAll(<String>[ + details.installationPath!, + 'VC', + 'Tools', + 'MSVC', + details.msvcVersion!, + 'bin', + 'Hostx64', + 'x64', + executable, + ]); + } + + /// The path to vcvars64.exe, or null if no Visual Studio installation has + /// the components necessary to build. + String? get vcvarsPath { + final VswhereDetails? details = _bestVisualStudioDetails; + if (details == null || !details.isUsable || details.installationPath == null) { + return null; + } + + return _fileSystem.path.joinAll(<String>[ + details.installationPath!, + 'VC', + 'Auxiliary', + 'Build', + 'vcvars64.bat', + ]); + } + /// The major version of the Visual Studio install, as an integer. int? get _majorVersion => fullVersion != null ? int.tryParse(fullVersion!.split('.')[0]) : null; @@ -301,7 +355,12 @@ class VisualStudio { if (whereResult.exitCode == 0) { final List<Map<String, dynamic>>? installations = _tryDecodeVswhereJson(whereResult.stdout); if (installations != null && installations.isNotEmpty) { - return VswhereDetails.fromJson(validateRequirements, installations[0]); + final String? msvcVersion = _findMsvcVersion(installations); + return VswhereDetails.fromJson( + validateRequirements, + installations[0], + msvcVersion, + ); } } } on ArgumentError { @@ -312,6 +371,28 @@ class VisualStudio { return null; } + String? _findMsvcVersion(List<Map<String, dynamic>> installations) { + final String? installationPath = installations[0]['installationPath'] as String?; + String? msvcVersion; + if (installationPath != null) { + final Directory installationDir = _fileSystem.directory(installationPath); + final Directory msvcDir = installationDir + .childDirectory('VC') + .childDirectory('Tools') + .childDirectory('MSVC'); + if (msvcDir.existsSync()) { + final Iterable<Directory> msvcVersionDirs = msvcDir.listSync().whereType<Directory>(); + if (msvcVersionDirs.isEmpty) { + return null; + } + msvcVersion = msvcVersionDirs.last.uri.pathSegments + .where((String e) => e.isNotEmpty) + .last; + } + } + return msvcVersion; + } + List<Map<String, dynamic>>? _tryDecodeVswhereJson(String vswhereJson) { List<dynamic>? result; FormatException? originalError; @@ -443,12 +524,14 @@ class VswhereDetails { required this.isRebootRequired, required this.isPrerelease, required this.catalogDisplayVersion, + required this.msvcVersion, }); /// Create a `VswhereDetails` from the JSON output of vswhere.exe. factory VswhereDetails.fromJson( bool meetsRequirements, - Map<String, dynamic> details + Map<String, dynamic> details, + String? msvcVersion, ) { final Map<String, dynamic>? catalog = details['catalog'] as Map<String, dynamic>?; @@ -467,6 +550,8 @@ class VswhereDetails { // contain replacement characters. displayName: details['displayName'] as String?, catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?, + + msvcVersion: msvcVersion, ); } @@ -511,6 +596,9 @@ class VswhereDetails { /// The user-friendly version. final String? catalogDisplayVersion; + /// The MSVC versions. + final String? msvcVersion; + /// Checks if the Visual Studio installation can be used by Flutter. /// /// Returns false if the installation has issues the user must resolve. diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index add59a60b1b28..48e7041d8aac6 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -7,6 +7,7 @@ import 'base/file_system.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'bundle.dart' as bundle; +import 'convert.dart'; import 'flutter_plugins.dart'; import 'globals.dart' as globals; import 'ios/code_signing.dart'; @@ -214,22 +215,29 @@ class IosProject extends XcodeBasedProject { return parent.isModule || _editableDirectory.existsSync(); } - Future<XcodeUniversalLinkSettings> universalLinkSettings({ + /// Outputs universal link related project settings of the iOS sub-project into + /// a json file. + /// + /// The return future will resolve to string path to the output file. + Future<String> outputsUniversalLinkSettings({ required String configuration, - required String scheme, required String target, }) async { final XcodeProjectBuildContext context = XcodeProjectBuildContext( configuration: configuration, - scheme: scheme, target: target, ); - - return XcodeUniversalLinkSettings( - bundleIdentifier: await _productBundleIdentifierWithBuildContext(context), - teamIdentifier: await _getTeamIdentifier(context), - associatedDomains: await _getAssociatedDomains(context), - ); + final File file = await parent.buildDirectory + .childDirectory('deeplink_data') + .childFile('universal-link-settings-$configuration-$target.json') + .create(recursive: true); + + await file.writeAsString(jsonEncode(<String, Object?>{ + 'bundleIdentifier': await _productBundleIdentifierWithBuildContext(context), + 'teamIdentifier': await _getTeamIdentifier(context), + 'associatedDomains': await _getAssociatedDomains(context), + })); + return file.absolute.path; } /// The product bundle identifier of the host app, or null if not set or if diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index d658af9149643..e77573b019693 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -3,15 +3,15 @@ description: Tools for building Flutter applications homepage: https://flutter.dev environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". archive: 3.3.2 args: 2.4.2 browser_launcher: 1.1.1 - dds: 2.9.0+hotfix - dwds: 19.0.1+1 + dds: 2.9.5 + dwds: 21.0.0 completion: 1.0.1 coverage: 1.6.3 crypto: 3.0.3 @@ -20,24 +20,24 @@ dependencies: html: 0.15.4 http: 0.13.6 intl: 0.18.1 - meta: 1.9.1 - multicast_dns: 0.3.2+3 + meta: 1.10.0 + multicast_dns: 0.3.2+4 mustache_template: 2.0.0 package_config: 2.1.0 process: 4.2.4 fake_async: 1.3.1 - stack_trace: 1.11.0 + stack_trace: 1.11.1 usage: 4.1.1 webdriver: 3.0.2 - webkit_inspection_protocol: 1.2.0 - xml: 6.3.0 + webkit_inspection_protocol: 1.2.1 + xml: 6.4.2 yaml: 3.1.2 native_stack_traces: 0.5.6 shelf: 1.4.1 vm_snapshot_analysis: 0.7.6 uuid: 3.0.7 web_socket_channel: 2.4.0 - stream_channel: 2.1.1 + stream_channel: 2.1.2 shelf_web_socket: 1.0.4 shelf_static: 1.1.2 pub_semver: 2.1.4 @@ -48,28 +48,33 @@ dependencies: http_multi_server: 3.2.1 convert: 3.1.1 async: 2.11.0 - unified_analytics: 2.0.0 + unified_analytics: 4.0.0 + + cli_config: 0.1.1 + graphs: 2.3.1 + native_assets_builder: 0.2.3 + native_assets_cli: 0.2.0 # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading # this, make sure the tests are still running correctly. - test_api: 0.6.0 - test_core: 0.5.3 + test_api: 0.6.1 + test_core: 0.5.6 - vm_service: 11.7.1 + vm_service: 11.10.0 - standard_message_codec: 0.0.1+3 + standard_message_codec: 0.0.1+4 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" built_collection: 5.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - built_value: 8.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + built_value: 8.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" csslib: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - dap: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - dds_service_extensions: 1.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - devtools_shared: 2.24.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + dap: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + dds_service_extensions: 1.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + devtools_shared: 2.26.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fixnum: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -78,8 +83,8 @@ dependencies: js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_rpc_2: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - petitparser: 5.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 6.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_proxy: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -91,19 +96,20 @@ dependencies: term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + yaml_edit: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - collection: 1.17.2 + collection: 1.18.0 file_testing: 3.0.0 pubspec_parse: 1.2.3 checked_yaml: 2.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_annotation: 4.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test: 1.24.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test: 1.24.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: bab7 +# PUBSPEC CHECKSUM: 537c diff --git a/packages/flutter_tools/templates/app/lib/main.dart.tmpl b/packages/flutter_tools/templates/app/lib/main.dart.tmpl index 7768a8e4191c4..78e535632ce35 100644 --- a/packages/flutter_tools/templates/app/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/app/lib/main.dart.tmpl @@ -27,11 +27,11 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart'; {{/withPlatformChannelPluginHook}} -{{#withFfiPluginHook}} +{{#withFfi}} import 'dart:async'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}}; -{{/withFfiPluginHook}} +{{/withFfi}} void main() { runApp(const MyApp()); @@ -50,7 +50,7 @@ class MyApp extends StatelessWidget { // This is the theme of your application. // // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a blue toolbar. Then, without quitting the app, + // the application has a purple toolbar. Then, without quitting the app, // try changing the seedColor in the colorScheme below to Colors.green // and then invoke "hot reload" (save your changes or press the "hot // reload" button in a Flutter-supported IDE, or press "r" if you used @@ -213,7 +213,7 @@ class _MyAppState extends State<MyApp> { } } {{/withPlatformChannelPluginHook}} -{{#withFfiPluginHook}} +{{#withFfi}} class MyApp extends StatefulWidget { const MyApp({super.key}); @@ -279,5 +279,5 @@ class _MyAppState extends State<MyApp> { ); } } -{{/withFfiPluginHook}} +{{/withFfi}} {{/withEmptyMain}} diff --git a/packages/flutter_tools/templates/app_shared/.gitignore.tmpl b/packages/flutter_tools/templates/app_shared/.gitignore.tmpl index 24476c5d1eb55..29a3a5017f048 100644 --- a/packages/flutter_tools/templates/app_shared/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app_shared/.gitignore.tmpl @@ -27,7 +27,6 @@ migrate_working_dir/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ diff --git a/packages/flutter_tools/templates/app_shared/android-java.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/app_shared/android-java.tmpl/build.gradle.tmpl index 1411541a8d260..40c56e3454e02 100644 --- a/packages/flutter_tools/templates/app_shared/android-java.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/app_shared/android-java.tmpl/build.gradle.tmpl @@ -6,7 +6,6 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:{{agpVersion}}' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/build.gradle.tmpl index 1411541a8d260..40c56e3454e02 100644 --- a/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/build.gradle.tmpl @@ -6,7 +6,6 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:{{agpVersion}}' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/flutter_tools/templates/app_shared/android.tmpl/gradle.properties.tmpl b/packages/flutter_tools/templates/app_shared/android.tmpl/gradle.properties.tmpl index 94adc3a3f97aa..598d13fee4463 100644 --- a/packages/flutter_tools/templates/app_shared/android.tmpl/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/app_shared/android.tmpl/gradle.properties.tmpl @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle.tmpl b/packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle.tmpl new file mode 100644 index 0000000000000..56f8773a953e3 --- /dev/null +++ b/packages/flutter_tools/templates/app_shared/android.tmpl/settings.gradle.tmpl @@ -0,0 +1,29 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "{{agpVersion}}" apply false +} + +include ":app" diff --git a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl index a4fc907bec376..580180401d132 100644 --- a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl @@ -127,6 +127,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl index 78f98b83d572f..0c89cfcd82feb 100644 --- a/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app_shared/windows.tmpl/CMakeLists.txt.tmpl @@ -91,6 +91,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/packages/flutter_tools/templates/app_shared/windows.tmpl/flutter/CMakeLists.txt b/packages/flutter_tools/templates/app_shared/windows.tmpl/flutter/CMakeLists.txt index 930d2071a324e..903f4899d6fce 100644 --- a/packages/flutter_tools/templates/app_shared/windows.tmpl/flutter/CMakeLists.txt +++ b/packages/flutter_tools/templates/app_shared/windows.tmpl/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $<CONFIG> + ${FLUTTER_TARGET_PLATFORM} $<CONFIG> VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/packages/flutter_tools/templates/module/android/deferred_component/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/deferred_component/build.gradle.tmpl index 502aa70449aaa..8a728d654336c 100644 --- a/packages/flutter_tools/templates/module/android/deferred_component/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/deferred_component/build.gradle.tmpl @@ -34,8 +34,8 @@ android { } defaultConfig { - minSdkVersion 19 - targetSdkVersion 31 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl index 616e17728a79e..66fb01795ae11 100644 --- a/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:{{gradleVersionForModule}}' + classpath 'com.android.tools.build:gradle:{{agpVersionForModule}}' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl index 94adc3a3f97aa..598d13fee4463 100644 --- a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/flutter_tools/templates/module/common/.gitignore.tmpl b/packages/flutter_tools/templates/module/common/.gitignore.tmpl index 9141595fb95f1..525e05cfbaf9e 100644 --- a/packages/flutter_tools/templates/module/common/.gitignore.tmpl +++ b/packages/flutter_tools/templates/module/common/.gitignore.tmpl @@ -1,7 +1,6 @@ .DS_Store .dart_tool/ -.packages .pub/ .idea/ diff --git a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl index c27006fbc8407..f05c069c1335a 100644 --- a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl +++ b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl @@ -8,9 +8,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -{{^withFfiPluginHook}} +{{^withFfi}} import 'package:{{projectName}}/main.dart'; -{{/withFfiPluginHook}} +{{/withFfi}} {{^withPluginHook}} void main() { diff --git a/packages/flutter_tools/templates/package/.gitignore.tmpl b/packages/flutter_tools/templates/package/.gitignore.tmpl index 96486fd930243..ac5aa9893e489 100644 --- a/packages/flutter_tools/templates/package/.gitignore.tmpl +++ b/packages/flutter_tools/templates/package/.gitignore.tmpl @@ -26,5 +26,4 @@ migrate_working_dir/ /pubspec.lock **/doc/api/ .dart_tool/ -.packages build/ diff --git a/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl b/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl new file mode 100644 index 0000000000000..96486fd930243 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/.gitignore.tmpl @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/flutter_tools/templates/package_ffi/.metadata.tmpl b/packages/flutter_tools/templates/package_ffi/.metadata.tmpl new file mode 100644 index 0000000000000..e1a1dd9321432 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/.metadata.tmpl @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: {{flutterRevision}} + channel: {{flutterChannel}} + +project_type: package_ffi diff --git a/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl b/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl new file mode 100644 index 0000000000000..41cc7d8192ecf --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/CHANGELOG.md.tmpl @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl b/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl new file mode 100644 index 0000000000000..ba75c69f7f217 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/LICENSE.tmpl @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_tools/templates/package_ffi/README.md.tmpl b/packages/flutter_tools/templates/package_ffi/README.md.tmpl new file mode 100644 index 0000000000000..3a636eb722def --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/README.md.tmpl @@ -0,0 +1,49 @@ +# {{projectName}} + +{{description}} + +## Getting Started + +This project is a starting point for a Flutter +[FFI package](https://docs.flutter.dev/development/platform-integration/c-interop), +a specialized package that includes native code directly invoked with Dart FFI. + +## Project stucture + +This template uses the following structure: + +* `src`: Contains the native source code, and a CmakeFile.txt file for building + that source code into a dynamic library. + +* `lib`: Contains the Dart code that defines the API of the plugin, and which + calls into the native code using `dart:ffi`. + +* `bin`: Contains the `build.dart` that performs the external native builds. + +## Buidling and bundling native code + +`build.dart` does the building of native components. + +Bundling is done by Flutter based on the output from `build.dart`. + +## Binding to native code + +To use the native code, bindings in Dart are needed. +To avoid writing these by hand, they are generated from the header file +(`src/{{projectName}}.h`) by `package:ffigen`. +Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`. + +## Invoking native code + +Very short-running native functions can be directly invoked from any isolate. +For example, see `sum` in `lib/{{projectName}}.dart`. + +Longer-running functions should be invoked on a helper isolate to avoid +dropping frames in Flutter applications. +For example, see `sumAsync` in `lib/{{projectName}}.dart`. + +## Flutter help + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl new file mode 100644 index 0000000000000..a5744c1cfbe77 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/analysis_options.yaml.tmpl @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_tools/templates/package_ffi/build.dart.tmpl b/packages/flutter_tools/templates/package_ffi/build.dart.tmpl new file mode 100644 index 0000000000000..3fe2224500d23 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/build.dart.tmpl @@ -0,0 +1,24 @@ +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +const packageName = '{{projectName}}'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: packageName, + assetId: + 'package:$packageName/${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: Logger('')..onRecord.listen((record) => print(record.message)), + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +} diff --git a/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl new file mode 100644 index 0000000000000..c33bb9f92cdaa --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/ffigen.yaml.tmpl @@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: {{pluginDartClass}}Bindings +description: | + Bindings for `src/{{projectName}}.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: 'lib/{{projectName}}_bindings_generated.dart' +headers: + entry-points: + - 'src/{{projectName}}.h' + include-directives: + - 'src/{{projectName}}.h' +ffi-native: +preamble: | + // ignore_for_file: always_specify_types + // ignore_for_file: camel_case_types + // ignore_for_file: non_constant_identifier_names +comments: + style: any + length: full diff --git a/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl b/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl new file mode 100644 index 0000000000000..2c3d5cd443cac --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/lib/projectName.dart.tmpl @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:isolate'; + +import '{{projectName}}_bindings_generated.dart' as bindings; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +int sum(int a, int b) => bindings.sum(a, b); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +/// +/// Modify this to suit your own use case. Example use cases: +/// +/// 1. Reuse a single isolate for various different kinds of requests. +/// 2. Use multiple helper isolates for parallel execution. +Future<int> sumAsync(int a, int b) async { + final SendPort helperIsolateSendPort = await _helperIsolateSendPort; + final int requestId = _nextSumRequestId++; + final _SumRequest request = _SumRequest(requestId, a, b); + final Completer<int> completer = Completer<int>(); + _sumRequests[requestId] = completer; + helperIsolateSendPort.send(request); + return completer.future; +} + +/// A request to compute `sum`. +/// +/// Typically sent from one isolate to another. +class _SumRequest { + final int id; + final int a; + final int b; + + const _SumRequest(this.id, this.a, this.b); +} + +/// A response with the result of `sum`. +/// +/// Typically sent from one isolate to another. +class _SumResponse { + final int id; + final int result; + + const _SumResponse(this.id, this.result); +} + +/// Counter to identify [_SumRequest]s and [_SumResponse]s. +int _nextSumRequestId = 0; + +/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request. +final Map<int, Completer<int>> _sumRequests = <int, Completer<int>>{}; + +/// The SendPort belonging to the helper isolate. +Future<SendPort> _helperIsolateSendPort = () async { + // The helper isolate is going to send us back a SendPort, which we want to + // wait for. + final Completer<SendPort> completer = Completer<SendPort>(); + + // Receive port on the main isolate to receive messages from the helper. + // We receive two types of messages: + // 1. A port to send messages on. + // 2. Responses to requests we sent. + final ReceivePort receivePort = ReceivePort() + ..listen((dynamic data) { + if (data is SendPort) { + // The helper isolate sent us the port on which we can sent it requests. + completer.complete(data); + return; + } + if (data is _SumResponse) { + // The helper isolate sent us a response to a request we sent. + final Completer<int> completer = _sumRequests[data.id]!; + _sumRequests.remove(data.id); + completer.complete(data.result); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Start the helper isolate. + await Isolate.spawn((SendPort sendPort) async { + final ReceivePort helperReceivePort = ReceivePort() + ..listen((dynamic data) { + // On the helper isolate listen to requests and respond to them. + if (data is _SumRequest) { + final int result = bindings.sum_long_running(data.a, data.b); + final _SumResponse response = _SumResponse(data.id, result); + sendPort.send(response); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Send the the port to the main isolate on which we can receive requests. + sendPort.send(helperReceivePort.sendPort); + }, receivePort.sendPort); + + // Wait until the helper isolate has sent us back the SendPort on which we + // can start sending requests. + return completer.future; +}(); diff --git a/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl b/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl new file mode 100644 index 0000000000000..65642b4381817 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl @@ -0,0 +1,30 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +@ffi.Native<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>() +external int sum( + int a, + int b, +); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +@ffi.Native<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>() +external int sum_long_running( + int a, + int b, +); diff --git a/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl b/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl new file mode 100644 index 0000000000000..9237f30b97191 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/pubspec.yaml.tmpl @@ -0,0 +1,19 @@ +name: {{projectName}} +description: {{description}} +version: 0.0.1 +homepage: + +environment: + sdk: {{dartSdkVersionBounds}} + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.2.0 + native_toolchain_c: ^0.2.3 + +dev_dependencies: + ffi: ^2.0.2 + ffigen: ^9.0.0 + flutter_lints: ^2.0.0 + test: ^1.21.0 diff --git a/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl new file mode 100644 index 0000000000000..a0d23594f02de --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.c.tmpl @@ -0,0 +1,29 @@ +#include "{{projectName}}.h" + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) { +#ifdef DEBUG + return a + b + 1000; +#else + return a + b; +#endif +} + +// A longer-lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) { + // Simulate work. +#if _WIN32 + Sleep(5000); +#else + usleep(5000 * 1000); +#endif + return a + b; +} diff --git a/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl new file mode 100644 index 0000000000000..084c64228f465 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/src.tmpl/projectName.h.tmpl @@ -0,0 +1,30 @@ +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> + +#if _WIN32 +#include <windows.h> +#else +#include <pthread.h> +#include <unistd.h> +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b); + +// A longer lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b); diff --git a/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl b/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl new file mode 100644 index 0000000000000..f19bce25aab86 --- /dev/null +++ b/packages/flutter_tools/templates/package_ffi/test/projectName_test.dart.tmpl @@ -0,0 +1,14 @@ +import 'package:test/test.dart'; + +import 'package:{{projectName}}/{{projectName}}.dart'; + +void main() { + test('invoke native function', () { + // Tests are run in debug mode. + expect(sum(24, 18), 1042); + }); + + test('invoke async native callback', () async { + expect(await sumAsync(24, 18), 42); + }); +} diff --git a/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl index 74511b59ced82..319ed1f910a0a 100644 --- a/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl @@ -33,9 +33,11 @@ android { // to bump the version in their app. compileSdkVersion {{compileSdkVersion}} - // Bumping the plugin ndkVersion requires all clients of this plugin to bump - // the version in their app and to download a newer version of the NDK. - ndkVersion "{{ndkVersion}}" + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion // Invoke the shared CMake build with the Android Gradle Plugin. externalNativeBuild { diff --git a/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl b/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl index cb21d861d25b1..11b9f06a22854 100644 --- a/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl +++ b/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl @@ -5,6 +5,7 @@ // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. +// ignore_for_file: type=lint import 'dart:ffi' as ffi; /// Bindings for `src/{{projectName}}.h`. diff --git a/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl b/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl index 96486fd930243..ac5aa9893e489 100644 --- a/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl @@ -26,5 +26,4 @@ migrate_working_dir/ /pubspec.lock **/doc/api/ .dart_tool/ -.packages build/ diff --git a/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl index 89546c72e7604..8cb05910ce97d 100644 --- a/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl @@ -10,6 +10,9 @@ version: {{#withFfiPluginHook}} project_type: plugin_ffi {{/withFfiPluginHook}} +{{#withFfiPackage}} +project_type: package_ffi +{{/withFfiPackage}} {{#withPlatformChannelPluginHook}} project_type: plugin {{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl index 5b0d6b2967ac5..c8d9bbf05af21 100644 --- a/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl @@ -17,10 +17,10 @@ dependencies: plugin_platform_interface: ^2.0.2 dev_dependencies: -{{#withFfiPluginHook}} - ffi: ^2.0.1 - ffigen: ^6.1.2 -{{/withFfiPluginHook}} +{{#withFfi}} + ffi: ^2.0.2 + ffigen: ^9.0.0 +{{/withFfi}} flutter_test: sdk: flutter flutter_lints: ^2.0.0 diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 3729d8909fa75..b43e75d4228e6 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -36,6 +36,7 @@ "templates/app_shared/android.tmpl/app/src/main/res/values/styles.xml", "templates/app_shared/android.tmpl/app/src/profile/AndroidManifest.xml.tmpl", "templates/app_shared/android.tmpl/gradle.properties.tmpl", + "templates/app_shared/android.tmpl/settings.gradle.tmpl", "templates/app_shared/android.tmpl/gradle/wrapper/gradle-wrapper.properties.tmpl", "templates/app_shared/android.tmpl/settings.gradle", "templates/app_shared/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl", @@ -248,6 +249,21 @@ "templates/package/README.md.tmpl", "templates/package/test/projectName_test.dart.tmpl", + "templates/package_ffi/.gitignore.tmpl", + "templates/package_ffi/.metadata.tmpl", + "templates/package_ffi/analysis_options.yaml.tmpl", + "templates/package_ffi/build.dart.tmpl", + "templates/package_ffi/CHANGELOG.md.tmpl", + "templates/package_ffi/ffigen.yaml.tmpl", + "templates/package_ffi/lib/projectName_bindings_generated.dart.tmpl", + "templates/package_ffi/lib/projectName.dart.tmpl", + "templates/package_ffi/LICENSE.tmpl", + "templates/package_ffi/pubspec.yaml.tmpl", + "templates/package_ffi/README.md.tmpl", + "templates/package_ffi/src.tmpl/projectName.c.tmpl", + "templates/package_ffi/src.tmpl/projectName.h.tmpl", + "templates/package_ffi/test/projectName_test.dart.tmpl", + "templates/plugin/android-java.tmpl/build.gradle.tmpl", "templates/plugin/android-java.tmpl/projectName_android.iml.tmpl", "templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl", diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart index ece77ecc4b088..adfbf616176fc 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart @@ -60,7 +60,7 @@ void main() { pubspecFile.writeAsStringSync(''' name: foo_project environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' '''); final File dartFile = fileSystem.file(fileSystem.path.join(directory.path, 'lib', 'main.dart')); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart new file mode 100644 index 0000000000000..44c9c5fa34cd2 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart @@ -0,0 +1,129 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/android/android_builder.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/analyze.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:test/fake.dart'; + +import '../../src/context.dart'; +import '../../src/test_flutter_command_runner.dart'; + +void main() { + + group('Android analyze command', () { + late FileSystem fileSystem; + late Platform platform; + late BufferLogger logger; + late FakeProcessManager processManager; + late Terminal terminal; + late AnalyzeCommand command; + late CommandRunner<void> runner; + late Directory tempDir; + late FakeAndroidBuilder builder; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() async { + fileSystem = MemoryFileSystem.test(); + platform = FakePlatform(); + logger = BufferLogger.test(); + processManager = FakeProcessManager.empty(); + terminal = Terminal.test(); + command = AnalyzeCommand( + artifacts: Artifacts.test(), + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + terminal: terminal, + allProjectValidators: <ProjectValidator>[], + suppressAnalytics: true, + ); + runner = createTestCommandRunner(command); + tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + tempDir.childDirectory('android').createSync(); + + // Setup repo roots + const String homePath = '/home/user/flutter'; + Cache.flutterRoot = homePath; + for (final String dir in <String>['dev', 'examples', 'packages']) { + fileSystem.directory(homePath).childDirectory(dir).createSync(recursive: true); + } + builder = FakeAndroidBuilder(); + + }); + + testUsingContext('can list build variants', () async { + builder.variants = <String>['debug', 'release']; + await runner.run(<String>['analyze', '--android', '--list-build-variants', tempDir.path]); + expect(logger.statusText, contains('["debug","release"]')); + }, overrides: <Type, Generator>{ + AndroidBuilder: () => builder, + }); + + testUsingContext('throw if provide multiple path', () async { + final Directory anotherTempDir = fileSystem.systemTempDirectory.createTempSync('another'); + await expectLater( + runner.run(<String>['analyze', '--android', '--list-build-variants', tempDir.path, anotherTempDir.path]), + throwsA( + isA<Exception>().having( + (Exception e) => e.toString(), + 'description', + contains('The Android analyze can process only one directory path'), + ), + ), + ); + }); + + testUsingContext('can output app link settings', () async { + const String buildVariant = 'release'; + await runner.run(<String>['analyze', '--android', '--output-app-link-settings', '--build-variant=$buildVariant', tempDir.path]); + expect(builder.outputVariant, buildVariant); + }, overrides: <Type, Generator>{ + AndroidBuilder: () => builder, + }); + + testUsingContext('output app link settings throws if no build variant', () async { + await expectLater( + runner.run(<String>['analyze', '--android', '--output-app-link-settings', tempDir.path]), + throwsA( + isA<Exception>().having( + (Exception e) => e.toString(), + 'description', + contains('"--build-variant" must be provided'), + ), + ), + ); + }); + }); +} + +class FakeAndroidBuilder extends Fake implements AndroidBuilder { + List<String> variants = const <String>[]; + String? outputVariant; + + @override + Future<List<String>> getBuildVariants({required FlutterProject project}) async { + return variants; + } + + @override + Future<void> outputsAppLinkSettings(String buildVariant, {required FlutterProject project}) async { + outputVariant = buildVariant; + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart index bf0434b3ca250..4e302df9b8033 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart @@ -192,7 +192,7 @@ void main() { )); await commandRunner.run(<String>['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']); }, overrides: <Type, Generator>{ - Artifacts: () => Artifacts.test(localEngine: 'out/host_release'), + Artifacts: () => Artifacts.testLocalEngine(localEngine: 'out/host_release', localEngineHost: 'out/host_release'), Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), @@ -302,7 +302,7 @@ void main() { 'debug_macos_bundle_flutter_assets', '--dart-define=k=v', '--dart-define-from-file=config']), - throwsToolExit(message: 'Json config define file "--dart-define-from-file=config" is not a file, please fix first!')); + throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config')); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index dd2de79fcb539..d334c8aa9dd87 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -19,6 +19,7 @@ import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/attach.dart'; +import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; import 'package:flutter_tools/src/ios/application_package.dart'; @@ -72,12 +73,12 @@ void main() { testFileSystem = MemoryFileSystem.test(); testFileSystem.directory('lib').createSync(); testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync(); - artifacts = Artifacts.test(); + artifacts = Artifacts.test(fileSystem: testFileSystem); stdio = FakeStdio(); terminal = FakeTerminal(); signals = Signals.test(); processInfo = FakeProcessInfo(); - testDeviceManager = TestDeviceManager(logger: BufferLogger.test()); + testDeviceManager = TestDeviceManager(logger: logger); }); group('with one device and no specified target file', () { @@ -135,7 +136,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -164,6 +164,139 @@ void main() { ), }); + testUsingContext('restores terminal to singleCharMode == false on command exit', () async { + final FakeIOSDevice device = FakeIOSDevice( + portForwarder: portForwarder, + majorSdkVersion: 12, + onGetLogReader: () { + fakeLogReader.addLine('Foo'); + fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort'); + return fakeLogReader; + }, + ); + testDeviceManager.devices = <Device>[device]; + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { + // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. + completer.complete(); + } + }); + final FakeHotRunner hotRunner = FakeHotRunner(); + hotRunner.onAttach = ( + Completer<DebugConnectionInfo>? connectionInfoCompleter, + Completer<void>? appStartedCompleter, + bool allowExistingDdsInstance, + bool enableDevTools, + ) async { + appStartedCompleter?.complete(); + return 0; + }; + hotRunner.exited = false; + hotRunner.isWaitingForVmService = false; + final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory() + ..hotRunner = hotRunner; + + await createTestCommandRunner(AttachCommand( + hotRunnerFactory: hotRunnerFactory, + stdio: stdio, + logger: logger, + terminal: terminal, + signals: signals, + platform: platform, + processInfo: processInfo, + fileSystem: testFileSystem, + )).run(<String>['attach']); + await Future.wait<void>(<Future<void>>[ + completer.future, + fakeLogReader.dispose(), + loggerSubscription.cancel(), + ]); + + expect(terminal.singleCharMode, isFalse); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + ProcessManager: () => FakeProcessManager.any(), + Logger: () => logger, + DeviceManager: () => testDeviceManager, + MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( + mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), + preliminaryMDnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), + logger: logger, + flutterUsage: TestUsage(), + ), + Signals: () => FakeSignals(), + }); + + testUsingContext('local engine artifacts are passed to runner', () async { + const String localEngineSrc = '/path/to/local/engine/src'; + const String localEngineDir = 'host_debug_unopt'; + testFileSystem.directory('$localEngineSrc/out/$localEngineDir').createSync(recursive: true); + final FakeIOSDevice device = FakeIOSDevice( + portForwarder: portForwarder, + majorSdkVersion: 12, + onGetLogReader: () { + fakeLogReader.addLine('Foo'); + fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort'); + return fakeLogReader; + }, + ); + testDeviceManager.devices = <Device>[device]; + final Completer<void> completer = Completer<void>(); + final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { + if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { + // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. + completer.complete(); + } + }); + final FakeHotRunner hotRunner = FakeHotRunner(); + hotRunner.onAttach = ( + Completer<DebugConnectionInfo>? connectionInfoCompleter, + Completer<void>? appStartedCompleter, + bool allowExistingDdsInstance, + bool enableDevTools, + ) async => 0; + hotRunner.exited = false; + hotRunner.isWaitingForVmService = false; + bool passedArtifactTest = false; + final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory() + ..hotRunner = hotRunner + .._artifactTester = (Artifacts artifacts) { + expect(artifacts, isA<CachedLocalEngineArtifacts>()); + // expecting this to be true ensures this test ran + passedArtifactTest = true; + }; + + await createTestCommandRunner(AttachCommand( + hotRunnerFactory: hotRunnerFactory, + stdio: stdio, + logger: logger, + terminal: terminal, + signals: signals, + platform: platform, + processInfo: processInfo, + fileSystem: testFileSystem, + )).run(<String>['attach', '--local-engine-src-path=$localEngineSrc', '--local-engine=$localEngineDir', '--local-engine-host=$localEngineDir']); + await Future.wait<void>(<Future<void>>[ + completer.future, + fakeLogReader.dispose(), + loggerSubscription.cancel(), + ]); + expect(passedArtifactTest, isTrue); + }, overrides: <Type, Generator>{ + Artifacts: () => artifacts, + DeviceManager: () => testDeviceManager, + FileSystem: () => testFileSystem, + Logger: () => logger, + MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( + mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), + preliminaryMDnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), + logger: logger, + flutterUsage: TestUsage(), + ), + ProcessManager: () => FakeProcessManager.empty(), + }); + testUsingContext('succeeds with iOS device with mDNS', () async { final FakeIOSDevice device = FakeIOSDevice( portForwarder: portForwarder, @@ -189,7 +322,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -254,7 +386,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -325,7 +456,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -400,7 +530,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -469,7 +598,6 @@ void main() { } }); final Future<void> task = createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -502,7 +630,6 @@ void main() { }; testDeviceManager.devices = <Device>[device]; expect(() => createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -546,7 +673,6 @@ void main() { final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -590,7 +716,6 @@ void main() { testDeviceManager.devices = <Device>[device]; final AttachCommand command = AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -644,7 +769,6 @@ void main() { await createTestCommandRunner(AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -676,7 +800,6 @@ void main() { testDeviceManager.devices = <Device>[device]; final AttachCommand command = AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -725,7 +848,6 @@ void main() { } }); final Future<void> task = createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -760,7 +882,6 @@ void main() { } }); final Future<void> task = createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -796,7 +917,6 @@ void main() { } }); final Future<void> task = createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -841,7 +961,6 @@ void main() { } }); final Future<void> task = createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -878,7 +997,6 @@ void main() { testUsingContext('exits when no device connected', () async { final AttachCommand command = AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -902,7 +1020,6 @@ void main() { final FakeIOSDevice device = FakeIOSDevice(); testDeviceManager.devices = <Device>[device]; expect(createTestCommandRunner(AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -923,7 +1040,6 @@ void main() { testUsingContext('exits when multiple devices connected', () async { final AttachCommand command = AttachCommand( - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -973,7 +1089,6 @@ void main() { final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -1014,7 +1129,6 @@ void main() { final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, - artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, @@ -1053,6 +1167,15 @@ class FakeHotRunner extends Fake implements HotRunner { }) { return onAttach(connectionInfoCompleter, appStartedCompleter, allowExistingDdsInstance, enableDevTools); } + + @override + bool supportsServiceProtocol = false; + + @override + bool stayResident = true; + + @override + void printHelp({required bool details}) {} } class FakeHotRunnerFactory extends Fake implements HotRunnerFactory { @@ -1060,6 +1183,7 @@ class FakeHotRunnerFactory extends Fake implements HotRunnerFactory { String? dillOutputPath; String? projectRootPath; late List<FlutterDevice> devices; + void Function(Artifacts artifacts)? _artifactTester; @override HotRunner build( @@ -1076,6 +1200,11 @@ class FakeHotRunnerFactory extends Fake implements HotRunnerFactory { bool ipv6 = false, FlutterProject? flutterProject, }) { + if (_artifactTester != null) { + for (final FlutterDevice device in devices) { + _artifactTester!((device.generator! as DefaultResidentCompiler).artifacts); + } + } this.devices = devices; this.dillOutputPath = dillOutputPath; this.projectRootPath = projectRootPath; @@ -1325,9 +1454,6 @@ class FakeAndroidDevice extends Fake implements AndroidDevice { return onGetLogReader!(); } - @override - OverrideArtifacts? get artifactOverrides => null; - @override final PlatformType platformType = PlatformType.android; @@ -1382,9 +1508,6 @@ class FakeIOSDevice extends Fake implements IOSDevice { return onGetLogReader!(); } - @override - OverrideArtifacts? get artifactOverrides => null; - @override final String name = 'name'; @@ -1479,4 +1602,10 @@ class FakeTerminal extends Fake implements AnsiTerminal { @override bool usesTerminalUi = false; + + @override + bool singleCharMode = false; + + @override + Stream<String> get keystrokes => StreamController<String>().stream; } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index 83756298bbaa3..8f0ae0d5b3257 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -17,7 +17,9 @@ import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build_ios.dart'; import 'package:flutter_tools/src/ios/code_signing.dart'; import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; @@ -117,7 +119,7 @@ void main() { 'xcresulttool', 'get', '--path', - _xcBundleFilePath, + _xcBundleDirectoryPath, '--format', 'json', ], @@ -173,7 +175,7 @@ void main() { '-destination', 'generic/platform=iOS', ], - '-resultBundlePath', _xcBundleFilePath, + '-resultBundlePath', _xcBundleDirectoryPath, '-resultBundleVersion', '3', 'FLUTTER_SUPPRESS_ANALYTICS=true', 'COMPILER_INDEX_STORE_ENABLE=NO', @@ -438,6 +440,133 @@ void main() { Usage: () => usage, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + + group('Analytics for impeller plist setting', () { + const String plistContents = ''' +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>FLTEnableImpeller</key> + <false/> +</dict> +</plist> +'''; + const FakeCommand plutilCommand = FakeCommand( + command: <String>[ + '/usr/bin/plutil', '-convert', 'xml1', '-o', '-', '/ios/Runner/Info.plist', + ], + stdout: plistContents, + ); + + testUsingContext('Sends an analytics event when Impeller is enabled', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run( + const <String>['build', 'ios', '--no-pub'] + ); + + expect(usage.events, contains( + const TestUsageEvent( + 'build', 'ios', + label:'plist-impeller-enabled', + parameters:CustomDimensions(), + ), + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ + xattrCommand, + setUpFakeXcodeBuildHandler(onRun: () { + fileSystem.directory('build/ios/Release-iphoneos/Runner.app') + .createSync(recursive: true); + }), + setUpRsyncCommand(onRun: () => + fileSystem.file('build/ios/iphoneos/Runner.app/Frameworks/App.framework/App') + ..createSync(recursive: true) + ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0))), + ]), + Platform: () => macosPlatform, + FileSystemUtils: () => FileSystemUtils( + fileSystem: fileSystem, + platform: macosPlatform, + ), + Usage: () => usage, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + + testUsingContext('Sends an analytics event when Impeller is disabled', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: fileSystem, + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + + fileSystem.file( + fileSystem.path.join('usr', 'bin', 'plutil'), + ).createSync(recursive: true); + + final File infoPlist = fileSystem.file(fileSystem.path.join( + 'ios', 'Runner', 'Info.plist', + ))..createSync(recursive: true); + + infoPlist.writeAsStringSync(plistContents); + + await createTestCommandRunner(command).run( + const <String>['build', 'ios', '--no-pub'] + ); + + expect(usage.events, contains( + const TestUsageEvent( + 'build', 'ios', + label:'plist-impeller-disabled', + parameters:CustomDimensions(), + ), + )); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ + xattrCommand, + setUpFakeXcodeBuildHandler(onRun: () { + fileSystem.directory('build/ios/Release-iphoneos/Runner.app') + .createSync(recursive: true); + }), + setUpRsyncCommand(onRun: () => + fileSystem.file('build/ios/iphoneos/Runner.app/Frameworks/App.framework/App') + ..createSync(recursive: true) + ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0))), + ]), + Platform: () => macosPlatform, + FileSystemUtils: () => FileSystemUtils( + fileSystem: fileSystem, + platform: macosPlatform, + ), + Usage: () => usage, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + FlutterProjectFactory: () => FlutterProjectFactory( + fileSystem: fileSystem, + logger: BufferLogger.test(), + ), + PlistParser: () => PlistParser( + fileSystem: fileSystem, + logger: BufferLogger.test(), + processManager: FakeProcessManager.list(<FakeCommand>[ + plutilCommand, plutilCommand, plutilCommand, + ]), + ), + }); + }); + group('xcresults device', () { testUsingContext('Trace error if xcresult is empty.', () async { final BuildCommand command = BuildCommand( @@ -461,7 +590,7 @@ void main() { ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ xattrCommand, setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }), setUpXCResultCommand(), setUpRsyncCommand(), @@ -495,7 +624,7 @@ void main() { ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ xattrCommand, setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }, stdout: 'Lots of spew from Xcode', ), setUpXCResultCommand(stdout: kSampleResultJsonWithIssues), @@ -530,7 +659,7 @@ void main() { ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ xattrCommand, setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }), setUpXCResultCommand(stdout: kSampleResultJsonWithIssuesToBeDiscarded), setUpRsyncCommand(), @@ -594,7 +723,7 @@ void main() { ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ xattrCommand, setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }), setUpXCResultCommand(stdout: kSampleResultJsonWithProvisionIssue), setUpRsyncCommand(), @@ -628,7 +757,7 @@ void main() { setUpFakeXcodeBuildHandler( exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonWithNoProvisioningProfileIssue), @@ -660,7 +789,7 @@ void main() { ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ xattrCommand, setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }), setUpXCResultCommand(stdout: kSampleResultJsonWithActionIssues), setUpRsyncCommand(), @@ -692,17 +821,17 @@ void main() { exitCode: 1, stdout: '$kConcurrentRunFailureMessage1 $kConcurrentRunFailureMessage2', onRun: () { - fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).childFile('result.xcresult').createSync(recursive: true); } ), // The second xcodebuild is triggered due to above concurrent run failure message. setUpFakeXcodeBuildHandler( onRun: () { // If the file is not cleaned, throw an error, test failure. - if (fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).existsSync()) { + if (fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).existsSync()) { throwToolExit('xcresult bundle file existed.', exitCode: 2); } - fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).childFile('result.xcresult').createSync(recursive: true); } ), setUpXCResultCommand(stdout: kSampleResultJsonNoIssues), @@ -740,7 +869,7 @@ void main() { Runner requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor ''', onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonInvalidIssuesMap), @@ -774,7 +903,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig setUpFakeXcodeBuildHandler( exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonInvalidIssuesMap), @@ -811,7 +940,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig Runner requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor ''', onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonNoIssues), @@ -846,7 +975,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig setUpFakeXcodeBuildHandler( exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonInvalidIssuesMap), @@ -881,7 +1010,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig setUpFakeXcodeBuildHandler( exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonWithNoProvisioningProfileIssue), @@ -916,7 +1045,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig setUpFakeXcodeBuildHandler( exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); } ), setUpXCResultCommand(stdout: kSampleResultJsonWithProvisionIssue), @@ -953,7 +1082,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig simulator: true, exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }, ), setUpXCResultCommand(), @@ -989,7 +1118,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig simulator: true, exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }, ), setUpXCResultCommand(stdout: kSampleResultJsonWithIssues), @@ -1027,7 +1156,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig simulator: true, exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); }, ), setUpXCResultCommand(stdout: kSampleResultJsonWithIssuesToBeDiscarded), @@ -1071,7 +1200,7 @@ Runner requires a provisioning profile. Select a provisioning profile in the Sig }); } -const String _xcBundleFilePath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle'; +const String _xcBundleDirectoryPath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle'; class FakeAndroidSdk extends Fake implements AndroidSdk { @override diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index 622be93c1fdee..e4a9d6ecae00b 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -63,7 +63,8 @@ class FakePlistUtils extends Fake implements PlistParser { @override T? getValueFromFile<T>(String plistFilePath, String key) { - return fileContents[plistFilePath]![key] as T?; + final Map<String, Object>? plistFile = fileContents[plistFilePath]; + return plistFile == null ? null : plistFile[key] as T?; } } @@ -168,6 +169,7 @@ void main() { FakeCommand exportArchiveCommand({ String exportOptionsPlist = '/ExportOptions.plist', File? cachePlist, + bool deleteExportOptionsPlist = false, }) { return FakeCommand( command: <String>[ @@ -189,6 +191,9 @@ void main() { if (cachePlist != null) { cachePlist.writeAsStringSync(fileSystem.file(_exportOptionsPlist).readAsStringSync()); } + if (deleteExportOptionsPlist) { + fileSystem.file(_exportOptionsPlist).deleteSync(); + } } ); } @@ -390,6 +395,37 @@ void main() { XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + testUsingContext('ipa build ignores deletion failure if generatedExportPlist does not exist', () async { + final File cachedExportOptionsPlist = fileSystem.file('/CachedExportOptions.plist'); + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + logger: BufferLogger.test(), + fileSystem: fileSystem, + osUtils: FakeOperatingSystemUtils(), + ); + fakeProcessManager.addCommands(<FakeCommand>[ + xattrCommand, + setUpFakeXcodeBuildHandler(), + exportArchiveCommand( + exportOptionsPlist: _exportOptionsPlist, + cachePlist: cachedExportOptionsPlist, + deleteExportOptionsPlist: true, + ), + ]); + createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run( + const <String>['build', 'ipa', '--no-pub'] + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => fakeProcessManager, + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + testUsingContext('ipa build invokes xcodebuild and archives for app store', () async { final File cachedExportOptionsPlist = fileSystem.file('/CachedExportOptions.plist'); final BuildCommand command = BuildCommand( @@ -954,6 +990,8 @@ void main() { plistUtils.fileContents[plistPath] = <String,String>{ 'CFBundleIdentifier': 'io.flutter.someProject', 'CFBundleDisplayName': 'Awesome Gallery', + // Will not use CFBundleName since CFBundleDisplayName is present. + 'CFBundleName': 'Awesome Gallery 2', 'MinimumOSVersion': '11.0', 'CFBundleVersion': '666', 'CFBundleShortVersionString': '12.34.56', @@ -992,6 +1030,62 @@ void main() { PlistParser: () => plistUtils, }); + testUsingContext( + 'Validate basic Xcode settings with CFBundleDisplayName fallback to CFBundleName', () async { + const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist'; + fakeProcessManager.addCommands(<FakeCommand>[ + xattrCommand, + setUpFakeXcodeBuildHandler(onRun: () { + fileSystem.file(plistPath).createSync(recursive: true); + }), + exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist), + ]); + + createMinimalMockProjectFiles(); + + plistUtils.fileContents[plistPath] = <String,String>{ + 'CFBundleIdentifier': 'io.flutter.someProject', + // Will use CFBundleName since CFBundleDisplayName is absent. + 'CFBundleName': 'Awesome Gallery', + 'MinimumOSVersion': '11.0', + 'CFBundleVersion': '666', + 'CFBundleShortVersionString': '12.34.56', + }; + + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + await createTestCommandRunner(command).run( + <String>['build', 'ipa', '--no-pub']); + + expect( + testLogger.statusText, + contains( + '[✓] App Settings Validation\n' + ' • Version Number: 12.34.56\n' + ' • Build Number: 666\n' + ' • Display Name: Awesome Gallery\n' + ' • Deployment Target: 11.0\n' + ' • Bundle Identifier: io.flutter.someProject\n' + ) + ); + expect( + testLogger.statusText, + contains('To update the settings, please refer to https://docs.flutter.dev/deployment/ios') + ); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => fakeProcessManager, + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + PlistParser: () => plistUtils, + }); + + testUsingContext( 'Validate basic Xcode settings with default bundle identifier prefix', () async { const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist'; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart index 8cb79fb7cc1d7..c9a5547239e52 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart @@ -142,6 +142,52 @@ void main() { }), }); + testUsingContext('Does not allow -O0 optimization level', () async { + final BuildCommand buildCommand = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: fileSystem, + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + final CommandRunner<void> runner = createTestCommandRunner(buildCommand); + setupFileSystemForEndToEndTest(fileSystem); + await expectLater( + () => runner.run(<String>[ + 'build', + 'web', + '--no-pub', '--no-web-resources-cdn', '--dart-define=foo=a', '--dart2js-optimization=O0']), + throwsUsageException(message: '"O0" is not an allowed value for option "dart2js-optimization"'), + ); + + final Directory buildDir = fileSystem.directory(fileSystem.path.join('build', 'web')); + + expect(buildDir.existsSync(), isFalse); + }, overrides: <Type, Generator>{ + Platform: () => fakePlatform, + FileSystem: () => fileSystem, + FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), + ProcessManager: () => FakeProcessManager.any(), + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) { + expect(environment.defines, <String, String>{ + 'TargetFile': 'lib/main.dart', + 'HasWebPlugins': 'true', + 'cspMode': 'false', + 'SourceMaps': 'false', + 'NativeNullAssertions': 'true', + 'ServiceWorkerStrategy': 'offline-first', + 'Dart2jsDumpInfo': 'false', + 'Dart2jsNoFrequencyBasedMinification': 'false', + 'Dart2jsOptimization': 'O3', + 'BuildMode': 'release', + 'DartDefines': 'Zm9vPWE=,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==', + 'DartObfuscation': 'false', + 'TrackWidgetCreation': 'false', + 'TreeShakeIcons': 'true', + }); + }), + }); + testUsingContext('Setup for a web build with a user specified output directory', () async { final BuildCommand buildCommand = BuildCommand( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart index 921b14ecfa30f..f77e830ebdb23 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart @@ -82,9 +82,12 @@ void main() { '-S', fileSystem.path.absolute(fileSystem.path.dirname(buildFilePath)), '-B', - r'build\windows', + r'build\windows\x64', '-G', generator, + '-A', + 'x64', + '-DFLUTTER_TARGET_PLATFORM=windows-x64', ], onRun: onRun, ); @@ -100,7 +103,7 @@ void main() { command: <String>[ _cmakePath, '--build', - r'build\windows', + r'build\windows\x64', '--config', buildMode, ...<String>['--target', 'INSTALL'], @@ -222,21 +225,21 @@ Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Framework Copyright (C) Microsoft Corporation. All rights reserved. Checking Build System - Generating C:/foo/windows/flutter/ephemeral/flutter_windows.dll, [etc], _phony_ - Building Custom Rule C:/foo/windows/flutter/CMakeLists.txt + Generating C:/foo/windows/x64/flutter/ephemeral/flutter_windows.dll, [etc], _phony_ + Building Custom Rule C:/foo/windows/x64/flutter/CMakeLists.txt standard_codec.cc Generating Code... - flutter_wrapper_plugin.vcxproj -> C:\foo\build\windows\flutter\Debug\flutter_wrapper_plugin.lib -C:\foo\windows\runner\main.cpp(18): error C2220: the following warning is treated as an error [C:\foo\build\windows\runner\test.vcxproj] -C:\foo\windows\runner\main.cpp(18): warning C4706: assignment within conditional expression [C:\foo\build\windows\runner\test.vcxproj] -main.obj : error LNK2019: unresolved external symbol "void __cdecl Bar(void)" (?Bar@@YAXXZ) referenced in function wWinMain [C:\foo\build\windows\runner\test.vcxproj] -C:\foo\build\windows\runner\Debug\test.exe : fatal error LNK1120: 1 unresolved externals [C:\foo\build\windows\runner\test.vcxproj] - Building Custom Rule C:/foo/windows/runner/CMakeLists.txt + flutter_wrapper_plugin.vcxproj -> C:\foo\build\windows\x64\flutter\Debug\flutter_wrapper_plugin.lib +C:\foo\windows\x64\runner\main.cpp(18): error C2220: the following warning is treated as an error [C:\foo\build\windows\x64\runner\test.vcxproj] +C:\foo\windows\x64\runner\main.cpp(18): warning C4706: assignment within conditional expression [C:\foo\build\windows\x64\runner\test.vcxproj] +main.obj : error LNK2019: unresolved external symbol "void __cdecl Bar(void)" (?Bar@@YAXXZ) referenced in function wWinMain [C:\foo\build\windows\x64\runner\test.vcxproj] +C:\foo\build\windows\x64\runner\Debug\test.exe : fatal error LNK1120: 1 unresolved externals [C:\foo\build\windows\x64\runner\test.vcxproj] + Building Custom Rule C:/foo/windows/x64/runner/CMakeLists.txt flutter_window.cpp main.cpp -C:\foo\windows\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier [C:\foo\build\windows\runner\test.vcxproj] +C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier [C:\foo\build\windows\x64\runner\test.vcxproj] -- Install configuration: "Debug" - -- Installing: C:/foo/build/windows/runner/Debug/data/icudtl.dat + -- Installing: C:/foo/build/windows/x64/runner/Debug/data/icudtl.dat '''; processManager = FakeProcessManager.list(<FakeCommand>[ @@ -251,11 +254,11 @@ C:\foo\windows\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier ); // Just the warnings and errors should be surfaced. expect(testLogger.errorText, r''' -C:\foo\windows\runner\main.cpp(18): error C2220: the following warning is treated as an error [C:\foo\build\windows\runner\test.vcxproj] -C:\foo\windows\runner\main.cpp(18): warning C4706: assignment within conditional expression [C:\foo\build\windows\runner\test.vcxproj] -main.obj : error LNK2019: unresolved external symbol "void __cdecl Bar(void)" (?Bar@@YAXXZ) referenced in function wWinMain [C:\foo\build\windows\runner\test.vcxproj] -C:\foo\build\windows\runner\Debug\test.exe : fatal error LNK1120: 1 unresolved externals [C:\foo\build\windows\runner\test.vcxproj] -C:\foo\windows\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier [C:\foo\build\windows\runner\test.vcxproj] +C:\foo\windows\x64\runner\main.cpp(18): error C2220: the following warning is treated as an error [C:\foo\build\windows\x64\runner\test.vcxproj] +C:\foo\windows\x64\runner\main.cpp(18): warning C4706: assignment within conditional expression [C:\foo\build\windows\x64\runner\test.vcxproj] +main.obj : error LNK2019: unresolved external symbol "void __cdecl Bar(void)" (?Bar@@YAXXZ) referenced in function wWinMain [C:\foo\build\windows\x64\runner\test.vcxproj] +C:\foo\build\windows\x64\runner\Debug\test.exe : fatal error LNK1120: 1 unresolved externals [C:\foo\build\windows\x64\runner\test.vcxproj] +C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier [C:\foo\build\windows\x64\runner\test.vcxproj] '''); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, @@ -311,7 +314,7 @@ C:\foo\windows\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identifier <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> - <CustomBuild Include="somepath\build\windows\CMakeFiles\8b570225f626c250e12bc1ede88babae\flutter_windows.dll.rule"> + <CustomBuild Include="somepath\build\windows\x64\CMakeFiles\8b570225f626c250e12bc1ede88babae\flutter_windows.dll.rule"> <Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Generating some files</Message> <Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">setlocal "C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" -E env FOO=bar C:/src/flutter/packages/flutter_tools/bin/tool_backend.bat windows-x64 Debug @@ -391,6 +394,7 @@ if %errorlevel% neq 0 goto :VCEnd</Command> final File assembleProject = fileSystem.currentDirectory .childDirectory('build') .childDirectory('windows') + .childDirectory('x64') .childDirectory('flutter') .childFile('flutter_assemble.vcxproj'); assembleProject.createSync(recursive: true); @@ -892,7 +896,7 @@ if %errorlevel% neq 0 goto :VCEnd</Command> ..visualStudioOverride = fakeVisualStudio; setUpMockProjectFilesForBuild(); - fileSystem.file(r'build\windows\runner\Release\app.so') + fileSystem.file(r'build\windows\x64\runner\Release\app.so') ..createSync(recursive: true) ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0)); @@ -962,6 +966,41 @@ if %errorlevel% neq 0 goto :VCEnd</Command> ProcessManager: () => FakeProcessManager.any(), FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), }); + + // Tests the case where stdout contains the error about pubspec.yaml + // And tests the case where stdout contains the error about missing assets + testUsingContext('Windows build extracts errors related to pubspec.yaml from stdout', () async { + final FakeVisualStudio fakeVisualStudio = FakeVisualStudio(); + final BuildWindowsCommand command = BuildWindowsCommand(logger: BufferLogger.test()) + ..visualStudioOverride = fakeVisualStudio; + setUpMockProjectFilesForBuild(); + + const String stdout = r''' +Error detected in pubspec.yaml: +No file or variants found for asset: images/a_dot_burr.jpeg. +'''; + + processManager = FakeProcessManager.list(<FakeCommand>[ + cmakeGenerationCommand(), + buildCommand('Release', + stdout: stdout, + ), + ]); + + await createTestCommandRunner(command).run( + const <String>['windows', '--no-pub'] + ); + // Just the warnings and errors should be surfaced. + expect(testLogger.errorText, r''' +Error detected in pubspec.yaml: +No file or variants found for asset: images/a_dot_burr.jpeg. +'''); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => windowsPlatform, + FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), + }); } class FakeVisualStudio extends Fake implements VisualStudio { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart index d3e86de9695e1..4f98a5b37020e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart @@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/config.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/version.dart'; @@ -48,6 +49,23 @@ void main() { } group('config', () { + testUsingContext('prints all settings with --list', () async { + final ConfigCommand configCommand = ConfigCommand(); + final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand); + await commandRunner.run(<String>['config', '--list']); + expect( + testLogger.statusText, + 'All Settings:\n' + '${allFeatures + .where((Feature e) => e.configSetting != null) + .map((Feature e) => ' ${e.configSetting}: (Not set)') + .join('\n')}' + '\n\n', + ); + }, overrides: <Type, Generator>{ + Usage: () => testUsage, + }); + testUsingContext('throws error on excess arguments', () { final ConfigCommand configCommand = ConfigCommand(); final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand); @@ -196,6 +214,7 @@ void main() { await commandRunner.run(<String>[ 'config', + '--list' ]); expect( @@ -270,20 +289,21 @@ void main() { Usage: () => testUsage, }); - testUsingContext('analytics reported disabled when suppressed', () async { + testUsingContext('analytics reported with help usages', () async { final ConfigCommand configCommand = ConfigCommand(); - final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand); + createTestCommandRunner(configCommand); testUsage.suppressAnalytics = true; - - await commandRunner.run(<String>[ - 'config', - ]); - expect( - testLogger.statusText, + configCommand.usage, containsIgnoringWhitespace('Analytics reporting is currently disabled'), ); + + testUsage.suppressAnalytics = false; + expect( + configCommand.usage, + containsIgnoringWhitespace('Analytics reporting is currently enabled'), + ); }, overrides: <Type, Generator>{ Usage: () => testUsage, }); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart index 088caf9e754a0..a3230581de189 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:args/command_runner.dart'; +import 'package:flutter_tools/src/android/java.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/create.dart'; @@ -10,11 +11,14 @@ import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; +import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; import '../../src/testbed.dart'; @@ -79,6 +83,7 @@ void main() { globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'skeleton'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package'), + globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package_ffi'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'), @@ -109,6 +114,7 @@ void main() { flutterManifest.writeAsStringSync('{"files":[]}'); }, overrides: <Type, Generator>{ DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), }); }); @@ -133,7 +139,13 @@ void main() { await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy5']); expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi'); - })); + + await runner.run(<String>['create', '--no-pub', '--template=package_ffi', 'testy6']); + expect((await command.usageValues).commandCreateProjectType, 'package_ffi'); + }), + overrides: <Type, Generator>{ + Java: () => FakeJava(), + }); testUsingContext('set iOS host language type as usage value', () => testbed.run(() async { final CreateCommand command = CreateCommand(); @@ -152,8 +164,10 @@ void main() { 'testy', ]); expect((await command.usageValues).commandCreateIosLanguage, 'objc'); - - })); + }), + overrides: <Type, Generator>{ + Java: () => FakeJava(), + }); testUsingContext('set Android host language type as usage value', () => testbed.run(() async { final CreateCommand command = CreateCommand(); @@ -170,7 +184,9 @@ void main() { 'testy', ]); expect((await command.usageValues).commandCreateAndroidLanguage, 'java'); - })); + }), overrides: <Type, Generator>{ + Java: () => FakeJava(), + }); testUsingContext('create --offline', () => testbed.run(() async { final CreateCommand command = CreateCommand(); @@ -181,8 +197,32 @@ void main() { expect(command.argParser.options.containsKey('offline'), true); expect(command.shouldUpdateCache, true); }, overrides: <Type, Generator>{ + Java: () => null, Pub: () => fakePub, })); + + testUsingContext('package_ffi template not enabled', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + expect( + runner.run( + <String>[ + 'create', + '--no-pub', + '--template=package_ffi', + 'my_ffi_package', + ], + ), + throwsUsageException( + message: '"package_ffi" is not an allowed value for option "template"', + ), + ); + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags( + isNativeAssetsEnabled: false, // ignore: avoid_redundant_argument_values, If we graduate the feature to true by default, don't break this test. + ), + }); }); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/custom_devices_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/custom_devices_test.dart index d0f3e6ea8f128..96563e92ef461 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/custom_devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/custom_devices_test.dart @@ -238,6 +238,9 @@ class FakeTerminal implements Terminal { @override bool get supportsColor => terminal.supportsColor; + @override + bool get isCliAnimationEnabled => terminal.isCliAnimationEnabled; + @override bool get supportsEmoji => terminal.supportsEmoji; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart index f0e1e4fc50c62..63f8f1fdea815 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart @@ -195,6 +195,63 @@ void main() { Logger: () => notifyingLogger, }); + testUsingContext('printTrace should send daemon.logMessage event when notifyVerbose is enabled', () async { + daemon = Daemon( + daemonConnection, + notifyingLogger: notifyingLogger, + ); + notifyingLogger.notifyVerbose = false; + globals.printTrace('daemon.logMessage test 1'); + notifyingLogger.notifyVerbose = true; + globals.printTrace('daemon.logMessage test 2'); + final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) { + return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'trace'; + }); + expect(response.data['id'], isNull); + expect(response.data['event'], 'daemon.logMessage'); + final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>(); + expect(logMessage['level'], 'trace'); + expect(logMessage['message'], 'daemon.logMessage test 2'); + }, overrides: <Type, Generator>{ + Logger: () => notifyingLogger, + }); + + testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to true', () async { + daemon = Daemon( + daemonConnection, + notifyingLogger: notifyingLogger, + ); + expect(notifyingLogger.notifyVerbose, false); + + daemonStreams.inputs.add(DaemonMessage(<String, Object?>{ + 'id': 0, + 'method': 'daemon.setNotifyVerbose', + 'params': <String, Object?>{ + 'verbose': true, + }, + })); + await daemonStreams.outputs.stream.firstWhere(_notEvent); + expect(notifyingLogger.notifyVerbose, true); + }); + + testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to false', () async { + daemon = Daemon( + daemonConnection, + notifyingLogger: notifyingLogger, + ); + notifyingLogger.notifyVerbose = false; + + daemonStreams.inputs.add(DaemonMessage(<String, Object?>{ + 'id': 0, + 'method': 'daemon.setNotifyVerbose', + 'params': <String, Object?>{ + 'verbose': false, + }, + })); + await daemonStreams.outputs.stream.firstWhere(_notEvent); + expect(notifyingLogger.notifyVerbose, false); + }); + testUsingContext('daemon.shutdown command should stop daemon', () async { daemon = Daemon( daemonConnection, @@ -755,6 +812,19 @@ void main() { expect(bufferLogger.errorText, isEmpty); }); + testUsingContext('sends trace messages in notify verbose mode', () async { + final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger, notifyVerbose: true); + + final Future<LogMessage> messageResult = logger.onMessage.first; + logger.printTrace('hello'); + + final LogMessage message = await messageResult; + + expect(message.level, 'trace'); + expect(message.message, 'hello'); + expect(bufferLogger.errorText, isEmpty); + }); + testUsingContext('buffers messages sent before a subscription', () async { final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart index 86b8d1f74b7f0..c9a2b131ec10a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart @@ -77,11 +77,11 @@ void main() { expect( testLogger.statusText, equals(''' -No devices detected. +No authorized devices detected. Run "flutter emulators" to list and start any available device emulators. -If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips. +If you expected a device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''), ); }, overrides: <Type, Generator>{ @@ -178,16 +178,18 @@ If you expected your device to be detected, please run "flutter doctor" to diagn final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices']); expect(testLogger.statusText, ''' -2 connected devices: +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 1 wirelessly connected device: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -1 wirelessly connected device: +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -200,12 +202,15 @@ wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2 final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices', '--device-connection', 'attached']); expect(testLogger.statusText, ''' -2 connected devices: +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) + +Cannot connect to device ABC -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -217,11 +222,14 @@ webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulato final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices', '--device-connection', 'wireless']); expect(testLogger.statusText, ''' -1 wirelessly connected device: +Found 1 wirelessly connected device: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) +Cannot connect to device ABC + +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -244,12 +252,15 @@ wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2 final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices']); expect(testLogger.statusText, ''' -2 connected devices: +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Cannot connect to device ABC -• Cannot connect to device ABC +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -270,11 +281,14 @@ webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulato final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices']); expect(testLogger.statusText, ''' -1 wirelessly connected device: +Found 1 wirelessly connected device: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) +Cannot connect to device ABC -• Cannot connect to device ABC +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -308,11 +322,11 @@ wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2 equals(''' No devices found yet. Checking for wireless devices... -No devices detected. +No authorized devices detected. Run "flutter emulators" to list and start any available device emulators. -If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips. +If you expected a device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''), ); }, overrides: <Type, Generator>{ @@ -329,11 +343,11 @@ If you expected your device to be detected, please run "flutter doctor" to diagn final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices', '--device-connection', 'attached']); expect(testLogger.statusText, ''' -No devices detected. +No authorized devices detected. Run "flutter emulators" to list and start any available device emulators. -If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips. +If you expected a device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => NoDevicesManager(), @@ -347,11 +361,11 @@ If you expected your device to be detected, please run "flutter doctor" to diagn expect(testLogger.statusText, ''' Checking for wireless devices... -No devices detected. +No authorized devices detected. Run "flutter emulators" to list and start any available device emulators. -If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips. +If you expected a device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => NoDevicesManager(), @@ -449,19 +463,21 @@ If you expected your device to be detected, please run "flutter doctor" to diagn final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices']); expect(testLogger.statusText, ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... -2 wirelessly connected devices: +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Cannot connect to device ABC -• Cannot connect to device ABC +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -477,10 +493,9 @@ wireless ios (mobile) • wireless-ios • ios • iOS 16 (simul terminal = FakeTerminal(supportsColor: true); fakeLogger = FakeBufferLogger(terminal: terminal); fakeLogger.originalStatusText = ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... '''; @@ -491,17 +506,19 @@ Checking for wireless devices... await createTestCommandRunner(command).run(<String>['devices']); expect(fakeLogger.statusText, ''' -2 connected devices: +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) -2 wirelessly connected devices: +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => @@ -525,24 +542,25 @@ wireless ios (mobile) • wireless-ios • ios • iOS 16 (simul await createTestCommandRunner(command).run(<String>['devices']); expect(fakeLogger.statusText, ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... -2 connected devices: +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) -2 wirelessly connected devices: +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager( @@ -560,12 +578,15 @@ wireless ios (mobile) • wireless-ios • ios • iOS 16 (simul expect(testLogger.statusText, ''' Checking for wireless devices... -2 wirelessly connected devices: +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) + +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -588,16 +609,19 @@ wireless ios (mobile) • wireless-ios • ios • iOS 16 (simul final DevicesCommand command = DevicesCommand(); await createTestCommandRunner(command).run(<String>['devices']); expect(testLogger.statusText, ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... No wireless devices were found. -• Cannot connect to device ABC +Cannot connect to device ABC + +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -613,10 +637,9 @@ No wireless devices were found. terminal = FakeTerminal(supportsColor: true); fakeLogger = FakeBufferLogger(terminal: terminal); fakeLogger.originalStatusText = ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... '''; @@ -627,14 +650,17 @@ Checking for wireless devices... await createTestCommandRunner(command).run(<String>['devices']); expect(fakeLogger.statusText, ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) No wireless devices were found. -• Cannot connect to device ABC +Cannot connect to device ABC + +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager( @@ -660,21 +686,23 @@ No wireless devices were found. await createTestCommandRunner(command).run(<String>['devices']); expect(fakeLogger.statusText, ''' -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) Checking for wireless devices... -2 connected devices: - -ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) -webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) +Found 2 connected devices: + ephemeral (mobile) • ephemeral • android-arm • Test SDK (1.2.3) (emulator) + webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulator) No wireless devices were found. -• Cannot connect to device ABC +Cannot connect to device ABC + +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager( @@ -703,12 +731,15 @@ No wireless devices were found. expect(testLogger.statusText, ''' No devices found yet. Checking for wireless devices... -2 wirelessly connected devices: +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Cannot connect to device ABC -• Cannot connect to device ABC +Run "flutter emulators" to list and start any available device emulators. + +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager(devices: deviceList), @@ -733,12 +764,15 @@ No devices found yet. Checking for wireless devices... await createTestCommandRunner(command).run(<String>['devices']); expect(fakeLogger.statusText, ''' -2 wirelessly connected devices: +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) + +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager( @@ -766,12 +800,15 @@ wireless ios (mobile) • wireless-ios • ios • iOS 16 (simul expect(fakeLogger.statusText, ''' No devices found yet. Checking for wireless devices... -2 wirelessly connected devices: +Found 2 wirelessly connected devices: + wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) + wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) + +Cannot connect to device ABC -wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2.3) (emulator) -wireless ios (mobile) • wireless-ios • ios • iOS 16 (simulator) +Run "flutter emulators" to list and start any available device emulators. -• Cannot connect to device ABC +If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips. '''); }, overrides: <Type, Generator>{ DeviceManager: () => _FakeDeviceManager( @@ -845,6 +882,9 @@ class FakeTerminal extends Fake implements AnsiTerminal { @override final bool supportsColor; + @override + bool get isCliAnimationEnabled => supportsColor; + @override bool singleCharMode = false; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart index 9d42023235ab7..c732f97ae7bee 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart @@ -1228,4 +1228,7 @@ class FakeDevice extends Fake implements Device { class FakeTerminal extends Fake implements AnsiTerminal { @override final bool supportsColor = false; + + @override + bool get isCliAnimationEnabled => supportsColor; } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/generate_localizations_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/generate_localizations_test.dart index 552dd37470a8b..4c43876481e86 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/generate_localizations_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/generate_localizations_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/localizations.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/generate_localizations.dart'; +import 'package:flutter_tools/src/localizations/gen_l10n_types.dart'; import '../../integration.shard/test_data/basic_project.dart'; import '../../src/common.dart'; @@ -78,6 +79,11 @@ void main() { "description": "Sample description" } }'''); + fileSystem + .file('pubspec.yaml') + .writeAsStringSync(''' +flutter: + generate: true'''); final GenerateLocalizationsCommand command = GenerateLocalizationsCommand( fileSystem: fileSystem, @@ -109,7 +115,11 @@ void main() { } }'''); fileSystem.file('header.txt').writeAsStringSync('a header file'); - + fileSystem + .file('pubspec.yaml') + .writeAsStringSync(''' +flutter: + generate: true'''); final GenerateLocalizationsCommand command = GenerateLocalizationsCommand( fileSystem: fileSystem, logger: logger, @@ -417,7 +427,7 @@ format: true pubspecFile.writeAsStringSync(''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -442,7 +452,7 @@ format: true ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('throw when generate: false and uses synthetic package when run via commandline options', () async { + testUsingContext('throw when generate: false and uses synthetic package when run via commandline options', () async { final File arbFile = fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb')) ..createSync(recursive: true); arbFile.writeAsStringSync(''' @@ -456,7 +466,7 @@ format: true pubspecFile.writeAsStringSync(''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -475,9 +485,50 @@ format: true () async => createTestCommandRunner(command).run(<String>['gen-l10n', '--synthetic-package']), throwsToolExit(message: 'Attempted to generate localizations code without having the flutter: generate flag turned on.') ); - }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); + + testUsingContext('throws error when unexpected positional argument is provided', () { + final GenerateLocalizationsCommand command = GenerateLocalizationsCommand( + fileSystem: fileSystem, + logger: logger, + artifacts: artifacts, + processManager: processManager, + ); + expect( + () async => createTestCommandRunner(command).run(<String>['gen-l10n', '--synthetic-package', 'false']), + throwsToolExit(message: 'Unexpected positional argument "false".') + ); + }); + + group(AppResourceBundle, () { + testWithoutContext("can be parsed without FormatException when it's content is empty", () { + final File arbFile = fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb')) + ..createSync(recursive: true); + expect(AppResourceBundle(arbFile), isA<AppResourceBundle>()); + }); + + testUsingContext("would not fail the gen-l10n command when it's content is empty", () async { + fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb')).createSync(recursive: true); + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + pubspecFile.writeAsStringSync(BasicProjectWithFlutterGen().pubspec); + final GenerateLocalizationsCommand command = GenerateLocalizationsCommand( + fileSystem: fileSystem, + logger: logger, + artifacts: artifacts, + processManager: processManager, + ); + await createTestCommandRunner(command).run(<String>['gen-l10n']); + + final Directory outputDirectory = fileSystem.directory(fileSystem.path.join('.dart_tool', 'flutter_gen', 'gen_l10n')); + expect(outputDirectory.existsSync(), true); + expect(outputDirectory.childFile('app_localizations_en.dart').existsSync(), true); + expect(outputDirectory.childFile('app_localizations.dart').existsSync(), true); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + }); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/ios_analyze_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/ios_analyze_test.dart new file mode 100644 index 0000000000000..065a0543fe3be --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/ios_analyze_test.dart @@ -0,0 +1,155 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:args/command_runner.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/analyze.dart'; +import 'package:flutter_tools/src/commands/ios_analyze.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/test_flutter_command_runner.dart'; + +void main() { + group('ios analyze command', () { + late BufferLogger logger; + late FileSystem fileSystem; + late Platform platform; + late FakeProcessManager processManager; + late Terminal terminal; + late AnalyzeCommand command; + late CommandRunner<void> runner; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() { + logger = BufferLogger.test(); + fileSystem = MemoryFileSystem.test(); + platform = FakePlatform(); + processManager = FakeProcessManager.empty(); + terminal = Terminal.test(); + command = AnalyzeCommand( + artifacts: Artifacts.test(), + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + terminal: terminal, + allProjectValidators: <ProjectValidator>[], + suppressAnalytics: true, + ); + runner = createTestCommandRunner(command); + + // Setup repo roots + const String homePath = '/home/user/flutter'; + Cache.flutterRoot = homePath; + for (final String dir in <String>['dev', 'examples', 'packages']) { + fileSystem.directory(homePath).childDirectory(dir).createSync(recursive: true); + } + }); + + testWithoutContext('can output json file', () async { + final MockIosProject ios = MockIosProject(); + final MockFlutterProject project = MockFlutterProject(ios); + const String expectedConfig = 'someConfig'; + const String expectedTarget = 'someTarget'; + const String expectedOutputFile = '/someFile'; + ios.outputFileLocation = expectedOutputFile; + await IOSAnalyze( + project: project, + option: IOSAnalyzeOption.outputUniversalLinkSettings, + configuration: expectedConfig, + target: expectedTarget, + logger: logger, + ).analyze(); + expect(logger.statusText, contains(expectedOutputFile)); + expect(ios.outputConfiguration, expectedConfig); + expect(ios.outputTarget, expectedTarget); + }); + + testWithoutContext('can list build options', () async { + final MockIosProject ios = MockIosProject(); + final MockFlutterProject project = MockFlutterProject(ios); + const List<String> targets = <String>['target1', 'target2']; + const List<String> configs = <String>['config1', 'config2']; + ios.expectedProjectInfo = XcodeProjectInfo(targets, configs, const <String>[], logger); + await IOSAnalyze( + project: project, + option: IOSAnalyzeOption.listBuildOptions, + logger: logger, + ).analyze(); + final Map<String, Object?> jsonOutput = jsonDecode(logger.statusText) as Map<String, Object?>; + expect(jsonOutput['targets'], unorderedEquals(targets)); + expect(jsonOutput['configurations'], unorderedEquals(configs)); + }); + + testUsingContext('throws if provide multiple path', () async { + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('someTemp'); + final Directory anotherTempDir = fileSystem.systemTempDirectory.createTempSync('another'); + await expectLater( + runner.run(<String>['analyze', '--ios', '--list-build-options', tempDir.path, anotherTempDir.path]), + throwsA( + isA<Exception>().having( + (Exception e) => e.toString(), + 'description', + contains('The iOS analyze can process only one directory path'), + ), + ), + ); + }); + + testUsingContext('throws if not enough parameters', () async { + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('someTemp'); + await expectLater( + runner.run(<String>['analyze', '--ios', '--output-universal-link-settings', tempDir.path]), + throwsA( + isA<Exception>().having( + (Exception e) => e.toString(), + 'description', + contains('"--configuration" must be provided'), + ), + ), + ); + }); + }); +} + +class MockFlutterProject extends Fake implements FlutterProject { + MockFlutterProject(this.ios); + + @override + final IosProject ios; +} + +class MockIosProject extends Fake implements IosProject { + String? outputConfiguration; + String? outputTarget; + late String outputFileLocation; + late XcodeProjectInfo expectedProjectInfo; + + @override + Future<String> outputsUniversalLinkSettings({required String configuration, required String target}) async { + outputConfiguration = configuration; + outputTarget = target; + return outputFileLocation; + } + @override + Future<XcodeProjectInfo> projectInfo() async => expectedProjectInfo; + +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index ca7564e59e9b6..2a5a644a85c07 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1335,13 +1335,10 @@ class FakeIOSDevice extends Fake implements IOSDevice { class TestRunCommandForUsageValues extends RunCommand { TestRunCommandForUsageValues({ - this.devices, - }); - - @override - // devices is not set within usageValues, so we override the field - // ignore: overridden_fields - List<Device>? devices; + List<Device>? devices, + }) { + this.devices = devices; + } @override Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/screenshot_command_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/screenshot_command_test.dart index 42954dc440a40..01725cc991394 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/screenshot_command_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/screenshot_command_test.dart @@ -31,11 +31,6 @@ void main() { .run(<String>['screenshot', '--type=skia', '--vm-service-url=http://localhost:8181']), throwsA(isException.having((Exception exception) => exception.toString(), 'message', contains('dummy'))), ); - - await expectLater(() => createTestCommandRunner(ScreenshotCommand(fs: MemoryFileSystem.test())) - .run(<String>['screenshot', '--type=rasterizer', '--vm-service-url=http://localhost:8181']), - throwsA(isException.having((Exception exception) => exception.toString(), 'message', contains('dummy'))), - ); }); @@ -44,11 +39,6 @@ void main() { .run(<String>['screenshot', '--type=skia']), throwsToolExit(message: 'VM Service URI must be specified for screenshot type skia') ); - - await expectLater(() => createTestCommandRunner(ScreenshotCommand(fs: MemoryFileSystem.test())) - .run(<String>['screenshot', '--type=rasterizer',]), - throwsToolExit(message: 'VM Service URI must be specified for screenshot type rasterizer'), - ); }); testUsingContext('device screenshots require device', () async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index 2c8d81e6e47aa..c0da0c933adee 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -16,14 +16,19 @@ import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:flutter_tools/src/test/coverage_collector.dart'; import 'package:flutter_tools/src/test/runner.dart'; +import 'package:flutter_tools/src/test/test_device.dart'; import 'package:flutter_tools/src/test/test_time_recorder.dart'; import 'package:flutter_tools/src/test/test_wrapper.dart'; import 'package:flutter_tools/src/test/watcher.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_devices.dart'; +import '../../src/fake_vm_services.dart'; import '../../src/logging_logger.dart'; import '../../src/test_flutter_command_runner.dart'; @@ -249,6 +254,137 @@ dev_dependencies: Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); + testUsingContext('Coverage provides current library name to Coverage Collector by default', () async { + const String currentPackageName = ''; + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: <VmServiceExpectation>[ + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse(<String, Object>{})! + ..isolates = <IsolateRef>[ + IsolateRef.parse(<String, Object>{ + 'id': '1', + })!, + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 57).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: <String, Object>{ + 'isolateId': '1', + 'reports': <Object>['Coverage'], + 'forceCompile': true, + 'reportLines': true, + 'libraryFilters': <String>['package:$currentPackageName/'], + }, + jsonResponse: SourceReport( + ranges: <SourceReportRange>[], + ).toJson(), + ), + ], + ); + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0, null, fakeVmServiceHost); + + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner<void> commandRunner = + createTestCommandRunner(testCommand); + await commandRunner.run(const <String>[ + 'test', + '--no-pub', + '--coverage', + '--', + 'test/some_test.dart', + ]); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + expect( + (testRunner.lastTestWatcher! as CoverageCollector).libraryNames, + <String>{currentPackageName}, + ); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + }); + + testUsingContext('Coverage provides library names matching regexps to Coverage Collector', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: <VmServiceExpectation>[ + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse(<String, Object>{})! + ..isolates = <IsolateRef>[ + IsolateRef.parse(<String, Object>{ + 'id': '1', + })!, + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 57).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: <String, Object>{ + 'isolateId': '1', + 'reports': <Object>['Coverage'], + 'forceCompile': true, + 'reportLines': true, + 'libraryFilters': <String>['package:test_api/'], + }, + jsonResponse: SourceReport( + ranges: <SourceReportRange>[], + ).toJson(), + ), + ], + ); + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0, null, fakeVmServiceHost); + + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner<void> commandRunner = + createTestCommandRunner(testCommand); + await commandRunner.run(const <String>[ + 'test', + '--no-pub', + '--coverage', + '--coverage-package=^test', + '--', + 'test/some_test.dart', + ]); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + expect( + (testRunner.lastTestWatcher! as CoverageCollector).libraryNames, + <String>{'test_api'}, + ); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + }); + + testUsingContext('Coverage provides error message if regular expression syntax is invalid', () async { + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); + + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand); + + expect(() => commandRunner.run(const <String>[ + 'test', + '--no-pub', + '--coverage', + r'--coverage-package="$+"', + '--', + 'test/some_test.dart', + ]), throwsToolExit(message: RegExp(r'Regular expression syntax is invalid. FormatException: Nothing to repeat[ \t]*"\$\+"'))); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + testUsingContext('Pipes start-paused to package:test', () async { final FakePackageTest fakePackageTest = FakePackageTest(); @@ -864,7 +1000,7 @@ dev_dependencies: } class FakeFlutterTestRunner implements FlutterTestRunner { - FakeFlutterTestRunner(this.exitCode, [this.leastRunTime]); + FakeFlutterTestRunner(this.exitCode, [this.leastRunTime, this.fakeVmServiceHost]); int exitCode; Duration? leastRunTime; @@ -873,6 +1009,8 @@ class FakeFlutterTestRunner implements FlutterTestRunner { String? lastFileReporterValue; String? lastReporterOption; int? lastConcurrency; + TestWatcher? lastTestWatcher; + FakeVmServiceHost? fakeVmServiceHost; @override Future<int> runTests( @@ -912,15 +1050,39 @@ class FakeFlutterTestRunner implements FlutterTestRunner { lastFileReporterValue = fileReporter; lastReporterOption = reporter; lastConcurrency = concurrency; + lastTestWatcher = watcher; if (leastRunTime != null) { await Future<void>.delayed(leastRunTime!); } + if (watcher is CoverageCollector) { + await watcher.collectCoverage( + TestTestDevice(), + serviceOverride: fakeVmServiceHost?.vmService, + ); + } + return exitCode; } } +class TestTestDevice extends TestDevice { + @override + Future<void> get finished => Future<void>.delayed(const Duration(seconds: 1)); + + @override + Future<void> kill() => Future<void>.value(); + + @override + Future<Uri?> get vmServiceUri => Future<Uri?>.value(Uri()); + + @override + Future<StreamChannel<String>> start(String entrypointPath) { + throw UnimplementedError(); + } +} + class FakePackageTest implements TestWrapper { List<String>? lastArgs; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart index 7e75ca9bfbb0a..7ca7ca01489a3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/update_packages_test.dart @@ -23,7 +23,7 @@ description: A framework for writing Flutter applications homepage: http://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -60,7 +60,7 @@ homepage: http://flutter.dev version: 1.0.0 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=2.5.0-6.0.pre.30 <3.0.0" dependencies: diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart index a88a3ad534cf4..8fdbac1efc4e7 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart @@ -6,6 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/android/java.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; @@ -295,6 +296,7 @@ void main() { }, overrides: <Type, Generator>{ FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + Java: () => null, ProcessManager: () => processManager, FeatureFlags: () => TestFeatureFlags(isIOSEnabled: false), AndroidStudio: () => FakeAndroidStudio(), diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart index 515c3a7d9f8d9..8e82ff48702fc 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart @@ -6,6 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/android/java.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/version.dart'; @@ -143,6 +144,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => null, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, AndroidStudio: () => FakeAndroidStudio(), @@ -174,6 +176,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, AndroidStudio: () => FakeAndroidStudio(), @@ -205,6 +208,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, AndroidStudio: () => FakeAndroidStudio(), @@ -236,6 +240,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, AndroidStudio: () => FakeAndroidStudio(), @@ -269,6 +274,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, AndroidStudio: () => FakeAndroidStudio(), @@ -320,6 +326,7 @@ void main() { }, overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, + Java: () => null, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => processManager, Usage: () => testUsage, @@ -374,6 +381,7 @@ void main() { overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + Java: () => null, ProcessManager: () => processManager, Usage: () => testUsage, AndroidStudio: () => FakeAndroidStudio(), @@ -420,6 +428,7 @@ void main() { overrides: <Type, Generator>{ AndroidSdk: () => mockAndroidSdk, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + Java: () => null, ProcessManager: () => processManager, Usage: () => testUsage, AndroidStudio: () => FakeAndroidStudio(), diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart index c4a501897f551..f20ec4999dd85 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart @@ -559,6 +559,42 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); + testUsingContext('values from --dart-define supersede values from --dart-define-from-file', () async { + globals.fs + .file(globals.fs.path.join('lib', 'main.dart')) + .createSync(recursive: true); + globals.fs.file('pubspec.yaml').createSync(); + globals.fs.file('.packages').createSync(); + globals.fs.file('.env').writeAsStringSync(''' + MY_VALUE=VALUE_FROM_ENV_FILE + '''); + final CommandRunner<void> runner = + createTestCommandRunner(BuildBundleCommand( + logger: BufferLogger.test(), + )); + + await runner.run(<String>[ + 'bundle', + '--no-pub', + '--dart-define=MY_VALUE=VALUE_FROM_COMMAND', + '--dart-define-from-file=.env', + ]); + + }, overrides: <Type, Generator>{ + BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), + (Target target, Environment environment) { + expect( + _decodeDartDefines(environment), + containsAllInOrder(const <String>[ + 'MY_VALUE=VALUE_FROM_ENV_FILE', + 'MY_VALUE=VALUE_FROM_COMMAND', + ]), + ); + }), + FileSystem: fsFactory, + ProcessManager: () => FakeProcessManager.any(), + }); + testUsingContext('--dart-define-from-file correctly parses a valid env file', () async { globals.fs .file(globals.fs.path.join('lib', 'main.dart')) @@ -763,7 +799,7 @@ void main() { 'bundle', '--no-pub', '--dart-define-from-file=config', - ]), throwsToolExit(message: 'Json config define file "--dart-define-from-file=config" is not a file, please fix first!')); + ]), throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config')); }, overrides: <Type, Generator>{ FileSystem: fsFactory, BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)), @@ -820,6 +856,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem, }) async {} } diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index 9069a35351744..0dee658224120 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -8,18 +8,22 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/android/gradle_utils.dart' show templateAndroidGradlePluginVersion, templateAndroidGradlePluginVersionForModule, templateDefaultGradleVersion; +import 'package:flutter_tools/src/android/java.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/version.dart' as software; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/create.dart'; import 'package:flutter_tools/src/commands/create_base.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/flutter_project_metadata.dart' show FlutterProjectType; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/version.dart'; @@ -40,6 +44,8 @@ const String _kNoPlatformsMessage = "You've created a plugin project that doesn' const String frameworkRevision = '12345678'; const String frameworkChannel = 'omega'; const String _kDisabledPlatformRequestedMessage = 'currently not supported on your local environment.'; +const String _kIncompatibleJavaVersionMessage = 'The configured version of Java detected may conflict with the'; +final String _kIncompatibleAgpVersionForModule = Version.parse(templateAndroidGradlePluginVersion) < Version.parse(templateAndroidGradlePluginVersionForModule) ? templateAndroidGradlePluginVersionForModule : templateAndroidGradlePluginVersion; // This needs to be created from the local platform due to re-entrant flutter calls made in this test. FakePlatform _kNoColorTerminalPlatform() => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; @@ -735,10 +741,13 @@ void main() { testUsingContext('plugin project with invalid custom project name', () async { expect( () => _createProject(projectDir, - <String>['--no-pub', '--template=plugin', '--project-name', 'xyz.xyz', '--platforms', 'android,ios',], + <String>['--no-pub', '--template=plugin', '--project-name', 'xyz-xyz', '--platforms', 'android,ios',], <String>[], ), - throwsToolExit(message: '"xyz.xyz" is not a valid Dart package name.'), + allOf( + throwsToolExit(message: '"xyz-xyz" is not a valid Dart package name.'), + throwsToolExit(message: 'Try "xyz_xyz" instead.'), + ), ); }); @@ -1414,6 +1423,7 @@ void main() { expect(xcodeProject, contains('DEVELOPMENT_TEAM = 3333CCCC33;')); }, overrides: <Type, Generator>{ FlutterVersion: () => fakeFlutterVersion, + Java: () => null, Platform: _kNoColorTerminalMacOSPlatform, ProcessManager: () => fakeProcessManager, }); @@ -2607,6 +2617,18 @@ void main() { , throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2)); }); + testUsingContext('create an ffi package with --platforms throws error.', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + await expectLater( + runner.run(<String>['create', '--no-pub', '--template=package_ffi', '--platform=ios', projectDir.path]) + , throwsToolExit(message: 'The "--platforms" argument is not supported', exitCode: 2)); + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); + testUsingContext('create a plugin with android, delete then re-create folders', () async { Cache.flutterRoot = '../..'; @@ -3266,6 +3288,24 @@ void main() { ), }); + testUsingContext('should escape ":" in project description', () async { + await _createProject( + projectDir, + <String>[ + '--no-pub', + '--description', + 'a: b', + ], + <String>[ + 'pubspec.yaml', + ], + ); + + final String rawPubspec = await projectDir.childFile('pubspec.yaml').readAsString(); + final Pubspec pubspec = Pubspec.parse(rawPubspec); + expect(pubspec.description, 'a: b'); + }); + testUsingContext('create an FFI plugin with ios, then add macos', () async { Cache.flutterRoot = '../..'; @@ -3294,75 +3334,329 @@ void main() { FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }); - testUsingContext('FFI plugins error android language', () async { + for (final String template in <String>['package_ffi', 'plugin_ffi']) { + testUsingContext('$template error android language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<String> args = <String>[ + 'create', + '--no-pub', + '--template=$template', + '-a', + 'kotlin', + if (template == 'plugin_ffi') '--platforms=android', + projectDir.path, + ]; + + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "android-language" option is not supported with the $template template: the language will always be C or C++.'), + ); + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); + + testUsingContext('$template error ios language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<String> args = <String>[ + 'create', + '--no-pub', + '--template=$template', + '--ios-language', + 'swift', + if (template == 'plugin_ffi') '--platforms=ios', + projectDir.path, + ]; + + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "ios-language" option is not supported with the $template template: the language will always be C or C++.'), + ); + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); + } + + testUsingContext('FFI plugins error web platform', () async { final CreateCommand command = CreateCommand(); final CommandRunner<void> runner = createTestCommandRunner(command); final List<String> args = <String>[ 'create', '--no-pub', '--template=plugin_ffi', - '-a', - 'kotlin', - '--platforms=android', + '--platforms=web', projectDir.path, ]; await expectLater( runner.run(args), - throwsToolExit(message: 'The "android-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), + throwsToolExit(message: 'The web platform is not supported in plugin_ffi template.'), ); }); - testUsingContext('FFI plugins error ios language', () async { + testUsingContext('should show warning when disabled platforms are selected while creating an FFI plugin', () async { + Cache.flutterRoot = '../..'; + final CreateCommand command = CreateCommand(); final CommandRunner<void> runner = createTestCommandRunner(command); - final List<String> args = <String>[ - 'create', - '--no-pub', - '--template=plugin_ffi', - '--ios-language', - 'swift', - '--platforms=ios', - projectDir.path, - ]; - await expectLater( - runner.run(args), - throwsToolExit(message: 'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), - ); + await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', '--platforms=android,ios,windows,macos,linux', projectDir.path]); + await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', projectDir.path]); + expect(logger.statusText, contains(_kDisabledPlatformRequestedMessage)); + + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(), + Logger: () => logger, }); - testUsingContext('FFI plugins error web platform', () async { + testUsingContext('should not show warning for incompatible Java/template Gradle versions when Java version not found', () async { + Cache.flutterRoot = '../..'; + final CreateCommand command = CreateCommand(); final CommandRunner<void> runner = createTestCommandRunner(command); - final List<String> args = <String>[ - 'create', - '--no-pub', - '--template=plugin_ffi', - '--platforms=web', - projectDir.path, - ]; - await expectLater( - runner.run(args), - throwsToolExit(message: 'The web platform is not supported in plugin_ffi template.'), - ); + await runner.run(<String>['create', '--no-pub', '--platforms=android', projectDir.path]); + + expect(logger.warningText, isNot(contains(_kIncompatibleJavaVersionMessage))); + }, overrides: <Type, Generator>{ + Java: () => null, + Logger: () => logger, }); - testUsingContext('should show warning when disabled platforms are selected while creating an FFI plugin', () async { + testUsingContext('should not show warning for incompatible Java/template Gradle versions when created project type is irrelevant', () async { Cache.flutterRoot = '../..'; final CreateCommand command = CreateCommand(); final CommandRunner<void> runner = createTestCommandRunner(command); - await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', '--platforms=android,ios,windows,macos,linux', projectDir.path]); + // Test not creating a project for Android. + await runner.run(<String>['create', '--no-pub', '--platforms=ios,windows,macos,linux', projectDir.path]); + tryToDelete(projectDir); + // Test creating a package (Dart-only code). + await runner.run(<String>['create', '--no-pub', '--template=package', projectDir.path]); + tryToDelete(projectDir); + // Test creating project types without configured Gradle versions. + await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]); + tryToDelete(projectDir); await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', projectDir.path]); - expect(logger.statusText, contains(_kDisabledPlatformRequestedMessage)); + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'app')))); + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'package')))); + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'plugin')))); + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'pluginFfi')))); }, overrides: <Type, Generator>{ - FeatureFlags: () => TestFeatureFlags(), + Java: () => FakeJava(version: const software.Version.withText(1000, 0, 0, '1000.0.0')), // Too high a version for template Gradle versions. + Logger: () => logger, + }); + + testUsingContext('should not show warning for incompatible Java/template AGP versions when project type unrelated', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + // Test not creating a project for Android. + await runner.run(<String>['create', '--no-pub', '--platforms=ios,windows,macos,linux', projectDir.path]); + tryToDelete(projectDir); + // Test creating a package (Dart-only code). + await runner.run(<String>['create', '--no-pub', '--template=package', projectDir.path]); + + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'app')))); + expect(logger.warningText, isNot(contains(getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, templateAndroidGradlePluginVersion, 'package')))); + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(0, 0, 0, '0.0.0')), // Too low a version for template AGP versions. + Logger: () => logger, + }); + + testUsingContext('should show warning for incompatible Java/template Gradle versions when detected', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<FlutterProjectType> relevantProjectTypes = <FlutterProjectType>[FlutterProjectType.app, FlutterProjectType.skeleton, FlutterProjectType.module]; + + for (final FlutterProjectType projectType in relevantProjectTypes) { + final String relevantAgpVersion = projectType == FlutterProjectType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; + final String expectedMessage = getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + final String unexpectedMessage = getIncompatibleJavaGradleAgpMessageHeader(true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + + await runner.run(<String>['create', '--no-pub', '--template=${projectType.cliName}', if (projectType != FlutterProjectType.module) '--platforms=android', projectDir.path]); + + // Check components of expected header warning message are printed. + expect(logger.warningText, contains(expectedMessage)); + expect(logger.warningText, isNot(contains(unexpectedMessage))); + expect(logger.warningText, contains('./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>')); + expect(logger.warningText, contains('https://docs.gradle.org/current/userguide/compatibility.html#java')); + + // Check expected file for updating Gradle version is present. + if (projectType == FlutterProjectType.app || projectType == FlutterProjectType.skeleton) { + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, 'android/gradle/wrapper/gradle-wrapper.properties'))); + } + else { + // Project type is module. + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, '.android/gradle/wrapper/gradle-wrapper.properties'))); + } + + // Cleanup to reuse projectDir and logger checks. + tryToDelete(projectDir); + logger.clear(); + } + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(500, 0, 0, '500.0.0')), // Too high a version for template Gradle versions. + Logger: () => logger, + }); + + testUsingContext('should show warning for incompatible Java/template AGP versions when detected', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<FlutterProjectType> relevantProjectTypes = <FlutterProjectType>[FlutterProjectType.app, FlutterProjectType.skeleton, FlutterProjectType.pluginFfi, FlutterProjectType.module, FlutterProjectType.plugin]; + + for (final FlutterProjectType projectType in relevantProjectTypes) { + final String relevantAgpVersion = projectType == FlutterProjectType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; + final String expectedMessage = getIncompatibleJavaGradleAgpMessageHeader(true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + final String unexpectedMessage = getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + + await runner.run(<String>['create', '--no-pub', '--template=${projectType.cliName}', if (projectType != FlutterProjectType.module) '--platforms=android', projectDir.path]); + + // Check components of expected header warning message are printed. + expect(logger.warningText, contains(expectedMessage)); + expect(logger.warningText, isNot(contains(unexpectedMessage))); + expect(logger.warningText, contains('https://developer.android.com/build/releases/gradle-plugin')); + + // Check expected file(s) for updating AGP version is/are present. + if (projectType == FlutterProjectType.app || projectType == FlutterProjectType.skeleton || projectType == FlutterProjectType.pluginFfi) { + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, 'android/build.gradle'))); + } + else if (projectType == FlutterProjectType.plugin) { + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, 'android/app/build.gradle'))); + } + else { + // Project type is module. + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, '.android/build.gradle'))); + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, '.android/app/build.gradle'))); + expect(logger.warningText, contains(globals.fs.path.join(projectDir.path, '.android/Flutter/build.gradle'))); + } + + // Cleanup to reuse projectDir and logger checks. + tryToDelete(projectDir); + logger.clear(); + } + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(1, 8, 0, '1.8.0')), // Too low a version for template AGP versions. Logger: () => logger, }); + + // The Java versions configured in the following tests will need updates as more Java versions are supported by AGP/Gradle: + + testUsingContext('should not show warning for incompatible Java/template AGP/Gradle versions when not detected', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<FlutterProjectType> relevantProjectTypes = <FlutterProjectType>[FlutterProjectType.app, FlutterProjectType.skeleton, FlutterProjectType.pluginFfi, FlutterProjectType.module, FlutterProjectType.plugin]; + + for (final FlutterProjectType projectType in relevantProjectTypes) { + final String relevantAgpVersion = projectType == FlutterProjectType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; + final String unexpectedIncompatibleAgpMessage = getIncompatibleJavaGradleAgpMessageHeader(true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + final String unexpectedIncompatibleGradleMessage = getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + + await runner.run(<String>['create', '--no-pub', '--template=${projectType.cliName}', if (projectType != FlutterProjectType.module) '--platforms=android', projectDir.path]); + + // We do not expect warnings for incompatible Java/template AGP versions if they are in fact, compatible. + expect(logger.warningText, isNot(contains(unexpectedIncompatibleAgpMessage))); + expect(logger.warningText, isNot(contains(unexpectedIncompatibleGradleMessage))); + + // Cleanup to reuse projectDir and logger checks. + tryToDelete(projectDir); + logger.clear(); + } + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(14, 0, 0, '14.0.0')), // Middle compatible Java version with current template AGP/Gradle versions. + Logger: () => logger, + }); + + testUsingContext('should not show warning for incompatible Java/template AGP/Gradle versions when not detected -- maximum compatible Java version', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<FlutterProjectType> relevantProjectTypes = <FlutterProjectType>[FlutterProjectType.app, FlutterProjectType.skeleton, FlutterProjectType.pluginFfi, FlutterProjectType.module, FlutterProjectType.plugin]; + + for (final FlutterProjectType projectType in relevantProjectTypes) { + final String relevantAgpVersion = projectType == FlutterProjectType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; + final String unexpectedIncompatibleAgpMessage = getIncompatibleJavaGradleAgpMessageHeader(true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + final String unexpectedIncompatibleGradleMessage = getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + + await runner.run(<String>['create', '--no-pub', '--template=${projectType.cliName}', if (projectType != FlutterProjectType.module) '--platforms=android', projectDir.path]); + + // We do not expect warnings for incompatible Java/template AGP versions if they are in fact, compatible. + expect(logger.warningText, isNot(contains(unexpectedIncompatibleAgpMessage))); + expect(logger.warningText, isNot(contains(unexpectedIncompatibleGradleMessage))); + + // Cleanup to reuse projectDir and logger checks. + tryToDelete(projectDir); + logger.clear(); + } + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(17, 0, 0, '18.0.0')), // Maximum compatible Java version with current template AGP/Gradle versions. + Logger: () => logger, + }); + + testUsingContext('should not show warning for incompatible Java/template AGP/Gradle versions when not detected -- minimum compatible Java version', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner<void> runner = createTestCommandRunner(command); + final List<FlutterProjectType> relevantProjectTypes = <FlutterProjectType>[FlutterProjectType.app, FlutterProjectType.skeleton, FlutterProjectType.pluginFfi, FlutterProjectType.module, FlutterProjectType.plugin]; + + for (final FlutterProjectType projectType in relevantProjectTypes) { + final String relevantAgpVersion = projectType == FlutterProjectType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; + final String unexpectedIncompatibleAgpMessage = getIncompatibleJavaGradleAgpMessageHeader(true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + final String unexpectedIncompatibleGradleMessage = getIncompatibleJavaGradleAgpMessageHeader(false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName); + + await runner.run(<String>['create', '--no-pub', '--template=${projectType.cliName}', if (projectType != FlutterProjectType.module) '--platforms=android', projectDir.path]); + + // We do not expect warnings for incompatible Java/template AGP versions if they are in fact, compatible. + expect(logger.warningText, isNot(contains(unexpectedIncompatibleAgpMessage))); + expect(logger.warningText, isNot(contains(unexpectedIncompatibleGradleMessage))); + + // Cleanup to reuse projectDir and logger checks. + tryToDelete(projectDir); + logger.clear(); + } + }, overrides: <Type, Generator>{ + Java: () => FakeJava(version: const software.Version.withText(11, 0, 0, '11.0.0')), // Minimum compatible Java version with current template AGP/Gradle versions. + Logger: () => logger, + }); + + testUsingContext('Does not double quote description in index.html on web', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--platforms=web'], + <String>['pubspec.yaml', 'web/index.html'], + ); + + final String rawIndexHtml = await projectDir.childDirectory('web').childFile('index.html').readAsString(); + const String expectedDescription = '<meta name="description" content="A new Flutter project.">'; + + expect(rawIndexHtml.contains(expectedDescription), isTrue); + }); + + testUsingContext('Does not double quote description in manifest.json on web', () async { + await _createProject( + projectDir, + <String>['--no-pub', '--platforms=web'], + <String>['pubspec.yaml', 'web/manifest.json'], + ); + + final String rawManifestJson = await projectDir.childDirectory('web').childFile('manifest.json').readAsString(); + const String expectedDescription = '"description": "A new Flutter project."'; + + expect(rawManifestJson.contains(expectedDescription), isTrue); + }); } Future<void> _createProject( diff --git a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart index 0a9be004bed09..df145350049e4 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart @@ -255,6 +255,81 @@ void main() { ), }); + testUsingContext('get generates synthetic package when l10n.yaml has synthetic-package: true', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + final Directory projectDir = globals.fs.directory(projectPath); + projectDir + .childDirectory('lib') + .childDirectory('l10n') + .childFile('app_en.arb') + ..createSync(recursive: true) + ..writeAsStringSync('{ "hello": "Hello world!" }'); + String pubspecFileContent = projectDir.childFile('pubspec.yaml').readAsStringSync(); + pubspecFileContent = pubspecFileContent.replaceFirst(RegExp(r'\nflutter\:'), ''' +flutter: + generate: true +'''); + projectDir + .childFile('pubspec.yaml') + .writeAsStringSync(pubspecFileContent); + projectDir + .childFile('l10n.yaml') + .writeAsStringSync('synthetic-package: true'); + await runCommandIn(projectPath, 'get'); + expect( + projectDir + .childDirectory('.dart_tool') + .childDirectory('flutter_gen') + .childDirectory('gen_l10n') + .childFile('app_localizations.dart') + .existsSync(), + true + ); + }, overrides: <Type, Generator>{ + Pub: () => Pub( + fileSystem: globals.fs, + logger: globals.logger, + processManager: globals.processManager, + usage: globals.flutterUsage, + botDetector: globals.botDetector, + platform: globals.platform, + ), + }); + + testUsingContext('get generates normal files when l10n.yaml has synthetic-package: false', () async { + final String projectPath = await createProject(tempDir, + arguments: <String>['--no-pub', '--template=module']); + final Directory projectDir = globals.fs.directory(projectPath); + projectDir + .childDirectory('lib') + .childDirectory('l10n') + .childFile('app_en.arb') + ..createSync(recursive: true) + ..writeAsStringSync('{ "hello": "Hello world!" }'); + projectDir + .childFile('l10n.yaml') + .writeAsStringSync('synthetic-package: false'); + await runCommandIn(projectPath, 'get'); + expect( + projectDir + .childDirectory('lib') + .childDirectory('l10n') + .childFile('app_localizations.dart') + .existsSync(), + true + ); + }, overrides: <Type, Generator>{ + Pub: () => Pub( + fileSystem: globals.fs, + logger: globals.logger, + processManager: globals.processManager, + usage: globals.flutterUsage, + botDetector: globals.botDetector, + platform: globals.platform, + ), + }); + testUsingContext('set no plugins as usage value', () async { final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=module']); diff --git a/packages/flutter_tools/test/data/asset_test/font/pubspec.yaml b/packages/flutter_tools/test/data/asset_test/font/pubspec.yaml index 4408e2ac59587..c16803941d9ba 100644 --- a/packages/flutter_tools/test/data/asset_test/font/pubspec.yaml +++ b/packages/flutter_tools/test/data/asset_test/font/pubspec.yaml @@ -2,7 +2,7 @@ name: font description: A test project that contains a font. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: uses-material-design: true diff --git a/packages/flutter_tools/test/data/asset_test/main/pubspec.yaml b/packages/flutter_tools/test/data/asset_test/main/pubspec.yaml index 8611c6daa8545..3cbb5d3994711 100644 --- a/packages/flutter_tools/test/data/asset_test/main/pubspec.yaml +++ b/packages/flutter_tools/test/data/asset_test/main/pubspec.yaml @@ -2,7 +2,7 @@ name: main description: A test project that has a package with a font as a dependency. environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: font: diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart index b989d371f4615..5f5076c4fb43b 100644 --- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart @@ -885,113 +885,13 @@ Gradle Crashed command: <String>[ 'gradlew', '-q', - 'printFreeDebugApplicationId', + 'outputFreeDebugAppLinkSettings', ], - stdout: ''' -ApplicationId: com.example.id - ''', - )); - final String actual = await builder.getApplicationIdForVariant( - 'freeDebug', - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - ); - expect(actual, 'com.example.id'); - }, overrides: <Type, Generator>{ - AndroidStudio: () => FakeAndroidStudio(), - }); - - testUsingContext('can call custom gradle task getApplicationIdForVariant with unknown crash', () async { - final AndroidGradleBuilder builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - usage: testUsage, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand(const FakeCommand( - command: <String>[ - 'gradlew', - '-q', - 'printFreeDebugApplicationId', - ], - stdout: ''' -unknown crash - ''', - )); - final String actual = await builder.getApplicationIdForVariant( - 'freeDebug', - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - ); - expect(actual, ''); - }, overrides: <Type, Generator>{ - AndroidStudio: () => FakeAndroidStudio(), - }); - - testUsingContext('can call custom gradle task getAppLinkDomainsForVariant and parse the result', () async { - final AndroidGradleBuilder builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - usage: testUsage, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - - processManager.addCommand(const FakeCommand( - command: <String>[ - 'gradlew', - '-q', - 'printFreeDebugAppLinkDomains', - ], - stdout: ''' -Domain: example.com -Domain: example2.com - ''', - )); - final List<String> actual = await builder.getAppLinkDomainsForVariant( - 'freeDebug', - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - ); - expect(actual, <String>['example.com', 'example2.com']); - }, overrides: <Type, Generator>{ - AndroidStudio: () => FakeAndroidStudio(), - }); - - testUsingContext('can call custom gradle task getAppLinkDomainsForVariant with unknown crash', () async { - final AndroidGradleBuilder builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - usage: testUsage, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - - processManager.addCommand(const FakeCommand( - command: <String>[ - 'gradlew', - '-q', - 'printFreeDebugAppLinkDomains', - ], - stdout: ''' -unknown crash - ''', )); - final List<String> actual = await builder.getAppLinkDomainsForVariant( + await builder.outputsAppLinkSettings( 'freeDebug', project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - expect(actual.isEmpty, isTrue); }, overrides: <Type, Generator>{ AndroidStudio: () => FakeAndroidStudio(), }); @@ -1187,7 +1087,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_arm'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_arm', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1200,6 +1100,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_arm', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-arm', '-Ptarget=lib/main.dart', '-Pbase-application-name=io.flutter.app.FlutterApplication', @@ -1266,7 +1167,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_arm64'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_arm64', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1279,6 +1180,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_arm64', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-arm64', '-Ptarget=lib/main.dart', '-Pbase-application-name=io.flutter.app.FlutterApplication', @@ -1345,7 +1247,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_x86'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_x86', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1358,6 +1260,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_x86', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-x86', '-Ptarget=lib/main.dart', '-Pbase-application-name=io.flutter.app.FlutterApplication', @@ -1424,7 +1327,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_x64'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_x64', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1437,6 +1340,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_x64', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-x64', '-Ptarget=lib/main.dart', '-Pbase-application-name=io.flutter.app.FlutterApplication', @@ -1565,7 +1469,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_arm'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_arm', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1586,6 +1490,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_arm', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-arm', 'assembleAarRelease', ], @@ -1653,7 +1558,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_arm64'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_arm64', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1674,6 +1579,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_arm64', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-arm64', 'assembleAarRelease', ], @@ -1741,7 +1647,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_x86'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_x86', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1762,6 +1668,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_x86', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-x86', 'assembleAarRelease', ], @@ -1829,7 +1736,7 @@ unknown crash logger: logger, processManager: processManager, fileSystem: fileSystem, - artifacts: Artifacts.test(localEngine: 'out/android_x64'), + artifacts: Artifacts.testLocalEngine(localEngine: 'out/android_x64', localEngineHost: 'out/host_release'), usage: testUsage, gradleUtils: FakeGradleUtils(), platform: FakePlatform(), @@ -1850,6 +1757,7 @@ unknown crash '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', '-Plocal-engine-build-mode=release', '-Plocal-engine-out=out/android_x64', + '-Plocal-engine-host-out=out/host_release', '-Ptarget-platform=android-x64', 'assembleAarRelease', ], diff --git a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart index 8722774497cce..5096ae0ec805b 100644 --- a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart @@ -7,6 +7,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/android/migrations/android_studio_java_gradle_conflict_migration.dart'; +import 'package:flutter_tools/src/android/migrations/min_sdk_version_migration.dart'; import 'package:flutter_tools/src/android/migrations/top_level_gradle_build_file_migration.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/version.dart'; @@ -41,6 +42,79 @@ zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists '''; +String sampleModuleGradleBuildFile(String minSdkVersionString) { + return r''' +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.asset_sample" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.asset_sample" + // 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. + ''' + minSdkVersionString + r''' + + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} +'''; +} + final Version androidStudioDolphin = Version(2021, 3, 1); const Version _javaVersion17 = Version.withText(17, 0, 2, 'openjdk 17.0.2'); @@ -257,14 +331,128 @@ tasks.register("clean", Delete) { expect(bufferLogger.traceText, contains(optOutFlagEnabled)); }); }); + + group('migrate min sdk versions less than 19 to flutter.minSdkVersion ' + 'when in a FlutterProject that is an app', () + { + late MemoryFileSystem memoryFileSystem; + late BufferLogger bufferLogger; + late FakeAndroidProject project; + late MinSdkVersionMigration migration; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + memoryFileSystem.currentDirectory.childDirectory('android').createSync(); + bufferLogger = BufferLogger.test(); + project = FakeAndroidProject( + root: memoryFileSystem.currentDirectory.childDirectory('android'), + ); + project.appGradleFile.parent.createSync(recursive: true); + migration = MinSdkVersionMigration( + project, + bufferLogger + ); + }); + + testWithoutContext('do nothing when files missing', () { + migration.migrate(); + expect(bufferLogger.traceText, contains(appGradleNotFoundWarning)); + }); + + testWithoutContext('replace when api 16', () { + const String minSdkVersion16 = 'minSdkVersion 16'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); + }); + + testWithoutContext('replace when api 17', () { + const String minSdkVersion17 = 'minSdkVersion 17'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion17)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); + }); + + testWithoutContext('replace when api 18', () { + const String minSdkVersion18 = 'minSdkVersion 18'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion18)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); + }); + + testWithoutContext('do nothing when >=api 19', () { + const String minSdkVersion19 = 'minSdkVersion 19'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion19)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion19)); + }); + + testWithoutContext('do nothing when already using ' + 'flutter.minSdkVersion', () { + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(replacementMinSdkText)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); + }); + + testWithoutContext('avoid rewriting comments', () { + const String code = '// minSdkVersion 16 // old default\n' + ' minSdkVersion 23 // new version'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(code)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(code)); + }); + + testWithoutContext('do nothing when project is a module', () { + project = FakeAndroidProject( + root: memoryFileSystem.currentDirectory.childDirectory('android'), + module: true, + ); + migration = MinSdkVersionMigration( + project, + bufferLogger + ); + const String minSdkVersion16 = 'minSdkVersion 16'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion16)); + }); + + testWithoutContext('do nothing when minSdkVersion is set ' + 'to a constant', () { + const String minSdkVersionConstant = 'minSdkVersion kMinSdkversion'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersionConstant)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersionConstant)); + }); + + testWithoutContext('do nothing when minSdkVersion is set ' + 'using = syntax', () { + const String equalsSyntaxMinSdkVersion16 = 'minSdkVersion = 16'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16)); + migration.migrate(); + expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16)); + }); + }); }); } class FakeAndroidProject extends Fake implements AndroidProject { - FakeAndroidProject({required Directory root}) : hostAppGradleRoot = root; + FakeAndroidProject({required Directory root, this.module, this.plugin}) : hostAppGradleRoot = root; @override Directory hostAppGradleRoot; + + final bool? module; + final bool? plugin; + + @override + bool get isPlugin => plugin ?? false; + + @override + bool get isModule => module ?? false; + + @override + File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); } class FakeAndroidStudio extends Fake implements AndroidStudio { diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart index 5b32f927997f0..493d76cb3826e 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -211,7 +211,7 @@ void main() { setUp(() { fs = MemoryFileSystem.test(); - localEngineArtifacts = Artifacts.test(localEngine: 'out/android_arm'); + localEngineArtifacts = Artifacts.testLocalEngine(localEngine: 'out/android_arm', localEngineHost: 'out/host_release'); }); void testUsingAndroidContext(String description, dynamic Function() testMethod) { diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart index 9aa482e5f3ef5..e789afaa8d02b 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/version_range.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; import '../../src/common.dart'; @@ -470,16 +471,20 @@ allprojects { group('validates gradle/agp versions', () { final List<GradleAgpTestData> testData = <GradleAgpTestData>[ - // Values too new *these need to update* when + // Values too new *these need to be updated* when // max known gradle and max known agp versions are updated: // Newer tools version supports max gradle version. GradleAgpTestData(true, agpVersion: '8.2', gradleVersion: '8.0'), - // Newer tools version does not even meet current gradle version requiremnts. + // Newer tools version does not even meet current gradle version requirements. GradleAgpTestData(false, agpVersion: '8.2', gradleVersion: '7.3'), // Newer tools version requires newer gradle version. GradleAgpTestData(true, agpVersion: '8.3', gradleVersion: '8.1'), - // Minimims as defined in + // Template versions of Gradle/AGP. + GradleAgpTestData(true, agpVersion: templateAndroidGradlePluginVersion, gradleVersion: templateDefaultGradleVersion), + GradleAgpTestData(true, agpVersion: templateAndroidGradlePluginVersionForModule, gradleVersion: templateDefaultGradleVersion), + + // Minimums as defined in // https://developer.android.com/studio/releases/gradle-plugin#updating-gradle GradleAgpTestData(true, agpVersion: '8.1', gradleVersion: '8.0'), GradleAgpTestData(true, agpVersion: '8.0', gradleVersion: '8.0'), @@ -577,16 +582,15 @@ allprojects { group('validates java/gradle versions', () { final List<JavaGradleTestData> testData = <JavaGradleTestData>[ - // Values too new *these need to update* when + // Values too new *these need to be updated* when // max supported java and max known gradle versions are updated: // Newer tools version does not even meet current gradle version requiremnts. JavaGradleTestData(false, javaVersion: '20', gradleVersion: '7.5'), // Newer tools version requires newer gradle version. JavaGradleTestData(true, javaVersion: '20', gradleVersion: '8.1'), - // Max known unsupported java version. - JavaGradleTestData(true, javaVersion: '24', gradleVersion: '8.1'), - - // Minimims as defined in + // Max known unsupported Java version. + JavaGradleTestData(true, javaVersion: '24', gradleVersion: maxKnownAndSupportedGradleVersion), + // Minimums as defined in // https://docs.gradle.org/current/userguide/compatibility.html#java JavaGradleTestData(true, javaVersion: '19', gradleVersion: '7.6'), JavaGradleTestData(true, javaVersion: '18', gradleVersion: '7.5'), @@ -600,7 +604,7 @@ allprojects { JavaGradleTestData(true, javaVersion: '1.10', gradleVersion: '4.7'), JavaGradleTestData(true, javaVersion: '1.9', gradleVersion: '4.3'), JavaGradleTestData(true, javaVersion: '1.8', gradleVersion: '2.0'), - // Gradle too old for java version. + // Gradle too old for Java version. JavaGradleTestData(false, javaVersion: '19', gradleVersion: '6.7'), JavaGradleTestData(false, javaVersion: '11', gradleVersion: '4.10.1'), JavaGradleTestData(false, javaVersion: '1.9', gradleVersion: '4.1'), @@ -642,7 +646,7 @@ allprojects { testWithoutContext( '(Java, gradle): (${data.javaVersion}, ${data.gradleVersion})', () { expect( - validateJavaGradle( + validateJavaAndGradle( BufferLogger.test(), javaV: data.javaVersion, gradleV: data.gradleVersion, @@ -653,6 +657,314 @@ allprojects { } }); }); + + group('validates java/AGP versions', () { + final List<JavaAgpTestData> testData = <JavaAgpTestData>[ + // Strictly too old Java versions for known AGP versions. + JavaAgpTestData(false, javaVersion: '1.6', agpVersion: maxKnownAgpVersion), + JavaAgpTestData(false, javaVersion: '1.6', agpVersion: maxKnownAndSupportedAgpVersion), + JavaAgpTestData(false, javaVersion: '1.6', agpVersion: '4.2'), + // Strictly too old AGP versions. + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '1.0'), + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '4.1'), + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '2.3'), + // Strictly too new Java versions for defined AGP versions. + JavaAgpTestData(true, javaVersion: '18', agpVersion: '8.1'), + JavaAgpTestData(true, javaVersion: '18', agpVersion: '7.4'), + JavaAgpTestData(true, javaVersion: '18', agpVersion: '4.2'), + // Strictly too new AGP versions. + // *The tests that follow need to be updated* when max supported AGP versions are updated: + JavaAgpTestData(false, javaVersion: '24', agpVersion: '8.3'), + JavaAgpTestData(false, javaVersion: '20', agpVersion: '8.3'), + JavaAgpTestData(false, javaVersion: '17', agpVersion: '8.3'), + // Java 17 & patch versions compatibility cases + // *The tests that follow need to be updated* when maxKnownAndSupportedAgpVersion is + // updated: + JavaAgpTestData(false, javaVersion: '17', agpVersion: '8.2'), + JavaAgpTestData(true, javaVersion: '17', agpVersion: maxKnownAndSupportedAgpVersion), + JavaAgpTestData(true, javaVersion: '17', agpVersion: '8.1'), + JavaAgpTestData(true, javaVersion: '17', agpVersion: '8.0'), + JavaAgpTestData(true, javaVersion: '17', agpVersion: '7.4'), + JavaAgpTestData(false, javaVersion: '17.0.3', agpVersion: '8.2'), + JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: maxKnownAndSupportedAgpVersion), + JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '8.1'), + JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '8.0'), + JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '7.4'), + // Java 11 & patch versions compatibility cases + JavaAgpTestData(false, javaVersion: '11', agpVersion: '8.0'), + JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.4'), + JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.2'), + JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.0'), + JavaAgpTestData(true, javaVersion: '11', agpVersion: '4.2'), + JavaAgpTestData(false, javaVersion: '11.0.18', agpVersion: '8.0'), + JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.4'), + JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.2'), + JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.0'), + JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '4.2'), + // Java 8 compatibility cases + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '7.0'), + JavaAgpTestData(true, javaVersion: '1.8', agpVersion: oldestDocumentedJavaAgpCompatibilityVersion), // agpVersion = 4.2 + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '4.1'), + // Null value cases + // ignore: avoid_redundant_argument_values + JavaAgpTestData(false, javaVersion: null, agpVersion: '4.2'), + // ignore: avoid_redundant_argument_values + JavaAgpTestData(false, javaVersion: '1.8', agpVersion: null), + // ignore: avoid_redundant_argument_values + JavaAgpTestData(false, javaVersion: null, agpVersion: null), + ]; + + for (final JavaAgpTestData data in testData) { + testWithoutContext( + '(Java, agp): (${data.javaVersion}, ${data.agpVersion})', () { + expect( + validateJavaAndAgp( + BufferLogger.test(), + javaV: data.javaVersion, + agpV: data.agpVersion, + ), + data.validPair ? isTrue : isFalse, + reason: 'J: ${data.javaVersion}, G: ${data.agpVersion}'); + }); + } + }); + + group('detecting valid Gradle/AGP versions for given Java version and vice versa', () { + testWithoutContext('getValidGradleVersionRangeForJavaVersion returns valid Gradle version range for Java version', () { + final Logger testLogger = BufferLogger.test(); + // Java version too high. + expect(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: oneMajorVersionHigherJavaVersion), isNull); + // Maximum known Java version. + // *The test case that follows needs to be updated* when higher versions of Java are supported: + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '20'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '20.0.2')), + isNull)); + // Known supported Java versions. + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '19'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '19.0.2')), + equals( + const JavaGradleCompat( + javaMin: '19', + javaMax: '20', + gradleMin: '7.6', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '18'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '18.0.2')), + equals( + const JavaGradleCompat( + javaMin: '18', + javaMax: '19', + gradleMin: '7.5', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '17'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '17.0.2')), + equals( + const JavaGradleCompat( + javaMin: '17', + javaMax: '18', + gradleMin: '7.3', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '16'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '16.0.2')), + equals( + const JavaGradleCompat( + javaMin: '16', + javaMax: '17', + gradleMin: '7.0', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '15'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '15.0.2')), + equals( + const JavaGradleCompat( + javaMin: '15', + javaMax: '16', + gradleMin: '6.7', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '14'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '14.0.2')), + equals( + const JavaGradleCompat( + javaMin: '14', + javaMax: '15', + gradleMin: '6.3', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '13'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '13.0.2')), + equals( + const JavaGradleCompat( + javaMin: '13', + javaMax: '14', + gradleMin: '6.0', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '12'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '12.0.2')), + equals( + const JavaGradleCompat( + javaMin: '12', + javaMax: '13', + gradleMin: '5.4', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '11'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '11.0.2')), + equals( + const JavaGradleCompat( + javaMin: '11', + javaMax: '12', + gradleMin: '5.0', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.10'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.10.2')), + equals( + const JavaGradleCompat( + javaMin: '1.10', + javaMax: '1.11', + gradleMin: '4.7', + gradleMax: maxKnownAndSupportedGradleVersion)))); + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.9'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.9.2')), + equals( + const JavaGradleCompat( + javaMin: '1.9', + javaMax: '1.10', + gradleMin: '4.3', + gradleMax: maxKnownAndSupportedGradleVersion)))); + // Java 1.8 -- return oldest documented compatibility info + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.8'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.8.2')), + equals( + const JavaGradleCompat( + javaMin: '1.8', + javaMax: '1.9', + gradleMin: '2.0', + gradleMax: maxKnownAndSupportedGradleVersion)))); + // Java version too low. + expect( + getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.7'), + allOf( + equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.7.2')), + isNull)); + }); + + testWithoutContext('getMinimumAgpVersionForJavaVersion returns minimum AGP version for Java version', () { + final Logger testLogger = BufferLogger.test(); + // Maximum known Java version. + // *The test case that follows needs to be updated* as higher versions of AGP are supported: + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: oneMajorVersionHigherJavaVersion), + equals( + const JavaAgpCompat( + javaMin: '17', + javaDefault: '17', + agpMin: '8.0', + agpMax: '8.1'))); + // Known Java versions. + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '17'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '17.0.2')), + equals( + const JavaAgpCompat( + javaMin: '17', + javaDefault: '17', + agpMin: '8.0', + agpMax: '8.1')))); + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '15'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '15.0.2')), + equals( + const JavaAgpCompat( + javaMin: '11', + javaDefault: '11', + agpMin: '7.0', + agpMax: '7.4')))); + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '11'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '11.0.2')), + equals( + const JavaAgpCompat( + javaMin: '11', + javaDefault: '11', + agpMin: '7.0', + agpMax: '7.4')))); + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.9'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.9.2')), + equals( + const JavaAgpCompat( + javaMin: '1.8', + javaDefault: '1.8', + agpMin: '4.2', + agpMax: '4.2')))); + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.8'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.8.2')), + equals( + const JavaAgpCompat( + javaMin: '1.8', + javaDefault: '1.8', + agpMin: '4.2', + agpMax: '4.2')))); + // Java version too low. + expect( + getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.7'), + allOf( + equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.7.2')), + isNull)); + }); + + testWithoutContext('getJavaVersionFor returns expected Java version range', () { + // Strictly too old Gradle and AGP versions. + expect(getJavaVersionFor(gradleV: '1.9', agpV: '4.1'), equals(const VersionRange(null, null))); + // Strictly too old Gradle or AGP version. + expect(getJavaVersionFor(gradleV: '1.9', agpV: '4.2'), equals(const VersionRange('1.8', null))); + expect(getJavaVersionFor(gradleV: '2.0', agpV: '4.1'), equals(const VersionRange(null, '1.9'))); + // Strictly too new Gradle and AGP versions. + expect(getJavaVersionFor(gradleV: '8.1', agpV: '8.2'), equals(const VersionRange(null, null))); + // Strictly too new Gradle version and maximum version of AGP. + //*This test case will need its expected Java range updated when a new version of AGP is supported.* + expect(getJavaVersionFor(gradleV: '8.1', agpV: maxKnownAndSupportedAgpVersion), equals(const VersionRange('17', null))); + // Strictly too new AGP version and maximum version of Gradle. + //*This test case will need its expected Java range updated when a new version of Gradle is supported.* + expect(getJavaVersionFor(gradleV: maxKnownAndSupportedGradleVersion, agpV: '8.2'), equals(const VersionRange(null, '20'))); + // Tests with a known compatible Gradle/AGP version pair. + expect(getJavaVersionFor(gradleV: '7.0', agpV: '7.2'), equals(const VersionRange('11', '17'))); + expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.2'), equals(const VersionRange('11', '17'))); + expect(getJavaVersionFor(gradleV: '7.2.2', agpV: '7.2'), equals(const VersionRange('11', '17'))); + expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.0'), equals(const VersionRange('11', '17'))); + expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.2'), equals(const VersionRange('11', '17'))); + expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.4'), equals(const VersionRange('11', '17'))); + }); + }); } class GradleAgpTestData { @@ -669,6 +981,13 @@ class JavaGradleTestData { final bool validPair; } +class JavaAgpTestData { + JavaAgpTestData(this.validPair, {this.javaVersion, this.agpVersion}); + final String? agpVersion; + final String? javaVersion; + final bool validPair; +} + final Platform windowsPlatform = FakePlatform( operatingSystem: 'windows', environment: <String, String>{ diff --git a/packages/flutter_tools/test/general.shard/artifacts_test.dart b/packages/flutter_tools/test/general.shard/artifacts_test.dart index b6afa09955448..920980a23ba2f 100644 --- a/packages/flutter_tools/test/general.shard/artifacts_test.dart +++ b/packages/flutter_tools/test/general.shard/artifacts_test.dart @@ -11,7 +11,7 @@ import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import '../src/common.dart'; -import '../src/fake_process_manager.dart'; +import '../src/context.dart'; import '../src/fakes.dart'; void main() { @@ -545,4 +545,37 @@ void main() { ); }); }); + + group('LocalEngineInfo', () { + late FileSystem fileSystem; + late LocalEngineInfo localEngineInfo; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + }); + + testUsingContext('determines the target device name from the path', () { + localEngineInfo = LocalEngineInfo( + targetOutPath: fileSystem.path.join(fileSystem.currentDirectory.path, 'out', 'android_debug_unopt'), + hostOutPath: fileSystem.path.join(fileSystem.currentDirectory.path, 'out', 'host_debug_unopt'), + ); + + expect(localEngineInfo.localTargetName, 'android_debug_unopt'); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('determines the target device name from the path when using a custom engine path', () { + localEngineInfo = LocalEngineInfo( + targetOutPath: fileSystem.path.join(fileSystem.currentDirectory.path, 'out', 'android_debug_unopt'), + hostOutPath: fileSystem.path.join(fileSystem.currentDirectory.path, 'out', 'host_debug_unopt'), + ); + + expect(localEngineInfo.localHostName, 'host_debug_unopt'); + }, overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + }); } diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index 11837c7fb2572..4b798f61ec665 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -325,6 +325,102 @@ flutter: }); }); + group('AssetBundle.build (web builds)', () { + late FileSystem testFileSystem; + + setUp(() async { + testFileSystem = MemoryFileSystem( + style: globals.platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); + testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + }); + + testUsingContext('empty pubspec', () async { + globals.fs.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''); + + final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); + await bundle.build(packagesPath: '.packages', targetPlatform: TargetPlatform.web_javascript); + + expect(bundle.entries.keys, + unorderedEquals(<String>[ + 'AssetManifest.json', + 'AssetManifest.bin', + 'AssetManifest.bin.json', + ]) + ); + expect( + utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()), + '{}', + ); + expect( + utf8.decode(await bundle.entries['AssetManifest.bin.json']!.contentsAsBytes()), + '""', + ); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('pubspec contains an asset', () async { + globals.fs.file('.packages').createSync(); + globals.fs.file('pubspec.yaml').writeAsStringSync(r''' +name: test +dependencies: + flutter: + sdk: flutter +flutter: + assets: + - assets/bar/lizard.png +'''); + globals.fs.file( + globals.fs.path.joinAll(<String>['assets', 'bar', 'lizard.png']) + ).createSync(recursive: true); + + final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); + await bundle.build(packagesPath: '.packages', targetPlatform: TargetPlatform.web_javascript); + + expect(bundle.entries.keys, + unorderedEquals(<String>[ + 'AssetManifest.json', + 'AssetManifest.bin', + 'AssetManifest.bin.json', + 'FontManifest.json', + 'NOTICES', // not .Z + 'assets/bar/lizard.png', + ]) + ); + final Map<Object?, Object?> manifestJson = json.decode( + utf8.decode( + await bundle.entries['AssetManifest.json']!.contentsAsBytes() + ) + ) as Map<Object?, Object?>; + expect(manifestJson, isNotEmpty); + expect(manifestJson['assets/bar/lizard.png'], isNotNull); + + final Uint8List manifestBinJsonBytes = base64.decode( + json.decode( + utf8.decode( + await bundle.entries['AssetManifest.bin.json']!.contentsAsBytes() + ) + ) as String + ); + + final Uint8List manifestBinBytes = Uint8List.fromList( + await bundle.entries['AssetManifest.bin']!.contentsAsBytes() + ); + + expect(manifestBinJsonBytes, equals(manifestBinBytes), + reason: 'JSON-encoded binary content should be identical to BIN file.'); + }, overrides: <Type, Generator>{ + FileSystem: () => testFileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + }); + testUsingContext('Failed directory delete shows message', () async { final FileExceptionHandler handler = FileExceptionHandler(); final FileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle); diff --git a/packages/flutter_tools/test/general.shard/base/command_help_test.dart b/packages/flutter_tools/test/general.shard/base/command_help_test.dart index 43451e2f42356..0706e826cb540 100644 --- a/packages/flutter_tools/test/general.shard/base/command_help_test.dart +++ b/packages/flutter_tools/test/general.shard/base/command_help_test.dart @@ -79,6 +79,7 @@ void main() { group('CommandHelp', () { group('toString', () { testWithoutContext('ends with a resetBold when it has parenthetical text', () { + // This is apparently required to work around bugs in some terminal clients. final Platform platform = FakePlatform(stdoutSupportsAnsi: true); final AnsiTerminal terminal = AnsiTerminal(stdio: FakeStdio(), platform: platform); @@ -131,19 +132,19 @@ void main() { wrapColumn: maxLineWidth, ); - expect(commandHelp.I.toString(), endsWith('\x1B[90m(debugInvertOversizedImages)\x1B[39m\x1B[22m')); - expect(commandHelp.L.toString(), endsWith('\x1B[90m(debugDumpLayerTree)\x1B[39m\x1B[22m')); - expect(commandHelp.P.toString(), endsWith('\x1B[90m(WidgetsApp.showPerformanceOverlay)\x1B[39m\x1B[22m')); - expect(commandHelp.S.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m')); - expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m')); - expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m')); - expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m')); - expect(commandHelp.f.toString(), endsWith('\x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m')); - expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m')); - expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m')); - expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m')); - expect(commandHelp.t.toString(), endsWith('\x1B[90m(debugDumpRenderTree)\x1B[39m\x1B[22m')); - expect(commandHelp.w.toString(), endsWith('\x1B[90m(debugDumpApp)\x1B[39m\x1B[22m')); + expect(commandHelp.I.toString(), contains('\x1B[90m(debugInvertOversizedImages)\x1B[39m')); + expect(commandHelp.L.toString(), contains('\x1B[90m(debugDumpLayerTree)\x1B[39m')); + expect(commandHelp.P.toString(), contains('\x1B[90m(WidgetsApp.showPerformanceOverlay)\x1B[39m')); + expect(commandHelp.S.toString(), contains('\x1B[90m(debugDumpSemantics)\x1B[39m')); + expect(commandHelp.U.toString(), contains('\x1B[90m(debugDumpSemantics)\x1B[39m')); + expect(commandHelp.a.toString(), contains('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m')); + expect(commandHelp.b.toString(), contains('\x1B[90m(debugBrightnessOverride)\x1B[39m')); + expect(commandHelp.f.toString(), contains('\x1B[90m(debugDumpFocusTree)\x1B[39m')); + expect(commandHelp.i.toString(), contains('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m')); + expect(commandHelp.o.toString(), contains('\x1B[90m(defaultTargetPlatform)\x1B[39m')); + expect(commandHelp.p.toString(), contains('\x1B[90m(debugPaintSizeEnabled)\x1B[39m')); + expect(commandHelp.t.toString(), contains('\x1B[90m(debugDumpRenderTree)\x1B[39m')); + expect(commandHelp.w.toString(), contains('\x1B[90m(debugDumpApp)\x1B[39m')); }); testWithoutContext('should not create a help text longer than maxLineWidth without ansi support', () { @@ -184,6 +185,7 @@ void main() { wrapColumn: maxLineWidth, ); + // The trailing \x1B[22m is to work around reported bugs in some terminal clients. expect(commandHelp.I.toString(), equals('\x1B[1mI\x1B[22m Toggle oversized image inversion. \x1B[90m(debugInvertOversizedImages)\x1B[39m\x1B[22m')); expect(commandHelp.L.toString(), equals('\x1B[1mL\x1B[22m Dump layer tree to the console. \x1B[90m(debugDumpLayerTree)\x1B[39m\x1B[22m')); expect(commandHelp.M.toString(), equals('\x1B[1mM\x1B[22m Write SkSL shaders to a unique file in the project directory.')); diff --git a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart index f9fa8ef8d21d9..e6363ceb91b39 100644 --- a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart +++ b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart @@ -12,7 +12,6 @@ import 'package:flutter_tools/src/base/error_handling_io.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/platform.dart'; -import 'package:path/path.dart' as p; // flutter_ignore: package_path_import import 'package:process/process.dart'; import 'package:test/fake.dart'; @@ -1293,7 +1292,7 @@ class FakeExistsFile extends Fake implements File { class FakeFileSystem extends Fake implements FileSystem { @override - p.Context get path => p.Context(); + Context get path => Context(); @override Directory get currentDirectory { diff --git a/packages/flutter_tools/test/general.shard/base/logger_test.dart b/packages/flutter_tools/test/general.shard/base/logger_test.dart index 29a142acdc508..72a88ec25231a 100644 --- a/packages/flutter_tools/test/general.shard/base/logger_test.dart +++ b/packages/flutter_tools/test/general.shard/base/logger_test.dart @@ -1286,6 +1286,47 @@ void main() { expect(mockLogger.traceText, 'Oooh, I do I do I do\n'); expect(mockLogger.errorText, 'Helpless!\n$stackTrace\n'); }); + + testWithoutContext('Animations are disabled when, uh, disabled.', () async { + final Logger logger = StdoutLogger( + terminal: AnsiTerminal( + stdio: fakeStdio, + platform: _kNoAnsiPlatform, + isCliAnimationEnabled: false, + ), + stdio: fakeStdio, + stopwatchFactory: FakeStopwatchFactory(stopwatch: FakeStopwatch()), + outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40), + ); + logger.startProgress('po').stop(); + expect(outputStderr(), <String>['']); + expect(outputStdout(), <String>[ + 'po 0ms', + '', + ]); + logger.startProgress('ta') + ..pause() + ..resume() + ..stop(); + expect(outputStderr(), <String>['']); + expect(outputStdout(), <String>[ + 'po 0ms', + 'ta ', + 'ta 0ms', + '', + ]); + logger.startSpinner() + ..pause() + ..resume() + ..stop(); + expect(outputStderr(), <String>['']); + expect(outputStdout(), <String>[ + 'po 0ms', + 'ta ', + 'ta 0ms', + '', + ]); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/base/signals_test.dart b/packages/flutter_tools/test/general.shard/base/signals_test.dart index 0d8cae24c1830..d2b321b102c4f 100644 --- a/packages/flutter_tools/test/general.shard/base/signals_test.dart +++ b/packages/flutter_tools/test/general.shard/base/signals_test.dart @@ -6,19 +6,24 @@ import 'dart:async'; import 'dart:io' as io; import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; +import '../../src/context.dart'; void main() { group('Signals', () { late Signals signals; late FakeProcessSignal fakeSignal; late ProcessSignal signalUnderTest; + late FakeShutdownHooks shutdownHooks; setUp(() { - signals = Signals.test(); + shutdownHooks = FakeShutdownHooks(); + signals = Signals.test(shutdownHooks: shutdownHooks); fakeSignal = FakeProcessSignal(); signalUnderTest = ProcessSignal(fakeSignal); }); @@ -168,9 +173,10 @@ void main() { expect(errList, isEmpty); }); - testWithoutContext('all handlers for exiting signals are run before exit', () async { + testUsingContext('all handlers for exiting signals are run before exit', () async { final Signals signals = Signals.test( exitSignals: <ProcessSignal>[signalUnderTest], + shutdownHooks: shutdownHooks, ); final Completer<void> completer = Completer<void>(); bool first = false; @@ -201,6 +207,27 @@ void main() { fakeSignal.controller.add(fakeSignal); await completer.future; + expect(shutdownHooks.ranShutdownHooks, isTrue); + }); + + testUsingContext('ShutdownHooks run before exiting', () async { + final Signals signals = Signals.test( + exitSignals: <ProcessSignal>[signalUnderTest], + shutdownHooks: shutdownHooks, + ); + final Completer<void> completer = Completer<void>(); + + setExitFunctionForTests((int exitCode) { + expect(exitCode, 0); + restoreExitFunction(); + completer.complete(); + }); + + signals.addHandler(signalUnderTest, (ProcessSignal s) {}); + + fakeSignal.controller.add(fakeSignal); + await completer.future; + expect(shutdownHooks.ranShutdownHooks, isTrue); }); }); } @@ -211,3 +238,12 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal { @override Stream<io.ProcessSignal> watch() => controller.stream; } + +class FakeShutdownHooks extends Fake implements ShutdownHooks { + bool ranShutdownHooks = false; + + @override + Future<void> runShutdownHooks(Logger logger) async { + ranShutdownHooks = true; + } +} diff --git a/packages/flutter_tools/test/general.shard/base/terminal_test.dart b/packages/flutter_tools/test/general.shard/base/terminal_test.dart index 3d335bbe254ab..5810a58290775 100644 --- a/packages/flutter_tools/test/general.shard/base/terminal_test.dart +++ b/packages/flutter_tools/test/general.shard/base/terminal_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/terminal.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; +import '../../src/fakes.dart'; void main() { group('output preferences', () { @@ -253,6 +254,16 @@ void main() { expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 10, 23)).preferredStyle, 2); expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 11, 23)).preferredStyle, 3); }); + + testWithoutContext('set singleCharMode resilient to StdinException', () async { + final FakeStdio stdio = FakeStdio(); + final AnsiTerminal terminal = AnsiTerminal(stdio: stdio, platform: const LocalPlatform()); + stdio.stdinHasTerminal = true; + stdio._stdin = FakeStdin()..echoModeCallback = (bool _) => throw const StdinException( + 'Error setting terminal echo mode, OS Error: The handle is invalid.', + ); + terminal.singleCharMode = true; + }); } late Stream<String> mockStdInStream; @@ -269,14 +280,36 @@ class TestTerminal extends AnsiTerminal { return mockStdInStream; } + bool _singleCharMode = false; + @override - bool singleCharMode = false; + bool get singleCharMode => _singleCharMode; + + void Function(bool newMode)? _singleCharModeCallback; + + @override + set singleCharMode(bool newMode) { + _singleCharMode = newMode; + if (_singleCharModeCallback != null) { + _singleCharModeCallback!(newMode); + } + } @override int get preferredStyle => 0; } class FakeStdio extends Fake implements Stdio { + Stream<List<int>>? _stdin; + + @override + Stream<List<int>> get stdin { + if (_stdin != null) { + return _stdin!; + } + throw UnimplementedError('stdin'); + } + @override bool stdinHasTerminal = false; } diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index af04ee243a2c1..131a8530a81b8 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:file/memory.dart'; + import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; @@ -101,20 +104,20 @@ void main() { expect(getNameForTargetPlatform(TargetPlatform.android), isNot(contains('ios'))); }); - testWithoutContext('defaultIOSArchsForEnvironment', () { + testUsingContext('defaultIOSArchsForEnvironment', () { expect(defaultIOSArchsForEnvironment( EnvironmentType.physical, - Artifacts.test(localEngine: 'ios_debug_unopt'), + Artifacts.testLocalEngine(localEngineHost: 'host_debug_unopt', localEngine: 'ios_debug_unopt'), ).single, DarwinArch.arm64); expect(defaultIOSArchsForEnvironment( EnvironmentType.simulator, - Artifacts.test(localEngine: 'ios_debug_sim_unopt'), + Artifacts.testLocalEngine(localEngineHost: 'host_debug_unopt', localEngine: 'ios_debug_sim_unopt'), ).single, DarwinArch.x86_64); expect(defaultIOSArchsForEnvironment( EnvironmentType.simulator, - Artifacts.test(localEngine: 'ios_debug_sim_unopt_arm64'), + Artifacts.testLocalEngine(localEngineHost: 'host_debug_unopt', localEngine: 'ios_debug_sim_unopt_arm64'), ).single, DarwinArch.arm64); expect(defaultIOSArchsForEnvironment( @@ -124,21 +127,27 @@ void main() { expect(defaultIOSArchsForEnvironment( EnvironmentType.simulator, Artifacts.test(), ), <DarwinArch>[ DarwinArch.x86_64, DarwinArch.arm64 ]); - }); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + }); - testWithoutContext('defaultMacOSArchsForEnvironment', () { + testUsingContext('defaultMacOSArchsForEnvironment', () { expect(defaultMacOSArchsForEnvironment( - Artifacts.test(localEngine: 'host_debug_unopt'), + Artifacts.testLocalEngine(localEngineHost: 'host_debug_unopt', localEngine: 'host_debug_unopt'), ).single, DarwinArch.x86_64); expect(defaultMacOSArchsForEnvironment( - Artifacts.test(localEngine: 'host_debug_unopt_arm64'), + Artifacts.testLocalEngine(localEngineHost: 'host_debug_unopt', localEngine: 'host_debug_unopt_arm64'), ).single, DarwinArch.arm64); expect(defaultMacOSArchsForEnvironment( Artifacts.test(), ), <DarwinArch>[ DarwinArch.x86_64, DarwinArch.arm64 ]); - }); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + }); testWithoutContext('getIOSArchForName on Darwin arches', () { expect(getIOSArchForName('armv7'), DarwinArch.armv7); @@ -166,6 +175,7 @@ void main() { dartDefines: <String>['foo=2', 'bar=2'], dartObfuscation: true, splitDebugInfoPath: 'foo/', + frontendServerStarterPath: 'foo/bar/frontend_server_starter.dart', extraFrontEndOptions: <String>['--enable-experiment=non-nullable', 'bar'], extraGenSnapshotOptions: <String>['--enable-experiment=non-nullable', 'fizz'], bundleSkSLPath: 'foo/bar/baz.sksl.json', @@ -181,6 +191,7 @@ void main() { 'BuildMode': 'debug', 'DartDefines': 'Zm9vPTI=,YmFyPTI=', 'DartObfuscation': 'true', + 'FrontendServerStarterPath': 'foo/bar/frontend_server_starter.dart', 'ExtraFrontEndOptions': '--enable-experiment=non-nullable,bar', 'ExtraGenSnapshotOptions': '--enable-experiment=non-nullable,fizz', 'SplitDebugInfo': 'foo/', @@ -202,6 +213,7 @@ void main() { dartDefines: <String>['foo=2', 'bar=2'], dartObfuscation: true, splitDebugInfoPath: 'foo/', + frontendServerStarterPath: 'foo/bar/frontend_server_starter.dart', extraFrontEndOptions: <String>['--enable-experiment=non-nullable', 'bar'], extraGenSnapshotOptions: <String>['--enable-experiment=non-nullable', 'fizz'], bundleSkSLPath: 'foo/bar/baz.sksl.json', @@ -217,6 +229,7 @@ void main() { 'DART_DEFINES': 'Zm9vPTI=,YmFyPTI=', 'DART_OBFUSCATION': 'true', 'SPLIT_DEBUG_INFO': 'foo/', + 'FRONTEND_SERVER_STARTER_PATH': 'foo/bar/frontend_server_starter.dart', 'EXTRA_FRONT_END_OPTIONS': '--enable-experiment=non-nullable,bar', 'EXTRA_GEN_SNAPSHOT_OPTIONS': '--enable-experiment=non-nullable,fizz', 'BUNDLE_SKSL_PATH': 'foo/bar/baz.sksl.json', @@ -233,6 +246,7 @@ void main() { dartDefineConfigJsonMap: <String, Object>{'baz': '2'}, dartObfuscation: true, splitDebugInfoPath: 'foo/', + frontendServerStarterPath: 'foo/bar/frontend_server_starter.dart', extraFrontEndOptions: <String>['--enable-experiment=non-nullable', 'bar'], extraGenSnapshotOptions: <String>['--enable-experiment=non-nullable', 'fizz'], bundleSkSLPath: 'foo/bar/baz.sksl.json', @@ -244,6 +258,7 @@ void main() { expect(buildInfo.toGradleConfig(), <String>[ '-Pdart-defines=Zm9vPTI=,YmFyPTI=', '-Pdart-obfuscation=true', + '-Pfrontend-server-starter-path=foo/bar/frontend_server_starter.dart', '-Pextra-front-end-options=--enable-experiment=non-nullable,bar', '-Pextra-gen-snapshot-options=--enable-experiment=non-nullable,fizz', '-Psplit-debug-info=foo/', diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart index c5bd533f12712..22bfa2ae942de 100644 --- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart @@ -701,7 +701,7 @@ class TestTarget extends Target { @override bool canSkip(Environment environment) { if (_canSkip != null) { - return _canSkip!(environment); + return _canSkip(environment); } return super.canSkip(environment); } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 5aab1be653bf3..a287f1b0d0923 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -71,10 +71,21 @@ void main() { throwsA(isA<MissingDefineException>())); }); + const String emptyNativeAssets = ''' +format-version: + - 1 + - 0 + - 0 +native-assets: {} +'''; + testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -102,6 +113,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], exitCode: 1), @@ -115,6 +128,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -142,6 +158,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -156,6 +174,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -183,6 +204,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -194,10 +217,60 @@ void main() { expect(processManager, hasNoRemainingExpectations); }); + testWithoutContext('KernelSnapshot correctly forwards FrontendServerStarterPath', () async { + fileSystem.file('.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); + final String build = androidEnvironment.buildDir.path; + final String flutterPatchedSdkPath = artifacts.getArtifactPath( + Artifact.flutterPatchedSdkPath, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ); + processManager.addCommands(<FakeCommand>[ + FakeCommand(command: <String>[ + artifacts.getArtifactPath(Artifact.engineDartBinary), + '--disable-dart-dev', + 'path/to/frontend_server_starter.dart', + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + ...buildModeOptions(BuildMode.profile, <String>[]), + '--track-widget-creation', + '--aot', + '--tfa', + '--target-os', + 'android', + '--packages', + '/.dart_tool/package_config.json', + '--output-dill', + '$build/app.dill', + '--depfile', + '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', + '--verbosity=error', + 'file:///lib/main.dart', + ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), + ]); + + await const KernelSnapshot() + .build(androidEnvironment..defines[kFrontendServerStarterPath] = 'path/to/frontend_server_starter.dart'); + + expect(processManager, hasNoRemainingExpectations); + }); + testWithoutContext('KernelSnapshot correctly forwards ExtraFrontEndOptions', () async { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -225,6 +298,8 @@ void main() { '$build/app.dill', '--depfile', '$build/kernel_snapshot.d', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'foo', 'bar', @@ -242,6 +317,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -268,6 +346,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -284,6 +364,9 @@ void main() { fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2, "packages":[]}'); + androidEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = androidEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -309,6 +392,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'), @@ -338,6 +423,9 @@ void main() { fileSystem: fileSystem, logger: logger, ); + testEnvironment.buildDir.childFile('native_assets.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(emptyNativeAssets); final String build = testEnvironment.buildDir.path; final String flutterPatchedSdkPath = artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, @@ -365,6 +453,8 @@ void main() { '--incremental', '--initialize-from-dill', '$build/app.dill', + '--native-assets', + '$build/native_assets.yaml', '--verbosity=error', 'file:///lib/main.dart', ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'), diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart index 7c78b96eb89f7..bee12ddd04c68 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart @@ -86,7 +86,7 @@ flutter: pluginClass: none environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=1.20.0" '''; diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart new file mode 100644 index 0000000000000..36d4628ad9eeb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart @@ -0,0 +1,158 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/exceptions.dart'; +import 'package:flutter_tools/src/build_system/targets/native_assets.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config.dart' show Package; + +import '../../../src/common.dart'; +import '../../../src/context.dart'; +import '../../../src/fakes.dart'; +import '../../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment iosEnvironment; + late Artifacts artifacts; + late FileSystem fileSystem; + late Logger logger; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + iosEnvironment = Environment.test( + fileSystem.currentDirectory, + defines: <String, String>{ + kBuildMode: BuildMode.profile.cliName, + kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios), + kIosArchs: 'arm64', + kSdkRoot: 'path/to/iPhoneOS.sdk', + }, + inputs: <String, String>{}, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + iosEnvironment.buildDir.createSync(recursive: true); + }); + + testWithoutContext('NativeAssets throws error if missing target platform', () async { + iosEnvironment.defines.remove(kTargetPlatform); + expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>())); + }); + + testUsingContext('NativeAssets throws error if missing ios archs', () async { + await createPackageConfig(iosEnvironment); + + iosEnvironment.defines.remove(kIosArchs); + expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>())); + }); + + testUsingContext('NativeAssets throws error if missing sdk root', () async { + await createPackageConfig(iosEnvironment); + + iosEnvironment.defines.remove(kSdkRoot); + expect(const NativeAssets().build(iosEnvironment), throwsA(isA<MissingDefineException>())); + }); + + // The NativeAssets Target should _always_ be creating a yaml an d file. + // The caching logic depends on this. + for (final bool isNativeAssetsEnabled in <bool>[true, false]) { + final String postFix = isNativeAssetsEnabled ? 'enabled' : 'disabled'; + testUsingContext( + 'Successfull native_assets.yaml and native_assets.d creation with feature $postFix', + overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: () => TestFeatureFlags( + isNativeAssetsEnabled: isNativeAssetsEnabled, + ), + }, + () async { + await createPackageConfig(iosEnvironment); + + final NativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner(); + await NativeAssets(buildRunner: buildRunner).build(iosEnvironment); + + expect(iosEnvironment.buildDir.childFile('native_assets.d'), exists); + expect(iosEnvironment.buildDir.childFile('native_assets.yaml'), exists); + }, + ); + } + + testUsingContext( + 'NativeAssets with an asset', + overrides: <Type, Generator>{ + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }, + () async { + await createPackageConfig(iosEnvironment); + + final NativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[Package('foo', iosEnvironment.buildDir.uri)], + buildResult: FakeNativeAssetsBuilderResult(assets: <native_assets_cli.Asset>[ + native_assets_cli.Asset( + id: 'package:foo/foo.dart', + linkMode: native_assets_cli.LinkMode.dynamic, + target: native_assets_cli.Target.iOSArm64, + path: native_assets_cli.AssetAbsolutePath( + Uri.file('libfoo.dylib'), + ), + ) + ], dependencies: <Uri>[ + Uri.file('src/foo.c'), + ]), + ); + await NativeAssets(buildRunner: buildRunner).build(iosEnvironment); + + final File nativeAssetsYaml = iosEnvironment.buildDir.childFile('native_assets.yaml'); + final File depsFile = iosEnvironment.buildDir.childFile('native_assets.d'); + expect(depsFile, exists); + // We don't care about the specific format, but it should contain the + // yaml as the file depending on the source files that went in to the + // build. + expect( + depsFile.readAsStringSync(), + stringContainsInOrder(<String>[ + nativeAssetsYaml.path, + ':', + 'src/foo.c', + ]), + ); + expect(nativeAssetsYaml, exists); + // We don't care about the specific format, but it should contain the + // asset id and the path to the dylib. + expect( + nativeAssetsYaml.readAsStringSync(), + stringContainsInOrder(<String>[ + 'package:foo/foo.dart', + 'libfoo.dylib', + ]), + ); + }, + ); +} + +Future<void> createPackageConfig(Environment iosEnvironment) async { + final File packageConfig = iosEnvironment.projectDir + .childDirectory('.dart_tool') + .childFile('package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index 8e47324eda371..a736b2b3b9a71 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -391,6 +391,38 @@ void main() { ProcessManager: () => processManager, })); + test('Dart2JSTarget ignores frontend server starter path option when calling dart2js', () => testbed.run(() async { + environment.defines[kBuildMode] = 'profile'; + environment.defines[kFrontendServerStarterPath] = 'path/to/frontend_server_starter.dart'; + processManager.addCommand(FakeCommand( + command: <String>[ + ..._kDart2jsLinuxArgs, + '-Ddart.vm.profile=true', + '--no-source-maps', + '-o', + environment.buildDir.childFile('app.dill').absolute.path, + '--packages=.dart_tool/package_config.json', + '--cfe-only', + environment.buildDir.childFile('main.dart').absolute.path, + ] + )); + processManager.addCommand(FakeCommand( + command: <String>[ + ..._kDart2jsLinuxArgs, + '-Ddart.vm.profile=true', + '--no-minify', + '--no-source-maps', + '-O4', + '-o', + environment.buildDir.childFile('main.dart.js').absolute.path, + environment.buildDir.childFile('app.dill').absolute.path, + ] + )); + + await Dart2JSTarget(WebRendererMode.auto).build(environment); + }, overrides: <Type, Generator>{ + ProcessManager: () => processManager, + })); test('Dart2JSTarget calls dart2js with expected args with enabled experiment', () => testbed.run(() async { environment.defines[kBuildMode] = 'profile'; diff --git a/packages/flutter_tools/test/general.shard/bundle_builder_test.dart b/packages/flutter_tools/test/general.shard/bundle_builder_test.dart index 190400acb5800..fb854f24c71ef 100644 --- a/packages/flutter_tools/test/general.shard/bundle_builder_test.dart +++ b/packages/flutter_tools/test/general.shard/bundle_builder_test.dart @@ -87,6 +87,7 @@ void main() { BuildMode.debug, null, trackWidgetCreation: true, + frontendServerStarterPath: 'path/to/frontend_server_starter.dart', extraFrontEndOptions: <String>['test1', 'test2'], extraGenSnapshotOptions: <String>['test3', 'test4'], fileSystemRoots: <String>['test5', 'test6'], @@ -106,6 +107,7 @@ void main() { expect(env!.defines[kTargetPlatform], 'ios'); expect(env!.defines[kTargetFile], mainPath); expect(env!.defines[kTrackWidgetCreation], 'true'); + expect(env!.defines[kFrontendServerStarterPath], 'path/to/frontend_server_starter.dart'); expect(env!.defines[kExtraFrontEndOptions], 'test1,test2'); expect(env!.defines[kExtraGenSnapshotOptions], 'test3,test4'); expect(env!.defines[kFileSystemRoots], 'test5,test6'); @@ -118,14 +120,14 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); - testWithoutContext('--flutter-widget-cache and --enable-experiment are removed from getDefaultCachedKernelPath hash', () { + testWithoutContext('--enable-experiment is removed from getDefaultCachedKernelPath hash', () { final FileSystem fileSystem = MemoryFileSystem.test(); final Config config = Config.test(); expect(getDefaultCachedKernelPath( trackWidgetCreation: true, dartDefines: <String>[], - extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'], + extraFrontEndOptions: <String>['--enable-experiment=foo'], fileSystem: fileSystem, config: config, ), 'build/cache.dill.track.dill'); @@ -133,7 +135,7 @@ void main() { expect(getDefaultCachedKernelPath( trackWidgetCreation: true, dartDefines: <String>['foo=bar'], - extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'], + extraFrontEndOptions: <String>['--enable-experiment=foo'], fileSystem: fileSystem, config: config, ), 'build/06ad47d8e64bd28de537b62ff85357c4.cache.dill.track.dill'); @@ -141,7 +143,7 @@ void main() { expect(getDefaultCachedKernelPath( trackWidgetCreation: false, dartDefines: <String>[], - extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'], + extraFrontEndOptions: <String>['--enable-experiment=foo'], fileSystem: fileSystem, config: config, ), 'build/cache.dill'); @@ -149,7 +151,7 @@ void main() { expect(getDefaultCachedKernelPath( trackWidgetCreation: true, dartDefines: <String>[], - extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache', '--foo=bar'], + extraFrontEndOptions: <String>['--enable-experiment=foo', '--foo=bar'], fileSystem: fileSystem, config: config, ), 'build/95b595cca01caa5f0ca0a690339dd7f6.cache.dill.track.dill'); diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index da9f6e10c8a9e..f5b7b6a6454bb 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -334,6 +334,37 @@ void main() { expect(logger.warningText, contains('Flutter assets will be downloaded from $baseUrl')); expect(logger.statusText, isEmpty); }); + + testWithoutContext('a non-empty realm is included in the storage url', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final Directory internalDir = fileSystem.currentDirectory + .childDirectory('cache') + .childDirectory('bin') + .childDirectory('internal'); + final File engineVersionFile = internalDir.childFile('engine.version'); + engineVersionFile.createSync(recursive: true); + engineVersionFile.writeAsStringSync('abcdef'); + + final File engineRealmFile = internalDir.childFile('engine.realm'); + engineRealmFile.createSync(recursive: true); + engineRealmFile.writeAsStringSync('flutter_archives_v2'); + + final Cache cache = Cache.test( + processManager: FakeProcessManager.any(), + fileSystem: fileSystem, + ); + + expect(cache.storageBaseUrl, contains('flutter_archives_v2')); + }); + + test('bin/internal/engine.realm is empty', () async { + final FileSystem fileSystem = globals.fs; + final String realmFilePath = fileSystem.path.join( + getFlutterRoot(), 'bin', 'internal', 'engine.realm'); + final String realm = fileSystem.file(realmFilePath).readAsStringSync().trim(); + expect(realm, isEmpty, + reason: 'The checked-in engine.realm file must be empty.'); + }); }); testWithoutContext('flattenNameSubdirs', () { diff --git a/packages/flutter_tools/test/general.shard/cmake_test.dart b/packages/flutter_tools/test/general.shard/cmake_test.dart index 091251e3c64d0..861a2e44846b5 100644 --- a/packages/flutter_tools/test/general.shard/cmake_test.dart +++ b/packages/flutter_tools/test/general.shard/cmake_test.dart @@ -11,23 +11,20 @@ import 'package:flutter_tools/src/cmake.dart'; import 'package:flutter_tools/src/project.dart'; import '../src/common.dart'; -import '../src/context.dart'; const String _kTestFlutterRoot = '/flutter'; const String _kTestWindowsFlutterRoot = r'C:\flutter'; void main() { late FileSystem fileSystem; - late ProcessManager processManager; late BufferLogger logger; setUp(() { - processManager = FakeProcessManager.any(); fileSystem = MemoryFileSystem.test(); logger = BufferLogger.test(); }); - testUsingContext('parses executable name from cmake file', () async { + testWithoutContext('parses executable name from cmake file', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); @@ -38,24 +35,18 @@ void main() { final String? name = getCmakeExecutableName(cmakeProject); expect(name, 'hello'); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('defaults executable name to null if cmake config does not exist', () async { + testWithoutContext('defaults executable name to null if cmake config does not exist', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); final String? name = getCmakeExecutableName(cmakeProject); expect(name, isNull); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generates config', () async { + testWithoutContext('generates config', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false); @@ -66,6 +57,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -91,12 +83,9 @@ void main() { r' "PROJECT_DIR=/"', r')', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('config escapes backslashes', () async { + testWithoutContext('config escapes backslashes', () async { fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); @@ -112,6 +101,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -138,12 +128,9 @@ void main() { r' "TEST=hello\\world"', r')', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses pubspec version', () async { + testWithoutContext('generated config uses pubspec version', () async { fileSystem.file('pubspec.yaml') ..createSync() ..writeAsStringSync('version: 1.2.3+4'); @@ -158,6 +145,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -173,12 +161,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 4 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build name', () async { + testWithoutContext('generated config uses build name', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -194,6 +179,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -209,12 +195,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build number', () async { + testWithoutContext('generated config uses build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -230,6 +213,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -245,12 +229,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 0 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 4 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build name and build number', () async { + testWithoutContext('generated config uses build name and build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -267,6 +248,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -282,12 +264,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 4 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build name over pubspec version', () async { + testWithoutContext('generated config uses build name over pubspec version', () async { fileSystem.file('pubspec.yaml') ..createSync() ..writeAsStringSync('version: 9.9.9+9'); @@ -307,6 +286,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -322,12 +302,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build number over pubspec version', () async { + testWithoutContext('generated config uses build number over pubspec version', () async { fileSystem.file('pubspec.yaml') ..createSync() ..writeAsStringSync('version: 1.2.3+4'); @@ -347,6 +324,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -362,12 +340,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 5 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config uses build name and build number over pubspec version', () async { + testWithoutContext('generated config uses build name and build number over pubspec version', () async { fileSystem.file('pubspec.yaml') ..createSync() ..writeAsStringSync('version: 9.9.9+9'); @@ -388,6 +363,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -403,12 +379,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 4 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, }); - testUsingContext('generated config ignores invalid build name', () async { + testWithoutContext('generated config ignores invalid build name', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -424,6 +397,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -441,13 +415,9 @@ void main() { ])); expect(logger.warningText, contains('Warning: could not parse version hello.world, defaulting to 1.0.0.')); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); - testUsingContext('generated config ignores invalid build number', () async { + testWithoutContext('generated config ignores invalid build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -464,6 +434,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); final File cmakeConfig = cmakeProject.generatedCmakeConfigFile; @@ -481,13 +452,9 @@ void main() { ])); expect(logger.warningText, contains('Warning: could not parse version 1.2.3+foo_bar, defaulting to 1.0.0.')); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); - testUsingContext('generated config handles non-numeric build number', () async { + testWithoutContext('generated config handles non-numeric build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -504,6 +471,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); expect(logger.warningText, isEmpty); @@ -521,13 +489,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); - testUsingContext('generated config handles complex build number', () async { + testWithoutContext('generated config handles complex build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = _FakeProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -544,6 +508,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); expect(logger.warningText, isEmpty); @@ -561,13 +526,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); - testUsingContext('generated config warns on Windows project with non-numeric build number', () async { + testWithoutContext('generated config warns on Windows project with non-numeric build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = WindowsProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -584,6 +545,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); expect(logger.warningText, contains( @@ -605,13 +567,9 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); - testUsingContext('generated config warns on Windows project with complex build number', () async { + testWithoutContext('generated config warns on Windows project with complex build number', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final CmakeBasedProject cmakeProject = WindowsProject.fromFlutter(project); const BuildInfo buildInfo = BuildInfo( @@ -628,6 +586,7 @@ void main() { cmakeProject, buildInfo, environment, + logger, ); expect(logger.warningText, contains( @@ -649,10 +608,6 @@ void main() { 'set(FLUTTER_VERSION_PATCH 3 PARENT_SCOPE)', 'set(FLUTTER_VERSION_BUILD 0 PARENT_SCOPE)', ])); - }, overrides: <Type, Generator>{ - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - Logger: () => logger, }); } diff --git a/packages/flutter_tools/test/general.shard/commands/build_test.dart b/packages/flutter_tools/test/general.shard/commands/build_test.dart index 33e6a5e78da7c..9663a387cb848 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_test.dart @@ -5,7 +5,6 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; -import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; @@ -75,7 +74,6 @@ void main() { ), ), AttachCommand( - artifacts: Artifacts.test(), stdio: FakeStdio(), logger: logger, terminal: FakeTerminal(), diff --git a/packages/flutter_tools/test/general.shard/compile_batch_test.dart b/packages/flutter_tools/test/general.shard/compile_batch_test.dart index 0b17a694bf446..e82fb93fe600e 100644 --- a/packages/flutter_tools/test/general.shard/compile_batch_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_batch_test.dart @@ -435,4 +435,55 @@ void main() { completer.complete(); await output; }); + + testWithoutContext('KernelCompiler passes native assets', () async { + final BufferLogger logger = BufferLogger.test(); + final StdoutHandler stdoutHandler = StdoutHandler(logger: logger, fileSystem: MemoryFileSystem.test()); + final Completer<void> completer = Completer<void>(); + + final KernelCompiler kernelCompiler = KernelCompiler( + artifacts: Artifacts.test(), + fileSystem: MemoryFileSystem.test(), + fileSystemRoots: <String>[], + fileSystemScheme: '', + logger: logger, + processManager: FakeProcessManager.list(<FakeCommand>[ + FakeCommand(command: const <String>[ + 'Artifact.engineDartBinary', + '--disable-dart-dev', + 'Artifact.frontendServerSnapshotForEngineDartSdk', + '--sdk-root', + '/path/to/sdkroot/', + '--target=flutter', + '--no-print-incremental-dependencies', + '-Ddart.vm.profile=false', + '-Ddart.vm.product=false', + '--enable-asserts', + '--no-link-platform', + '--packages', + '.packages', + '--native-assets', + 'path/to/native_assets.yaml', + '--verbosity=error', + 'file:///path/to/main.dart', + ], completer: completer), + ]), + stdoutHandler: stdoutHandler, + ); + final Future<CompilerOutput?> output = kernelCompiler.compile( + sdkRoot: '/path/to/sdkroot', + mainPath: '/path/to/main.dart', + buildMode: BuildMode.debug, + trackWidgetCreation: false, + dartDefines: const <String>[], + packageConfig: PackageConfig.empty, + packagesPath: '.packages', + nativeAssets: 'path/to/native_assets.yaml', + ); + stdoutHandler.compilerOutput + ?.complete(const CompilerOutput('', 0, <Uri>[])); + completer.complete(); + + expect((await output)?.outputFilename, ''); + }); } diff --git a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart index be5792e5b85f1..9ecce507434e3 100644 --- a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart +++ b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart @@ -6,6 +6,8 @@ import 'dart:convert' show jsonEncode; import 'dart:io' show Directory, File; import 'package:coverage/src/hitmap.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart' show FileSystem; import 'package:flutter_tools/src/test/coverage_collector.dart'; import 'package:flutter_tools/src/test/test_device.dart' show TestDevice; import 'package:flutter_tools/src/test/test_time_recorder.dart'; @@ -13,6 +15,7 @@ import 'package:stream_channel/stream_channel.dart' show StreamChannel; import 'package:vm_service/vm_service.dart'; import '../src/common.dart'; +import '../src/context.dart'; import '../src/fake_vm_services.dart'; import '../src/logging_logger.dart'; @@ -515,6 +518,52 @@ void main() { } }); + testUsingContext('Coverage collector respects libraryNames in finalized report', () async { + Directory? tempDir; + try { + tempDir = Directory.systemTemp.createTempSync('flutter_coverage_collector_test.'); + final File packagesFile = writeFooBarPackagesJson(tempDir); + File('${tempDir.path}/foo/foo.dart').createSync(recursive: true); + File('${tempDir.path}/bar/bar.dart').createSync(recursive: true); + + final String packagesPath = packagesFile.path; + CoverageCollector collector = CoverageCollector( + libraryNames: <String>{'foo', 'bar'}, + verbose: false, + packagesPath: packagesPath, + resolver: await CoverageCollector.getResolver(packagesPath) + ); + await collector.collectCoverage( + TestTestDevice(), + serviceOverride: createFakeVmServiceHostWithFooAndBar(libraryFilters: <String>['package:foo/', 'package:bar/']).vmService, + ); + + String? report = await collector.finalizeCoverage(); + expect(report, contains('foo.dart')); + expect(report, contains('bar.dart')); + + collector = CoverageCollector( + libraryNames: <String>{'foo'}, + verbose: false, + packagesPath: packagesPath, + resolver: await CoverageCollector.getResolver(packagesPath) + ); + await collector.collectCoverage( + TestTestDevice(), + serviceOverride: createFakeVmServiceHostWithFooAndBar(libraryFilters: <String>['package:foo/']).vmService, + ); + + report = await collector.finalizeCoverage(); + expect(report, contains('foo.dart')); + expect(report, isNot(contains('bar.dart'))); + } finally { + tempDir?.deleteSync(recursive: true); + } + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + }); + testWithoutContext('Coverage collector records test timings when provided TestTimeRecorder', () async { Directory? tempDir; try { diff --git a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart index 5bb3fa36759ba..be8e2394375d2 100644 --- a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart +++ b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart @@ -100,7 +100,7 @@ void main() { expect(logger.statusText, contains('NoPIIFakeDoctorText')); expect(logger.statusText, isNot(contains('Ignored'))); expect(logger.statusText, contains('https://github.com/flutter/flutter/issues/new')); - expect(logger.errorText, contains('A crash report has been written to ${file.path}.')); + expect(logger.errorText.trim(), 'A crash report has been written to ${file.path}'); }); testWithoutContext('suppress analytics', () async { diff --git a/packages/flutter_tools/test/general.shard/create_config_test.dart b/packages/flutter_tools/test/general.shard/create_config_test.dart index 4d41ec9e965c7..bc48b2d998df1 100644 --- a/packages/flutter_tools/test/general.shard/create_config_test.dart +++ b/packages/flutter_tools/test/general.shard/create_config_test.dart @@ -20,6 +20,18 @@ void main() { expect(isValidPackageName('Foo_bar'), false); }); + test('Suggests a valid Pub package name', () { + expect(potentialValidPackageName('92'), '_92'); + expect(potentialValidPackageName('a-b-c'), 'a_b_c'); + + + expect(potentialValidPackageName('Foo_bar'), 'foo_bar'); + expect(potentialValidPackageName('foo-_bar'), 'foo__bar'); + + expect(potentialValidPackageName('잘못된 이름'), isNull, reason: 'It should return null if it cannot find a valid name.'); + + }); + test('kWindowsDrivePattern', () { expect(CreateBase.kWindowsDrivePattern.hasMatch(r'D:\'), isFalse); expect(CreateBase.kWindowsDrivePattern.hasMatch(r'z:\'), isFalse); diff --git a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart index 981ead6085bce..fd18c1ce27219 100644 --- a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart +++ b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart @@ -644,6 +644,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem }) async {} } diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index d5b958ae23958..4dd3e0bb22818 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -44,7 +44,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -63,7 +63,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', env: <String, String>{ 'MY_TEST_ENV': 'MY_TEST_VALUE', @@ -85,7 +85,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -112,7 +112,7 @@ void main() { ); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -146,7 +146,7 @@ void main() { ); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -170,7 +170,7 @@ void main() { ); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -195,7 +195,7 @@ void main() { final Completer<void> launchCompleter = Completer<void>(); final FlutterLaunchRequestArguments launchArgs = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); final Completer<void> restartCompleter = Completer<void>(); @@ -221,7 +221,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -261,7 +261,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', ); await adapter.configurationDoneRequest(MockRequest(), null, () {}); @@ -280,7 +280,7 @@ void main() { final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', program: 'program/main.dart', ); @@ -308,7 +308,7 @@ void main() { final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', program: 'program/main.dart', vmServiceUri: 'ws://1.2.3.4/ws' ); @@ -340,7 +340,7 @@ void main() { final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', program: 'program/main.dart', vmServiceInfoFile: serviceInfoFile.path, ); @@ -374,7 +374,7 @@ void main() { final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', program: 'program/main.dart', vmServiceInfoFile: serviceInfoFile.path, ); @@ -408,7 +408,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', ); await adapter.configurationDoneRequest(MockRequest(), null, () {}); @@ -430,7 +430,7 @@ void main() { ); final FlutterAttachRequestArguments args = FlutterAttachRequestArguments( - cwd: '/project', + cwd: '.', ); await adapter.configurationDoneRequest(MockRequest(), null, () {}); @@ -518,7 +518,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', ); @@ -537,7 +537,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', noDebug: true, ); @@ -557,7 +557,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', toolArgs: <String>['--profile'], ); @@ -577,7 +577,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', toolArgs: <String>['--release'], ); @@ -598,7 +598,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', toolArgs: <String>['tool_arg'], noDebug: true, @@ -659,7 +659,7 @@ void main() { platform: platform, ); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', customTool: '/custom/flutter', noDebug: true, @@ -681,7 +681,7 @@ void main() { platform: platform, ); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', customTool: '/custom/flutter', customToolReplacesArgs: 9999, // replaces all built-in args diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_test_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_test_adapter_test.dart index a2717ea7382ee..26710f2d98144 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_test_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_test_adapter_test.dart @@ -37,7 +37,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final MockRequest request = MockRequest(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', toolArgs: <String>['tool_arg'], noDebug: true, @@ -59,7 +59,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final MockRequest request = MockRequest(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', env: <String, String>{ 'MY_TEST_ENV': 'MY_TEST_VALUE', @@ -82,7 +82,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final MockRequest request = MockRequest(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', customTool: '/custom/flutter', noDebug: true, @@ -105,7 +105,7 @@ void main() { final Completer<void> responseCompleter = Completer<void>(); final MockRequest request = MockRequest(); final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( - cwd: '/project', + cwd: '.', program: 'foo.dart', customTool: '/custom/flutter', customToolReplacesArgs: 9999, // replaces all built-in args diff --git a/packages/flutter_tools/test/general.shard/desktop_device_test.dart b/packages/flutter_tools/test/general.shard/desktop_device_test.dart index 57631ac8fe70c..39890473e67ea 100644 --- a/packages/flutter_tools/test/general.shard/desktop_device_test.dart +++ b/packages/flutter_tools/test/general.shard/desktop_device_test.dart @@ -155,14 +155,15 @@ void main() { 'FLUTTER_ENGINE_SWITCH_10': 'dump-skp-on-shader-compilation=true', 'FLUTTER_ENGINE_SWITCH_11': 'cache-sksl=true', 'FLUTTER_ENGINE_SWITCH_12': 'purge-persistent-cache=true', - 'FLUTTER_ENGINE_SWITCH_13': 'enable-checked-mode=true', - 'FLUTTER_ENGINE_SWITCH_14': 'verify-entry-points=true', - 'FLUTTER_ENGINE_SWITCH_15': 'start-paused=true', - 'FLUTTER_ENGINE_SWITCH_16': 'disable-service-auth-codes=true', - 'FLUTTER_ENGINE_SWITCH_17': 'dart-flags=--null_assertions', - 'FLUTTER_ENGINE_SWITCH_18': 'use-test-fonts=true', - 'FLUTTER_ENGINE_SWITCH_19': 'verbose-logging=true', - 'FLUTTER_ENGINE_SWITCHES': '19', + 'FLUTTER_ENGINE_SWITCH_13': 'enable-impeller=false', + 'FLUTTER_ENGINE_SWITCH_14': 'enable-checked-mode=true', + 'FLUTTER_ENGINE_SWITCH_15': 'verify-entry-points=true', + 'FLUTTER_ENGINE_SWITCH_16': 'start-paused=true', + 'FLUTTER_ENGINE_SWITCH_17': 'disable-service-auth-codes=true', + 'FLUTTER_ENGINE_SWITCH_18': 'dart-flags=--null_assertions', + 'FLUTTER_ENGINE_SWITCH_19': 'use-test-fonts=true', + 'FLUTTER_ENGINE_SWITCH_20': 'verbose-logging=true', + 'FLUTTER_ENGINE_SWITCHES': '20', } ), ]); @@ -209,7 +210,8 @@ void main() { 'FLUTTER_ENGINE_SWITCH_2': 'trace-startup=true', 'FLUTTER_ENGINE_SWITCH_3': 'trace-allowlist=foo,bar', 'FLUTTER_ENGINE_SWITCH_4': 'cache-sksl=true', - 'FLUTTER_ENGINE_SWITCHES': '4', + 'FLUTTER_ENGINE_SWITCH_5': 'enable-impeller=false', + 'FLUTTER_ENGINE_SWITCHES': '5', } ), ]); @@ -301,7 +303,7 @@ void main() { ); }); - testWithoutContext('Desktop devices that support impeller pass through the enable-impeller flag', () async { + testWithoutContext('Desktop devices pass through the enable-impeller flag', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand( command: <String>['debug'], @@ -317,7 +319,6 @@ void main() { ]); final FakeDesktopDevice device = setUpDesktopDevice( processManager: processManager, - supportsImpeller: true, ); final FakeApplicationPackage package = FakeApplicationPackage(); @@ -332,16 +333,17 @@ void main() { ); }); - testWithoutContext('Desktop devices that do not support impeller ignore the enable-impeller flag', () async { + testWithoutContext('Desktop devices pass through the --no-enable-impeller flag', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand( command: <String>['debug'], exitCode: -1, environment: <String, String>{ 'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true', - 'FLUTTER_ENGINE_SWITCH_2': 'enable-checked-mode=true', - 'FLUTTER_ENGINE_SWITCH_3': 'verify-entry-points=true', - 'FLUTTER_ENGINE_SWITCHES': '3' + 'FLUTTER_ENGINE_SWITCH_2': 'enable-impeller=false', + 'FLUTTER_ENGINE_SWITCH_3': 'enable-checked-mode=true', + 'FLUTTER_ENGINE_SWITCH_4': 'verify-entry-points=true', + 'FLUTTER_ENGINE_SWITCHES': '4' } ), ]); @@ -355,7 +357,7 @@ void main() { prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled( BuildInfo.debug, - enableImpeller: ImpellerStatus.enabled, + enableImpeller: ImpellerStatus.disabled, dartEntrypointArgs: <String>[], ), ); @@ -368,7 +370,6 @@ FakeDesktopDevice setUpDesktopDevice({ ProcessManager? processManager, OperatingSystemUtils? operatingSystemUtils, bool nullExecutablePathForDevice = false, - bool supportsImpeller = false, }) { return FakeDesktopDevice( fileSystem: fileSystem ?? MemoryFileSystem.test(), @@ -376,7 +377,6 @@ FakeDesktopDevice setUpDesktopDevice({ processManager: processManager ?? FakeProcessManager.any(), operatingSystemUtils: operatingSystemUtils ?? FakeOperatingSystemUtils(), nullExecutablePathForDevice: nullExecutablePathForDevice, - supportsImpeller: supportsImpeller, ); } @@ -388,7 +388,6 @@ class FakeDesktopDevice extends DesktopDevice { required FileSystem fileSystem, required OperatingSystemUtils operatingSystemUtils, this.nullExecutablePathForDevice = false, - this.supportsImpeller = false, }) : super( 'dummy', platformType: PlatformType.linux, @@ -407,9 +406,6 @@ class FakeDesktopDevice extends DesktopDevice { final bool nullExecutablePathForDevice; - @override - final bool supportsImpeller; - @override String get name => 'dummy'; diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart index 1f02688181abe..d4b4e5fcf1098 100644 --- a/packages/flutter_tools/test/general.shard/devfs_test.dart +++ b/packages/flutter_tools/test/general.shard/devfs_test.dart @@ -702,7 +702,18 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { Future<CompilerOutput> Function(Uri mainUri, List<Uri>? invalidatedFiles)? onRecompile; @override - Future<CompilerOutput> recompile(Uri mainUri, List<Uri>? invalidatedFiles, {String? outputPath, PackageConfig? packageConfig, String? projectRootPath, FileSystem? fs, bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant}) { + Future<CompilerOutput> recompile( + Uri mainUri, + List<Uri>? invalidatedFiles, { + String? outputPath, + PackageConfig? packageConfig, + String? projectRootPath, + FileSystem? fs, + bool suppressErrors = false, + bool checkDartPluginRegistry = false, + File? dartPluginRegistrant, + Uri? nativeAssetsYaml, + }) { return onRecompile?.call(mainUri, invalidatedFiles) ?? Future<CompilerOutput>.value(const CompilerOutput('', 1, <Uri>[])); } diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index 860a58ff687ca..14e3dbb6bfeab 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -234,7 +234,6 @@ void main() { FakeAsync().run((FakeAsync time) { final FakePollingDeviceDiscovery pollingDeviceDiscovery = FakePollingDeviceDiscovery(); pollingDeviceDiscovery.startPolling(); - time.elapse(const Duration(milliseconds: 4001)); // First check should use the default polling timeout // to quickly populate the list. diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart index bab05c5169cdb..28f66f2cc2fd6 100644 --- a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart +++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart @@ -258,6 +258,29 @@ void main() { WebRunnerFactory: () => FakeWebRunnerFactory(), }); + testUsingContext('WebDriverService can start an app with a launch url provided', () async { + final WebDriverService service = setUpDriverService(); + final FakeDevice device = FakeDevice(); + const String testUrl = 'http://localhost:1234/test'; + await service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, webLaunchUrl: testUrl), true); + await service.stop(); + expect(service.webUri, Uri.parse(testUrl)); + }, overrides: <Type, Generator>{ + WebRunnerFactory: () => FakeWebRunnerFactory(), + }); + + testUsingContext('WebDriverService will throw when an invalid launch url is provided', () async { + final WebDriverService service = setUpDriverService(); + final FakeDevice device = FakeDevice(); + const String invalidTestUrl = '::INVALID_URL::'; + await expectLater( + service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, webLaunchUrl: invalidTestUrl), true), + throwsA(isA<FormatException>()), + ); + }, overrides: <Type, Generator>{ + WebRunnerFactory: () => FakeWebRunnerFactory(), + }); + testUsingContext('WebDriverService forwards exception when run future fails before app starts', () async { final WebDriverService service = setUpDriverService(); final Device device = FakeDevice(); diff --git a/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart b/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart new file mode 100644 index 0000000000000..bd4f99042effd --- /dev/null +++ b/packages/flutter_tools/test/general.shard/fake_native_assets_build_runner.dart @@ -0,0 +1,91 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_builder/native_assets_builder.dart' + as native_assets_builder; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:package_config/package_config_types.dart'; + +/// Mocks all logic instead of using `package:native_assets_builder`, which +/// relies on doing process calls to `pub` and the local file system. +class FakeNativeAssetsBuildRunner implements NativeAssetsBuildRunner { + FakeNativeAssetsBuildRunner({ + this.hasPackageConfigResult = true, + this.packagesWithNativeAssetsResult = const <Package>[], + this.dryRunResult = const FakeNativeAssetsBuilderResult(), + this.buildResult = const FakeNativeAssetsBuilderResult(), + CCompilerConfig? cCompilerConfigResult, + }) : cCompilerConfigResult = cCompilerConfigResult ?? CCompilerConfig(); + + final native_assets_builder.BuildResult buildResult; + final native_assets_builder.DryRunResult dryRunResult; + final bool hasPackageConfigResult; + final List<Package> packagesWithNativeAssetsResult; + final CCompilerConfig cCompilerConfigResult; + + int buildInvocations = 0; + int dryRunInvocations = 0; + int hasPackageConfigInvocations = 0; + int packagesWithNativeAssetsInvocations = 0; + + @override + Future<native_assets_builder.BuildResult> build({ + required bool includeParentEnvironment, + required BuildMode buildMode, + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + CCompilerConfig? cCompilerConfig, + int? targetAndroidNdkApi, + IOSSdk? targetIOSSdk, + }) async { + buildInvocations++; + return buildResult; + } + + @override + Future<native_assets_builder.DryRunResult> dryRun({ + required bool includeParentEnvironment, + required LinkModePreference linkModePreference, + required OS targetOS, + required Uri workingDirectory, + }) async { + dryRunInvocations++; + return dryRunResult; + } + + @override + Future<bool> hasPackageConfig() async { + hasPackageConfigInvocations++; + return hasPackageConfigResult; + } + + @override + Future<List<Package>> packagesWithNativeAssets() async { + packagesWithNativeAssetsInvocations++; + return packagesWithNativeAssetsResult; + } + + @override + Future<CCompilerConfig> get cCompilerConfig async => cCompilerConfigResult; +} + +final class FakeNativeAssetsBuilderResult + implements native_assets_builder.BuildResult { + const FakeNativeAssetsBuilderResult({ + this.assets = const <Asset>[], + this.dependencies = const <Uri>[], + this.success = true, + }); + + @override + final List<Asset> assets; + + @override + final List<Uri> dependencies; + + @override + final bool success; +} diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index f15220e90b542..07d870b0f5363 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -400,5 +400,13 @@ void main() { }); } + test('${nativeAssets.name} availability and default enabled', () { + expect(nativeAssets.master.enabledByDefault, false); + expect(nativeAssets.master.available, true); + expect(nativeAssets.beta.enabledByDefault, false); + expect(nativeAssets.beta.available, false); + expect(nativeAssets.stable.enabledByDefault, false); + expect(nativeAssets.stable.available, false); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart index 89510dcd47692..6f4b5f65e6be4 100644 --- a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/test/flutter_platform.dart'; -import 'package:test/fake.dart'; import 'package:test_core/backend.dart'; // ignore: deprecated_member_use import '../src/common.dart'; @@ -25,6 +24,11 @@ void main() { }); group('FlutterPlatform', () { + late SuitePlatform fakeSuitePlatform; + setUp(() { + fakeSuitePlatform = SuitePlatform(Runtime.vm); + }); + testUsingContext('ensureConfiguration throws an error if an ' 'explicitVmServicePort is specified and more than one test file', () async { final FlutterPlatform flutterPlatform = FlutterPlatform( @@ -35,9 +39,9 @@ void main() { ), enableVmService: false, ); - flutterPlatform.loadChannel('test1.dart', FakeSuitePlatform()); + flutterPlatform.loadChannel('test1.dart', fakeSuitePlatform); - expect(() => flutterPlatform.loadChannel('test2.dart', FakeSuitePlatform()), throwsToolExit()); + expect(() => flutterPlatform.loadChannel('test2.dart', fakeSuitePlatform), throwsToolExit()); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -51,9 +55,9 @@ void main() { precompiledDillPath: 'example.dill', enableVmService: false, ); - flutterPlatform.loadChannel('test1.dart', FakeSuitePlatform()); + flutterPlatform.loadChannel('test1.dart', fakeSuitePlatform); - expect(() => flutterPlatform.loadChannel('test2.dart', FakeSuitePlatform()), throwsToolExit()); + expect(() => flutterPlatform.loadChannel('test2.dart', fakeSuitePlatform), throwsToolExit()); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -119,5 +123,3 @@ void main() { }); }); } - -class FakeSuitePlatform extends Fake implements SuitePlatform { } diff --git a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart index f0033b9e871cf..6e2970faf8b90 100644 --- a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart @@ -5,10 +5,13 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/flutter_project_metadata.dart'; import 'package:flutter_tools/src/project.dart'; import '../src/common.dart'; +import '../src/context.dart'; +import '../src/fakes.dart'; void main() { late FileSystem fileSystem; @@ -184,4 +187,16 @@ migration: expect(logger.traceText, contains('The key `create_revision` was not found')); }); + + testUsingContext('enabledValues does not contain packageFfi if native-assets not enabled', () { + expect(FlutterProjectType.enabledValues, isNot(contains(FlutterProjectType.packageFfi))); + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin)); + }); + + testUsingContext('enabledValues contains packageFfi if natives-assets enabled', () { + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.packageFfi)); + expect(FlutterProjectType.enabledValues, contains(FlutterProjectType.plugin)); + }, overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + }); } diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index ba35df50a8805..fa35cfd510b45 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -59,6 +59,13 @@ void _standardFlutterDirectoryL10nSetup(FileSystem fs) { .writeAsStringSync(singleMessageArbFileString); l10nDirectory.childFile(esArbFileName) .writeAsStringSync(singleEsMessageArbFileString); + fs.file('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(''' +flutter: + generate: true +'''); + } void main() { @@ -88,6 +95,7 @@ void main() { bool useEscaping = false, bool areResourceAttributeRequired = false, bool suppressWarnings = false, + bool relaxSyntax = false, void Function(Directory)? setup, } ) { @@ -119,6 +127,7 @@ void main() { useEscaping: useEscaping, areResourceAttributesRequired: areResourceAttributeRequired, suppressWarnings: suppressWarnings, + useRelaxedSyntax: relaxSyntax, ) ..loadResources() ..writeOutputFiles(isFromYaml: isFromYaml); @@ -763,7 +772,7 @@ class FooEn extends Foo { _standardFlutterDirectoryL10nSetup(fs); // Missing flutter: generate: true should throw exception. - fs.file(fs.path.join(syntheticPackagePath, 'pubspec.yaml')) + fs.file('pubspec.yaml') ..createSync(recursive: true) ..writeAsStringSync(''' flutter: @@ -799,6 +808,34 @@ flutter: ); }); + testWithoutContext('uses the same line terminator as pubspec.yaml', () async { + _standardFlutterDirectoryL10nSetup(fs); + + fs.file('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(''' +flutter:\r + generate: true\r +'''); + + final LocalizationOptions options = LocalizationOptions( + arbDir: fs.path.join('lib', 'l10n'), + outputClass: defaultClassNameString, + outputLocalizationFile: defaultOutputFileString, + ); + await generateLocalizations( + fileSystem: fs, + options: options, + logger: BufferLogger.test(), + projectDir: fs.currentDirectory, + dependenciesDir: fs.currentDirectory, + artifacts: artifacts, + processManager: processManager, + ); + final String content = getGeneratedFileContent(locale: 'en'); + expect(content, contains('\r\n')); + }); + testWithoutContext('blank lines generated nicely', () async { _standardFlutterDirectoryL10nSetup(fs); @@ -1440,6 +1477,22 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorld(Object name) {')); expect(getGeneratedFileContent(locale: 'es'), contains('String helloWorld(Object name) {')); }); + + testWithoutContext('braces are ignored as special characters if placeholder does not exist', () { + setupLocalizations(<String, String>{ + 'en': ''' +{ + "helloWorld": "Hello {name}", + "@@helloWorld": { + "placeholders": { + "names": {} + } + } +}''' + }, relaxSyntax: true); + final String content = getGeneratedFileContent(locale: 'en'); + expect(content, contains("String get helloWorld => 'Hello {name}'")); + }); }); group('DateTime tests', () { diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart index 8060f80ef00ad..b30361d367655 100644 --- a/packages/flutter_tools/test/general.shard/hot_test.dart +++ b/packages/flutter_tools/test/general.shard/hot_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - - import 'package:file/memory.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; @@ -16,12 +14,15 @@ import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_devtools_handler.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -29,6 +30,7 @@ import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/context.dart'; import '../src/fakes.dart'; +import 'fake_native_assets_build_runner.dart'; void main() { group('validateReloadReport', () { @@ -178,7 +180,6 @@ void main() { Map<FlutterDevice?, List<FlutterView>> viewCache, void Function(String message)? onSlow, String reloadMessage, - String? fastReassembleClassName, ) async => ReassembleResult( <FlutterView?, FlutterVmService?>{null: null}, false, @@ -296,7 +297,6 @@ void main() { hotEventSdkName: 'Tester', hotEventEmulator: false, hotEventFullRestart: true, - fastReassemble: false, hotEventOverallTimeInMs: 64000, hotEventSyncedBytes: 4, hotEventInvalidatedSourcesCount: 2, @@ -366,6 +366,7 @@ void main() { String? sdkName, bool? emulator, String? reason, + Usage usage, ) async { firstReloadDetails['finalLibraryCount'] = 2; firstReloadDetails['receivedLibraryCount'] = 3; @@ -378,7 +379,6 @@ void main() { Map<FlutterDevice?, List<FlutterView>> viewCache, void Function(String message)? onSlow, String reloadMessage, - String? fastReassembleClassName, ) async => ReassembleResult( <FlutterView?, FlutterVmService?>{null: null}, false, @@ -401,7 +401,6 @@ void main() { hotEventSdkName: 'Tester', hotEventEmulator: false, hotEventFullRestart: false, - fastReassemble: false, hotEventCompileTimeInMs: 16000, hotEventFindInvalidatedTimeInMs: 64000, hotEventScannedSourcesCount: 16, @@ -551,6 +550,134 @@ void main() { expect(flutterDevice2.stoppedEchoingDeviceLog, true); }); }); + + group('native assets', () { + late TestHotRunnerConfig testingConfig; + late FileSystem fileSystem; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + testingConfig = TestHotRunnerConfig( + successfulHotRestartSetup: true, + ); + }); + testUsingContext('native assets restart', () async { + final FakeDevice device = FakeDevice(); + final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); + final List<FlutterDevice> devices = <FlutterDevice>[ + fakeFlutterDevice, + ]; + + fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport( + success: true, + invalidatedSourcesCount: 6, + syncedBytes: 8, + scannedSourcesCount: 16, + compileDuration: const Duration(seconds: 16), + transferDuration: const Duration(seconds: 32), + ); + + (fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri'); + + final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', fileSystem.currentDirectory.uri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ); + + final HotRunner hotRunner = HotRunner( + devices, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), + target: 'main.dart', + devtoolsHandler: createNoOpHandler, + buildRunner: buildRunner, + ); + final OperationResult result = await hotRunner.restart(fullRestart: true); + expect(result.isOk, true); + // Hot restart does not require reruning anything for native assets. + // The previous native assets mapping should be used. + expect(buildRunner.buildInvocations, 0); + expect(buildRunner.dryRunInvocations, 0); + expect(buildRunner.hasPackageConfigInvocations, 0); + expect(buildRunner.packagesWithNativeAssetsInvocations, 0); + }, overrides: <Type, Generator>{ + HotRunnerConfig: () => testingConfig, + Artifacts: () => Artifacts.test(), + FileSystem: () => fileSystem, + Platform: () => FakePlatform(), + ProcessManager: () => FakeProcessManager.empty(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true), + }); + + testUsingContext('native assets run unsupported', () async { + final FakeDevice device = FakeDevice(targetPlatform: TargetPlatform.android_arm64); + final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); + final List<FlutterDevice> devices = <FlutterDevice>[ + fakeFlutterDevice, + ]; + + fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport( + success: true, + invalidatedSourcesCount: 6, + syncedBytes: 8, + scannedSourcesCount: 16, + compileDuration: const Duration(seconds: 16), + transferDuration: const Duration(seconds: 32), + ); + + (fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri'); + + final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', fileSystem.currentDirectory.uri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ); + + final HotRunner hotRunner = HotRunner( + devices, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), + target: 'main.dart', + devtoolsHandler: createNoOpHandler, + buildRunner: buildRunner, + ); + expect( + () => hotRunner.run(), + throwsToolExit( message: + 'Package(s) bar require the native assets feature. ' + 'This feature has not yet been implemented for `TargetPlatform.android_arm64`. ' + 'For more info see https://github.com/flutter/flutter/issues/129757.', + ) + ); + + }, overrides: <Type, Generator>{ + HotRunnerConfig: () => testingConfig, + Artifacts: () => Artifacts.test(), + FileSystem: () => fileSystem, + Platform: () => FakePlatform(), + ProcessManager: () => FakeProcessManager.empty(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true), + }); + }); } class FakeDevFs extends Fake implements DevFS { @@ -583,6 +710,12 @@ class FakeDevFs extends Fake implements DevFS { // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeDevice extends Fake implements Device { + FakeDevice({ + TargetPlatform targetPlatform = TargetPlatform.tester, + }) : _targetPlatform = targetPlatform; + + final TargetPlatform _targetPlatform; + bool disposed = false; @override @@ -598,7 +731,7 @@ class FakeDevice extends Fake implements Device { bool supportsFlutterExit = true; @override - Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester; + Future<TargetPlatform> get targetPlatform async => _targetPlatform; @override Future<String> get sdkNameAndVersion async => 'Tester'; @@ -661,6 +794,9 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { required List<Uri> invalidatedFiles, required PackageConfig packageConfig, }) => updateDevFSReportCallback(); + + @override + TargetPlatform? get targetPlatform => device._targetPlatform; } class TestFlutterDevice extends FlutterDevice { diff --git a/packages/flutter_tools/test/general.shard/intellij/intellij_validator_test.dart b/packages/flutter_tools/test/general.shard/intellij/intellij_validator_test.dart index eab4b0b9f8927..152fc5f333380 100644 --- a/packages/flutter_tools/test/general.shard/intellij/intellij_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/intellij/intellij_validator_test.dart @@ -5,12 +5,14 @@ import 'package:archive/archive.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; import 'package:flutter_tools/src/intellij/intellij_validator.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; +import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -305,7 +307,9 @@ void main() { processManager: processManager, plistParser: FakePlistParser(<String, String>{ PlistParser.kCFBundleShortVersionStringKey: '2020.10', + PlistParser.kCFBundleIdentifierKey: 'com.jetbrains.intellij', }), + logger: BufferLogger.test(), ).whereType<IntelliJValidatorOnMac>(); expect(validators.length, 2); @@ -371,6 +375,115 @@ void main() { expect(validator.pluginsPath, '/path/to/JetBrainsToolboxApp.plugins'); }); + + testWithoutContext('IntelliJValidatorOnMac.installed() handles FileSystemExceptions)', () async { + const FileSystemException exception = FileSystemException('cannot list'); + final FileSystem fileSystem = _ThrowingFileSystem(exception); + + final FakeProcessManager processManager = FakeProcessManager.empty(); + + final Iterable<DoctorValidator> validators = IntelliJValidatorOnMac.installed( + fileSystem: fileSystem, + fileSystemUtils: FileSystemUtils(fileSystem: fileSystem, platform: macPlatform), + userMessages: UserMessages(), + plistParser: FakePlistParser(<String, String>{ + 'JetBrainsToolboxApp': '/path/to/JetBrainsToolboxApp', + 'CFBundleIdentifier': 'com.jetbrains.toolbox.linkapp', + }), + processManager: processManager, + logger: BufferLogger.test(), + ); + + expect(validators.length, 1); + final DoctorValidator validator = validators.first; + expect(validator, isA<ValidatorWithResult>()); + expect(validator.title, 'Cannot determine if IntelliJ is installed'); + }); + + testWithoutContext('Remove JetBrains Toolbox', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final List<String> installPaths = <String>[ + fileSystem.path.join('/', 'foo', 'bar', 'Applications', + 'JetBrains Toolbox', 'IntelliJ IDEA Ultimate.app'), + fileSystem.path.join('/', 'foo', 'bar', 'Applications', + 'JetBrains Toolbox', 'IntelliJ IDEA Community Edition.app') + ]; + + for (final String installPath in installPaths) { + fileSystem.directory(installPath).createSync(recursive: true); + } + + final FakeProcessManager processManager = + FakeProcessManager.list(<FakeCommand>[ + const FakeCommand(command: <String>[ + 'mdfind', + 'kMDItemCFBundleIdentifier="com.jetbrains.intellij.ce"', + ], stdout: 'skip'), + const FakeCommand(command: <String>[ + 'mdfind', + 'kMDItemCFBundleIdentifier="com.jetbrains.intellij*"', + ], stdout: 'skip') + ]); + + final Iterable<DoctorValidator> installed = IntelliJValidatorOnMac.installed( + fileSystem: fileSystem, + fileSystemUtils: FileSystemUtils(fileSystem: fileSystem, platform: macPlatform), + userMessages: UserMessages(), + plistParser: FakePlistParser(<String, String>{ + 'JetBrainsToolboxApp': '/path/to/JetBrainsToolboxApp', + 'CFBundleIdentifier': 'com.jetbrains.toolbox.linkapp', + }), + processManager: processManager, + logger: BufferLogger.test(), + ); + + expect(installed.length, 0); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Does not crash when installation is missing its CFBundleIdentifier property', () async { + final BufferLogger logger = BufferLogger.test(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final String ultimatePath = fileSystem.path.join('/', 'foo', 'bar', 'Applications', + 'JetBrains Toolbox', 'IntelliJ IDEA Ultimate.app'); + final String communityEditionPath = fileSystem.path.join('/', 'foo', 'bar', 'Applications', + 'JetBrains Toolbox', 'IntelliJ IDEA Community Edition.app'); + final List<String> installPaths = <String>[ + ultimatePath, + communityEditionPath + ]; + + for (final String installPath in installPaths) { + fileSystem.directory(installPath).createSync(recursive: true); + } + + final FakeProcessManager processManager = + FakeProcessManager.list(<FakeCommand>[ + FakeCommand(command: const <String>[ + 'mdfind', + 'kMDItemCFBundleIdentifier="com.jetbrains.intellij.ce"', + ], stdout: communityEditionPath), + FakeCommand(command: const <String>[ + 'mdfind', + 'kMDItemCFBundleIdentifier="com.jetbrains.intellij*"', + ], stdout: ultimatePath) + ]); + + final Iterable<DoctorValidator> installed = IntelliJValidatorOnMac.installed( + fileSystem: fileSystem, + fileSystemUtils: FileSystemUtils(fileSystem: fileSystem, platform: macPlatform), + userMessages: UserMessages(), + plistParser: FakePlistParser(<String, String>{ + 'JetBrainsToolboxApp': '/path/to/JetBrainsToolboxApp', + }), + processManager: processManager, + logger: logger, + ); + + expect(installed.length, 2); + expect(logger.traceText, contains('installation at $ultimatePath has a null CFBundleIdentifierKey')); + expect(processManager, hasNoRemainingExpectations); + }); } class IntelliJValidatorTestTarget extends IntelliJValidator { @@ -416,7 +529,6 @@ void createIntellijFlutterPluginJar(String pluginJarPath, FileSystem fileSystem, fileSystem.file(pluginJarPath) ..createSync(recursive: true) ..writeAsBytesSync(ZipEncoder().encode(flutterPlugins)!); - } /// A helper to create a Intellij Dart plugin jar. @@ -454,3 +566,30 @@ void createIntellijDartPluginJar(String pluginJarPath, FileSystem fileSystem) { ..createSync(recursive: true) ..writeAsBytesSync(ZipEncoder().encode(dartPlugins)!); } + +// TODO(fujino): this should use the MemoryFileSystem and a +// FileExceptionHandler, blocked by https://github.com/google/file.dart/issues/227. +class _ThrowingFileSystem extends Fake implements FileSystem { + _ThrowingFileSystem(this._exception); + + final Exception _exception; + final MemoryFileSystem memfs = MemoryFileSystem.test(); + + @override + Context get path => memfs.path; + + @override + Directory directory(dynamic _) => _ThrowingDirectory(_exception); +} + +class _ThrowingDirectory extends Fake implements Directory { + _ThrowingDirectory(this._exception); + + final Exception _exception; + + @override + bool existsSync() => true; + + @override + List<FileSystemEntity> listSync({bool recursive = false, bool followLinks = true}) => throw _exception; +} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart index 72e453823fcb7..7862b4d919158 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart @@ -80,7 +80,7 @@ void main () { expect(iosDeployDebugger.logLines, emits('Did finish launching.')); expect(await iosDeployDebugger.launchAndAttach(), isTrue); - await iosDeployDebugger.logLines.drain(); + await iosDeployDebugger.logLines.drain<Object?>(); expect(processManager, hasNoRemainingExpectations); expect(appDeltaDirectory, exists); }); @@ -141,7 +141,7 @@ void main () { 'process detach', ])); expect(await iosDeployDebugger.launchAndAttach(), isTrue); - await logLines.drain(); + await logLines.drain<Object?>(); expect(logger.traceText, contains('PROCESS_STOPPED')); expect(logger.traceText, contains('thread backtrace all')); @@ -189,7 +189,7 @@ void main () { expect(iosDeployDebugger.logLines, emitsDone); expect(await iosDeployDebugger.launchAndAttach(), isFalse); - await iosDeployDebugger.logLines.drain(); + await iosDeployDebugger.logLines.drain<Object?>(); }); testWithoutContext('app exit', () async { @@ -209,7 +209,7 @@ void main () { ])); expect(await iosDeployDebugger.launchAndAttach(), isTrue); - await iosDeployDebugger.logLines.drain(); + await iosDeployDebugger.logLines.drain<Object?>(); }); testWithoutContext('app crash', () async { @@ -239,7 +239,7 @@ void main () { ])); expect(await iosDeployDebugger.launchAndAttach(), isTrue); - await iosDeployDebugger.logLines.drain(); + await iosDeployDebugger.logLines.drain<Object?>(); expect(logger.traceText, contains('Process 6156 stopped')); expect(logger.traceText, contains('thread backtrace all')); @@ -263,7 +263,7 @@ void main () { expect(iosDeployDebugger.logLines, emitsDone); expect(await iosDeployDebugger.launchAndAttach(), isFalse); - await iosDeployDebugger.logLines.drain(); + await iosDeployDebugger.logLines.drain<Object?>(); }); testWithoutContext('no provisioning profile 1, stdout', () async { @@ -341,6 +341,33 @@ void main () { await iosDeployDebugger.launchAndAttach(); expect(logger.errorText, contains('Try launching from within Xcode')); }); + + testWithoutContext('debugger attached and received logs', () async { + final StreamController<List<int>> stdin = StreamController<List<int>>(); + final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ + FakeCommand( + command: const <String>['ios-deploy'], + stdout: '(lldb) run\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n', + stdin: IOSink(stdin.sink), + ), + ]); + final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( + processManager: processManager, + logger: logger, + ); + final List<String> receivedLogLines = <String>[]; + final Stream<String> logLines = iosDeployDebugger.logLines + ..listen(receivedLogLines.add); + + expect(iosDeployDebugger.logLines, emitsInOrder(<String>[ + 'Log on attach1', + 'Log on attach2', + ])); + expect(await iosDeployDebugger.launchAndAttach(), isTrue); + await logLines.drain<Object?>(); + + expect(LineSplitter.split(logger.traceText), containsOnce('Received logs from ios-deploy.')); + }); }); testWithoutContext('detach', () async { diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 9ca0aceca475e..90f834081b269 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -75,6 +75,8 @@ FakeCommand attachDebuggerCommand({ String stdout = '(lldb) run\nsuccess', Completer<void>? completer, bool isWirelessDevice = false, + bool uninstallFirst = false, + bool skipInstall = false, }) { return FakeCommand( command: <String>[ @@ -87,6 +89,10 @@ FakeCommand attachDebuggerCommand({ '123', '--bundle', '/', + if (uninstallFirst) + '--uninstall', + if (skipInstall) + '--noinstall', '--debug', if (!isWirelessDevice) '--no-wifi', '--args', @@ -207,7 +213,7 @@ void main() { completer.complete(); expect(secondLaunchResult.started, true); expect(secondLaunchResult.hasVmService, true); - expect(await device.stopApp(iosApp), false); + expect(await device.stopApp(iosApp), true); }); testWithoutContext('IOSDevice.startApp launches in debug mode via log reading on <iOS 13', () async { @@ -285,7 +291,7 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); - expect(await device.stopApp(iosApp), false); + expect(await device.stopApp(iosApp), true); expect(logger.errorText, contains('The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...')); expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt')); completer.complete(); @@ -330,7 +336,7 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); - expect(await device.stopApp(iosApp), false); + expect(await device.stopApp(iosApp), true); expect(logger.errorText, contains('The Dart VM Service was not discovered after 45 seconds. This is taking much longer than expected...')); expect(logger.errorText, contains('Click "Allow" to the prompt asking if you would like to find and connect devices on your local network.')); completer.complete(); @@ -339,6 +345,88 @@ void main() { MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(), }); + testWithoutContext('IOSDevice.startApp retries when ios-deploy loses connection the first time in CI', () async { + final BufferLogger logger = BufferLogger.test(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final Completer<void> completer = Completer<void>(); + final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ + attachDebuggerCommand( + stdout: '(lldb) run\nsuccess\nProcess 525 exited with status = -1 (0xffffffff) lost connection', + uninstallFirst: true, + ), + attachDebuggerCommand( + stdout: '(lldb) run\nsuccess\nThe Dart VM service is listening on http://127.0.0.1:456', + completer: completer, + skipInstall: true, + ), + ]); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: fileSystem.currentDirectory, + applicationPackage: fileSystem.currentDirectory, + ); + + device.portForwarder = const NoOpDevicePortForwarder(); + + final LaunchResult launchResult = await device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + usingCISystem: true, + uninstallFirst: true, + ), + platformArgs: <String, dynamic>{}, + ); + completer.complete(); + + expect(processManager, hasNoRemainingExpectations); + expect(launchResult.started, true); + expect(launchResult.hasVmService, true); + expect(await device.stopApp(iosApp), true); + }); + + testWithoutContext('IOSDevice.startApp does not retry when ios-deploy loses connection if not in CI', () async { + final BufferLogger logger = BufferLogger.test(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ + attachDebuggerCommand( + stdout: '(lldb) run\nsuccess\nProcess 525 exited with status = -1 (0xffffffff) lost connection', + ), + ]); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: fileSystem.currentDirectory, + applicationPackage: fileSystem.currentDirectory, + ); + + device.portForwarder = const NoOpDevicePortForwarder(); + + final LaunchResult launchResult = await device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + ), + platformArgs: <String, dynamic>{}, + ); + + expect(processManager, hasNoRemainingExpectations); + expect(launchResult.started, false); + expect(launchResult.hasVmService, false); + expect(await device.stopApp(iosApp), false); + }); + testWithoutContext('IOSDevice.startApp succeeds in release mode', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ diff --git a/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart new file mode 100644 index 0000000000000..baac43c2aa35a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart @@ -0,0 +1,288 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/ios/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: <String, String>{}, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + environment.buildDir.createSync(recursive: true); + projectUri = environment.projectDir.uri; + }); + + testUsingContext('dry run with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + expect( + await dryRunNativeAssetsIOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ), + null, + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('build with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsIOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsIOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('dry run with assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final Uri? nativeAssetsYaml = await dryRunNativeAssetsIOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Dry running native assets for ios.', + 'Dry running native assets for ios done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/ios/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + contains('package:bar/bar.dart'), + ); + }); + + testUsingContext('build with assets but not enabled', () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => buildNativeAssetsIOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('build no assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + await buildNativeAssetsIOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ); + expect( + environment.buildDir.childFile('native_assets.yaml'), + exists, + ); + }); + + testUsingContext('build with assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + <FakeCommand>[ + const FakeCommand( + command: <Pattern>[ + 'lipo', + '-create', + '-output', + '/build/native_assets/ios/bar.dylib', + 'bar.dylib', + ], + ), + const FakeCommand( + command: <Pattern>[ + 'install_name_tool', + '-id', + '@executable_path/Frameworks/bar.dylib', + '/build/native_assets/ios/bar.dylib', + ], + ), + const FakeCommand( + command: <Pattern>[ + 'codesign', + '--force', + '--sign', + '-', + '--timestamp=none', + '/build/native_assets/ios/bar.dylib', + ], + ), + ], + ), + }, () async { + if (const LocalPlatform().isWindows) { + return; // Backslashes in commands, but we will never run these commands on Windows. + } + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + await buildNativeAssetsIOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + environmentType: EnvironmentType.simulator, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + yamlParentDirectory: environment.buildDir.uri, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.iOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Building native assets for [ios_arm64] debug.', + 'Building native assets for [ios_arm64] done.', + ]), + ); + expect( + environment.buildDir.childFile('native_assets.yaml'), + exists, + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index b2f79385050dd..0d5a24ea3c8e7 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -754,7 +754,7 @@ Information about project "Runner": setUp(() { fs = MemoryFileSystem.test(); - localIosArtifacts = Artifacts.test(localEngine: 'out/ios_profile_arm64'); + localIosArtifacts = Artifacts.testLocalEngine(localEngine: 'out/ios_profile_arm64', localEngineHost: 'out/host_release'); macOS = FakePlatform(operatingSystem: 'macos'); fs.file(xcodebuild).createSync(recursive: true); }); @@ -938,7 +938,7 @@ Build settings for action build and target plugin2: } testUsingOsxContext('exits when armv7 local engine is set', () async { - localIosArtifacts = Artifacts.test(localEngine: 'out/ios_profile_arm'); + localIosArtifacts = Artifacts.testLocalEngine(localEngine: 'out/ios_profile_arm', localEngineHost: 'out/host_release'); const BuildInfo buildInfo = BuildInfo.debug; final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project')); await expectLater(() => @@ -971,7 +971,7 @@ Build settings for action build and target plugin2: final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync(); expect(buildPhaseScriptContents.contains('export "ARCHS=arm64"'), isTrue); }, overrides: <Type, Generator>{ - Artifacts: () => Artifacts.test(localEngine: 'out/host_profile_arm64'), + Artifacts: () => Artifacts.testLocalEngine(localEngine: 'out/host_profile_arm64', localEngineHost: 'out/host_release'), Platform: () => macOS, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -998,7 +998,7 @@ Build settings for action build and target plugin2: final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync(); expect(buildPhaseScriptContents.contains('export "ARCHS=x86_64"'), isTrue); }, overrides: <Type, Generator>{ - Artifacts: () => Artifacts.test(localEngine: 'out/host_profile'), + Artifacts: () => Artifacts.testLocalEngine(localEngine: 'out/host_profile', localEngineHost: 'out/host_release'), Platform: () => macOS, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1083,7 +1083,7 @@ Build settings for action build and target plugin2: final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync(); expect(buildPhaseScriptContents.contains('ARCHS=x86_64'), isTrue); }, overrides: <Type, Generator>{ - Artifacts: () => Artifacts.test(localEngine: 'out/ios_debug_sim_unopt'), + Artifacts: () => Artifacts.testLocalEngine(localEngine: 'out/ios_debug_sim_unopt', localEngineHost: 'out/host_debug_unopt'), Platform: () => macOS, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1109,7 +1109,7 @@ Build settings for action build and target plugin2: final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync(); expect(buildPhaseScriptContents.contains('ARCHS=arm64'), isTrue); }, overrides: <Type, Generator>{ - Artifacts: () => Artifacts.test(localEngine: 'out/ios_debug_sim_arm64'), + Artifacts: () => Artifacts.testLocalEngine(localEngine: 'out/ios_debug_sim_arm64', localEngineHost: 'out/host_debug_unopt'), Platform: () => macOS, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), diff --git a/packages/flutter_tools/test/general.shard/linux/native_assets_test.dart b/packages/flutter_tools/test/general.shard/linux/native_assets_test.dart new file mode 100644 index 0000000000000..19b5ae7016fd0 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/linux/native_assets_test.dart @@ -0,0 +1,410 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/dart/package_map.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/linux/native_assets.dart'; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: <String, String>{}, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + environment.buildDir.createSync(recursive: true); + projectUri = environment.projectDir.uri; + }); + + testUsingContext('dry run with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + expect( + await dryRunNativeAssetsLinux( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ), + null, + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('build with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsLinux( + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('does not throw if clang not present but no native assets present', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.create(recursive: true); + await buildNativeAssetsLinux( + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: _BuildRunnerWithoutClang(), + ); + expect( + (globals.logger as BufferLogger).traceText, + isNot(contains('Building native assets for ')), + ); + }); + + testUsingContext('dry run for multiple OSes with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: <TargetPlatform>[ + TargetPlatform.darwin, + TargetPlatform.ios, + ], + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsLinux( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('dry run with assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final Uri? nativeAssetsYaml = await dryRunNativeAssetsLinux( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.linuxX64, + path: AssetAbsolutePath(Uri.file('libbar.so')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.linuxArm64, + path: AssetAbsolutePath(Uri.file('libbar.so')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Dry running native assets for linux.', + 'Dry running native assets for linux done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/linux/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + contains('package:bar/bar.dart'), + ); + }); + + testUsingContext('build with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => buildNativeAssetsLinux( + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('build no assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsLinux( + targetPlatform: TargetPlatform.linux_x64, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/linux/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + isNot(contains('package:bar/bar.dart')), + ); + expect( + environment.projectDir + .childDirectory('build') + .childDirectory('native_assets') + .childDirectory('linux'), + exists, + ); + }); + + for (final bool flutterTester in <bool>[false, true]) { + String testName = ''; + if (flutterTester) { + testName += ' flutter tester'; + } + testUsingContext('build with assets$testName', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childDirectory('.dart_tool').childFile('package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final File dylibAfterCompiling = fileSystem.file('libbar.so'); + // The mock doesn't create the file, so create it here. + await dylibAfterCompiling.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsLinux( + targetPlatform: TargetPlatform.linux_x64, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + flutterTester: flutterTester, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.linuxX64, + path: AssetAbsolutePath(dylibAfterCompiling.uri), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Building native assets for linux_x64 debug.', + 'Building native assets for linux_x64 done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/linux/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + stringContainsInOrder(<String>[ + 'package:bar/bar.dart', + if (flutterTester) + // Tests run on host system, so the have the full path on the system. + '- ${projectUri.resolve('build/native_assets/linux/libbar.so').toFilePath()}' + else + // Apps are a bundle with the dylibs on their dlopen path. + '- libbar.so', + ]), + ); + }); + } + + testUsingContext('static libs not supported', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsLinux( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + ], + ), + ), + ), + throwsToolExit( + message: 'Native asset(s) package:bar/bar.dart have their link mode set to ' + 'static, but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ), + ); + }); + + // This logic is mocked in the other tests to avoid having test order + // randomization causing issues with what processes are invoked. + // Exercise the parsing of the process output in this separate test. + testUsingContext('NativeAssetsBuildRunnerImpl.cCompilerConfig', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + <FakeCommand>[ + const FakeCommand( + command: <Pattern>['which', 'clang++'], + stdout: ''' +/some/path/to/clang++ +''', // Newline at the end of the string. + ) + ], + ), + FileSystem: () => fileSystem, + }, () async { + if (!const LocalPlatform().isLinux) { + return; + } + + await fileSystem.directory('/some/path/to/').create(recursive: true); + await fileSystem.file('/some/path/to/clang++').create(); + await fileSystem.file('/some/path/to/clang').create(); + await fileSystem.file('/some/path/to/llvm-ar').create(); + await fileSystem.file('/some/path/to/ld.lld').create(); + + final File packagesFile = fileSystem + .directory(projectUri) + .childDirectory('.dart_tool') + .childFile('package_config.json'); + await packagesFile.parent.create(); + await packagesFile.create(); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + packagesFile, + logger: environment.logger, + ); + final NativeAssetsBuildRunner runner = + NativeAssetsBuildRunnerImpl(projectUri, packageConfig, fileSystem, logger); + final CCompilerConfig result = await runner.cCompilerConfig; + expect(result.cc, Uri.file('/some/path/to/clang')); + }); +} + +class _BuildRunnerWithoutClang extends FakeNativeAssetsBuildRunner { + @override + Future<CCompilerConfig> get cCompilerConfig async => throwToolExit('Failed to find clang++ on the PATH.'); +} diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart index 1a5c24068366a..22216ba9f8adc 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart @@ -41,6 +41,7 @@ void main() { expect(message.type, ValidationMessageType.hint); expect(message.message, contains('CocoaPods $currentVersion out of date')); expect(message.message, contains('(1.11.0 is recommended)')); + expect(message.message, contains('getting-started.html#updating-cocoapods')); }); }); } diff --git a/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart new file mode 100644 index 0000000000000..877521bc1077b --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart @@ -0,0 +1,414 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/dart/package_map.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/macos/native_assets.dart'; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: <String, String>{}, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + environment.buildDir.createSync(recursive: true); + projectUri = environment.projectDir.uri; + }); + + testUsingContext('dry run with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + expect( + await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ), + null, + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('build with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsMacOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run for multiple OSes with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: <TargetPlatform>[ + TargetPlatform.darwin, + TargetPlatform.ios, + ], + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('dry run with assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final Uri? nativeAssetsYaml = await dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Dry running native assets for macos.', + 'Dry running native assets for macos done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + contains('package:bar/bar.dart'), + ); + }); + + testUsingContext('build with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => buildNativeAssetsMacOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('build no assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + isNot(contains('package:bar/bar.dart')), + ); + }); + + for (final bool flutterTester in <bool>[false, true]) { + String testName = ''; + if (flutterTester) { + testName += ' flutter tester'; + } + testUsingContext('build with assets$testName', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + <FakeCommand>[ + const FakeCommand( + command: <Pattern>[ + 'lipo', + '-create', + '-output', + '/build/native_assets/macos/bar.dylib', + 'bar.dylib', + ], + ), + const FakeCommand( + command: <Pattern>[ + 'install_name_tool', + '-id', + '@executable_path/Frameworks/bar.dylib', + '/build/native_assets/macos/bar.dylib', + ], + ), + const FakeCommand( + command: <Pattern>[ + 'codesign', + '--force', + '--sign', + '-', + '--timestamp=none', + '/build/native_assets/macos/bar.dylib', + ], + ), + ], + ), + }, () async { + if (const LocalPlatform().isWindows) { + return; // Backslashes in commands, but we will never run these commands on Windows. + } + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsMacOS( + darwinArchs: <DarwinArch>[DarwinArch.arm64], + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + flutterTester: flutterTester, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Building native assets for [macos_arm64] debug.', + 'Building native assets for [macos_arm64] done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/macos/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + stringContainsInOrder(<String>[ + 'package:bar/bar.dart', + if (flutterTester) + // Tests run on host system, so the have the full path on the system. + '- ${projectUri.resolve('build/native_assets/macos/bar.dylib').toFilePath()}' + else + // Apps are a bundle with the dylibs on their dlopen path. + '- bar.dylib', + ]), + ); + }); + } + + testUsingContext('static libs not supported', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsMacOS( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.macOSX64, + path: AssetAbsolutePath(Uri.file('bar.a')), + ), + ], + ), + ), + ), + throwsToolExit( + message: 'Native asset(s) package:bar/bar.dart have their link mode set to ' + 'static, but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ), + ); + }); + + // This logic is mocked in the other tests to avoid having test order + // randomization causing issues with what processes are invoked. + // Exercise the parsing of the process output in this separate test. + testUsingContext('NativeAssetsBuildRunnerImpl.cCompilerConfig', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + <FakeCommand>[ + const FakeCommand( + command: <Pattern>['xcrun', 'clang', '--version'], + stdout: ''' +Apple clang version 14.0.0 (clang-1400.0.29.202) +Target: arm64-apple-darwin22.6.0 +Thread model: posix +InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin''', + ) + ], + ), + }, () async { + if (!const LocalPlatform().isMacOS) { + return; + } + + final File packagesFile = fileSystem + .directory(projectUri) + .childDirectory('.dart_tool') + .childFile('package_config.json'); + await packagesFile.parent.create(); + await packagesFile.create(); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + packagesFile, + logger: environment.logger, + ); + final NativeAssetsBuildRunner runner = NativeAssetsBuildRunnerImpl( + projectUri, + packageConfig, + fileSystem, + logger, + ); + final CCompilerConfig result = await runner.cCompilerConfig; + expect( + result.cc, + Uri.file( + '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang', + ), + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/message_parser_test.dart b/packages/flutter_tools/test/general.shard/message_parser_test.dart index efc9680adf249..3b716cd3c05ae 100644 --- a/packages/flutter_tools/test/general.shard/message_parser_test.dart +++ b/packages/flutter_tools/test/general.shard/message_parser_test.dart @@ -293,6 +293,54 @@ void main() { ))); }); + testWithoutContext('relaxed lexer', () { + final List<Node> tokens1 = Parser( + 'string', + 'app_en.arb', + '{ }', + placeholders: <String>[], + ).lexIntoTokens(); + expect(tokens1, equals(<Node>[ + Node(ST.string, 0, value: '{'), + Node(ST.string, 1, value: ' '), + Node(ST.string, 2, value: '}') + ])); + + final List<Node> tokens2 = Parser( + 'string', + 'app_en.arb', + '{ notAPlaceholder }', + placeholders: <String>['isAPlaceholder'], + ).lexIntoTokens(); + expect(tokens2, equals(<Node>[ + Node(ST.string, 0, value: '{'), + Node(ST.string, 1, value: ' notAPlaceholder '), + Node(ST.string, 18, value: '}') + ])); + + final List<Node> tokens3 = Parser( + 'string', + 'app_en.arb', + '{ isAPlaceholder }', + placeholders: <String>['isAPlaceholder'], + ).lexIntoTokens(); + expect(tokens3, equals(<Node>[ + Node(ST.openBrace, 0, value: '{'), + Node(ST.identifier, 2, value: 'isAPlaceholder'), + Node(ST.closeBrace, 17, value: '}') + ])); + }); + + testWithoutContext('relaxed lexer complex', () { + const String message = '{ notPlaceholder } {count,plural, =0{Hello} =1{Hello World} =2{Hello two worlds} few{Hello {count} worlds} many{Hello all {count} worlds} other{Hello other {count} worlds}}'; + final List<Node> tokens = Parser( + 'string', + 'app_en.arb', + message, + placeholders: <String>['count'], + ).lexIntoTokens(); + expect(tokens[0].type, equals(ST.string)); + }); testWithoutContext('parser basic', () { expect(Parser('helloWorld', 'app_en.arb', 'Hello {name}').parse(), equals( diff --git a/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart index 36ee8d7824a3a..6ebbfa9bc2705 100644 --- a/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cmake_project.dart'; import 'package:flutter_tools/src/migrations/cmake_custom_command_migration.dart'; +import 'package:flutter_tools/src/migrations/cmake_native_assets_migration.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; @@ -155,6 +156,134 @@ add_custom_command( expect(testLogger.statusText, contains('add_custom_command() missing VERBATIM or FLUTTER_TARGET_PLATFORM, updating.')); }); }); + + group('migrate add install() NATIVE_ASSETS_DIR command', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeCmakeProject mockCmakeProject; + late File managedCmakeFile; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + managedCmakeFile = memoryFileSystem.file('CMakeLists.txtx'); + + testLogger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences.test(), + ); + + mockCmakeProject = FakeCmakeProject(managedCmakeFile); + }); + + testWithoutContext('skipped if files are missing', () { + final CmakeNativeAssetsMigration cmakeProjectMigration = CmakeNativeAssetsMigration( + mockCmakeProject, + 'linux', + testLogger, + ); + cmakeProjectMigration.migrate(); + expect(managedCmakeFile.existsSync(), isFalse); + + expect(testLogger.traceText, contains('CMake project not found, skipping install() NATIVE_ASSETS_DIR migration.')); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if nothing to migrate', () { + const String contents = 'Nothing to migrate'; + managedCmakeFile.writeAsStringSync(contents); + final DateTime projectLastModified = managedCmakeFile.lastModifiedSync(); + + final CmakeNativeAssetsMigration cmakeProjectMigration = CmakeNativeAssetsMigration( + mockCmakeProject, + 'linux', + testLogger, + ); + cmakeProjectMigration.migrate(); + + expect(managedCmakeFile.lastModifiedSync(), projectLastModified); + expect(managedCmakeFile.readAsStringSync(), contents); + + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if already migrated', () { + const String contents = r''' +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +'''; + managedCmakeFile.writeAsStringSync(contents); + final DateTime projectLastModified = managedCmakeFile.lastModifiedSync(); + + final CmakeNativeAssetsMigration cmakeProjectMigration = CmakeNativeAssetsMigration( + mockCmakeProject, + 'linux', + testLogger, + ); + cmakeProjectMigration.migrate(); + + expect(managedCmakeFile.lastModifiedSync(), projectLastModified); + expect(managedCmakeFile.readAsStringSync(), contents); + + expect(testLogger.statusText, isEmpty); + }); + + for (final String os in <String>['linux', 'windows']) { + testWithoutContext('is migrated to copy native assets', () { + managedCmakeFile.writeAsStringSync(r''' +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) +'''); + + final CmakeNativeAssetsMigration cmakeProjectMigration = CmakeNativeAssetsMigration( + mockCmakeProject, + os, + testLogger, + ); + cmakeProjectMigration.migrate(); + + expect(managedCmakeFile.readAsStringSync(), ''' +foreach(bundled_library \${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "\${bundled_library}" + DESTINATION "\${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "\${PROJECT_BUILD_DIR}native_assets/$os/") +install(DIRECTORY "\${NATIVE_ASSETS_DIR}" + DESTINATION "\${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \\"\${INSTALL_BUNDLE_DATA_DIR}/\${FLUTTER_ASSET_DIR_NAME}\\") + " COMPONENT Runtime) +install(DIRECTORY "\${PROJECT_BUILD_DIR}/\${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "\${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) +'''); + + expect(testLogger.statusText, + contains('CMake missing install() NATIVE_ASSETS_DIR command, updating.')); + }); + } + }); }); } diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 811b3fd84f2ce..8efcbefca3456 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -1690,6 +1690,24 @@ flutter: ); }); + testWithoutContext('Symlink failures instruct developers to have their project on the same drive as their SDK', () async { + final Platform platform = FakePlatform(operatingSystem: 'windows'); + final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972]'); + + const FileSystemException e = FileSystemException('', '', OSError('', 1)); + + expect( + () => handleSymlinkException( + e, + platform: platform, + os: os, + source: pubCachePath, + destination: ephemeralPackagePath, + ), + throwsToolExit(message: 'Try moving your Flutter project to the same drive as your Flutter SDK'), + ); + }); + testWithoutContext('Symlink failures only give instructions for specific errors', () async { final Platform platform = FakePlatform(operatingSystem: 'windows'); final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14393]'); diff --git a/packages/flutter_tools/test/general.shard/preview_device_test.dart b/packages/flutter_tools/test/general.shard/preview_device_test.dart index fa94d8600f366..876dbc64950ad 100644 --- a/packages/flutter_tools/test/general.shard/preview_device_test.dart +++ b/packages/flutter_tools/test/general.shard/preview_device_test.dart @@ -99,6 +99,7 @@ class FakeBundleBuilder extends Fake implements BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, + Uri? nativeAssets, @visibleForTesting BuildSystem? buildSystem }) async { final Directory assetDirectory = fileSystem diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 85918adcc4d1b..2cd3ca26f8bf6 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -733,7 +733,6 @@ apply plugin: 'kotlin-android' const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{ @@ -751,13 +750,15 @@ apply plugin: 'kotlin-android' 'applinks:example2.com', ], ); - final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings( + final String outputFilePath = await project.ios.outputsUniversalLinkSettings( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); + final File outputFile = fs.file(outputFilePath); + final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>; + expect( - settings.associatedDomains, + json['associatedDomains'], unorderedEquals( <String>[ 'example.com', @@ -765,8 +766,8 @@ apply plugin: 'kotlin-android' ], ), ); - expect(settings.teamIdentifier, 'ABC'); - expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix'); + expect(json['teamIdentifier'], 'ABC'); + expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix'); }); testWithMocks('can handle entitlement file in nested directory structure.', () async { @@ -778,7 +779,6 @@ apply plugin: 'kotlin-android' const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{ @@ -796,13 +796,15 @@ apply plugin: 'kotlin-android' 'applinks:example2.com', ], ); - final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings( + + final String outputFilePath = await project.ios.outputsUniversalLinkSettings( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); + final File outputFile = fs.file(outputFilePath); + final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>; expect( - settings.associatedDomains, + json['associatedDomains'], unorderedEquals( <String>[ 'example.com', @@ -810,8 +812,8 @@ apply plugin: 'kotlin-android' ], ), ); - expect(settings.teamIdentifier, 'ABC'); - expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix'); + expect(json['teamIdentifier'], 'ABC'); + expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix'); }); testWithMocks('return empty when no entitlement', () async { @@ -821,7 +823,6 @@ apply plugin: 'kotlin-android' const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{ @@ -830,13 +831,15 @@ apply plugin: 'kotlin-android' }; xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger); testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER)'); - final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings( + final String outputFilePath = await project.ios.outputsUniversalLinkSettings( target: 'Runner', - scheme: 'Debug', configuration: 'config', ); - expect(settings.teamIdentifier, 'ABC'); - expect(settings.bundleIdentifier, 'io.flutter.someProject'); + final File outputFile = fs.file(outputFilePath); + final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>; + expect(json['teamIdentifier'], 'ABC'); + expect(json['bundleIdentifier'], 'io.flutter.someProject'); + expect(json['associatedDomains'], unorderedEquals(<String>[])); }); }); @@ -1617,7 +1620,7 @@ String gradleFileWithApplicationId(String id) { return ''' apply plugin: 'com.android.application' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { applicationId '$id' @@ -1634,7 +1637,7 @@ version '1.0-SNAPSHOT' apply plugin: 'com.android.library' android { - compileSdkVersion 31 + compileSdkVersion 33 } '''; } diff --git a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart index 8522d41e77c29..d6f982e35ae3d 100644 --- a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter_tools/src/base/dds.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/utils.dart'; import 'package:flutter_tools/src/daemon.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/proxied_devices/devices.dart'; @@ -363,6 +364,39 @@ void main() { expect(fakeFilter.devices![0].id, fakeDevice['id']); expect(fakeFilter.devices![1].id, fakeDevice2['id']); }); + + testWithoutContext('publishes the devices on deviceNotifier after startPolling', () async { + bufferLogger = BufferLogger.test(); + final ProxiedDevices proxiedDevices = ProxiedDevices( + clientDaemonConnection, + logger: bufferLogger, + ); + + proxiedDevices.startPolling(); + + final ItemListNotifier<Device>? deviceNotifier = proxiedDevices.deviceNotifier; + expect(deviceNotifier, isNotNull); + + final List<Device> devicesAdded = <Device>[]; + deviceNotifier!.onAdded.listen((Device device) { + devicesAdded.add(device); + }); + + final DaemonMessage message = await serverDaemonConnection.incomingCommands.first; + expect(message.data['id'], isNotNull); + expect(message.data['method'], 'device.discoverDevices'); + + serverDaemonConnection.sendResponse(message.data['id']!, <Map<String, Object?>>[ + fakeDevice, + fakeDevice2, + ]); + + await pumpEventQueue(); + + expect(devicesAdded.length, 2); + expect(devicesAdded[0].id, fakeDevice['id']); + expect(devicesAdded[1].id, fakeDevice2['id']); + }); }); group('ProxiedDartDevelopmentService', () { diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 35970ba5c196c..2af0b715306db 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/scene_importer.dart'; import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; @@ -35,6 +36,9 @@ import 'package:flutter_tools/src/run_cold.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' + hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -44,6 +48,7 @@ import '../src/context.dart'; import '../src/fake_vm_services.dart'; import '../src/fakes.dart'; import '../src/testbed.dart'; +import 'fake_native_assets_build_runner.dart'; final vm_service.Event fakeUnpausedEvent = vm_service.Event( kind: vm_service.EventKind.kResume, @@ -424,7 +429,6 @@ void main() { hotEventSdkName: 'Android', hotEventEmulator: false, hotEventFullRestart: false, - fastReassemble: false, )), )); expect(fakeVmServiceHost?.hasRemainingExpectations, false); @@ -480,7 +484,6 @@ void main() { hotEventSdkName: 'Android', hotEventEmulator: false, hotEventFullRestart: false, - fastReassemble: false, )), )); expect(fakeVmServiceHost?.hasRemainingExpectations, false); @@ -527,7 +530,6 @@ void main() { hotEventSdkName: 'Android', hotEventEmulator: false, hotEventFullRestart: false, - fastReassemble: false, )), )); expect(fakeVmServiceHost?.hasRemainingExpectations, false); @@ -766,96 +768,6 @@ void main() { Usage: () => TestUsage(), })); - testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async { - fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ - listViews, - FakeVmServiceRequest( - method: 'getVM', - jsonResponse: fakeVM.toJson(), - ), - listViews, - listViews, - FakeVmServiceRequest( - method: 'getVM', - jsonResponse: fakeVM.toJson(), - ), - const FakeVmServiceRequest( - method: kReloadSourcesServiceName, - args: <String, Object>{ - 'isolateId': '1', - 'pause': false, - 'rootLibUri': 'main.dart.incremental.dill', - }, - jsonResponse: <String, Object>{ - 'type': 'ReloadReport', - 'success': true, - 'details': <String, Object>{ - 'loadedLibraryCount': 1, - }, - }, - ), - FakeVmServiceRequest( - method: 'getIsolatePauseEvent', - args: <String, Object>{ - 'isolateId': '1', - }, - jsonResponse: fakeUnpausedEvent.toJson(), - ), - FakeVmServiceRequest( - method: 'ext.flutter.fastReassemble', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'className': 'FOO', - }, - ), - ]); - final FakeDelegateFlutterDevice flutterDevice = FakeDelegateFlutterDevice( - device, - BuildInfo.debug, - FakeResidentCompiler(), - devFS, - )..vmService = fakeVmServiceHost!.vmService; - residentRunner = HotRunner( - <FlutterDevice>[ - flutterDevice, - ], - stayResident: false, - debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), - target: 'main.dart', - devtoolsHandler: createNoOpHandler, - ); - devFS.nextUpdateReport = UpdateFSReport( - success: true, - fastReassembleClassName: 'FOO', - invalidatedSourcesCount: 1, - ); - - final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync(); - final Completer<void> futureAppStart = Completer<void>.sync(); - unawaited(residentRunner.attach( - appStartedCompleter: futureAppStart, - connectionInfoCompleter: futureConnectionInfo, - enableDevTools: true, - )); - - await futureAppStart.future; - final OperationResult result = await residentRunner.restart(); - - expect(result.fatal, false); - expect(result.code, 0); - - final TestUsageEvent event = (globals.flutterUsage as TestUsage).events.first; - expect(event.category, 'hot'); - expect(event.parameter, 'reload'); - expect(event.parameters?.fastReassemble, true); - }, overrides: <Type, Generator>{ - FileSystem: () => MemoryFileSystem.test(), - Platform: () => FakePlatform(), - ProjectFileInvalidator: () => FakeProjectFileInvalidator(), - Usage: () => TestUsage(), - FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true), - })); - testUsingContext('ResidentRunner reports hot reload time details', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ listViews, @@ -893,10 +805,9 @@ void main() { jsonResponse: fakeUnpausedEvent.toJson(), ), FakeVmServiceRequest( - method: 'ext.flutter.fastReassemble', + method: 'ext.flutter.reassemble', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, - 'className': 'FOO', }, ), ]); @@ -917,7 +828,6 @@ void main() { ); devFS.nextUpdateReport = UpdateFSReport( success: true, - fastReassembleClassName: 'FOO', invalidatedSourcesCount: 1, ); @@ -942,7 +852,6 @@ void main() { Platform: () => FakePlatform(), ProjectFileInvalidator: () => FakeProjectFileInvalidator(), Usage: () => TestUsage(), - FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true), })); testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async { @@ -1225,7 +1134,6 @@ void main() { hotEventSdkName: 'Android', hotEventEmulator: false, hotEventFullRestart: true, - fastReassemble: false, )), )); expect(fakeVmServiceHost?.hasRemainingExpectations, false); @@ -1984,10 +1892,11 @@ flutter: ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('FlutterDevice passes flutter-widget-cache flag when feature is enabled', () async { + testUsingContext('FlutterDevice passes alternative-invalidation-strategy flag', () async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]); final FakeDevice device = FakeDevice(); + final DefaultResidentCompiler? residentCompiler = (await FlutterDevice.create( device, buildInfo: const BuildInfo( @@ -2000,19 +1909,17 @@ flutter: )).generator as DefaultResidentCompiler?; expect(residentCompiler!.extraFrontEndOptions, - contains('--flutter-widget-cache')); + contains('--enable-experiment=alternative-invalidation-strategy')); }, overrides: <Type, Generator>{ Artifacts: () => Artifacts.test(), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), - FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true), }); - testUsingContext('FlutterDevice passes alternative-invalidation-strategy flag', () async { + testUsingContext('FlutterDevice passes initializeFromDill parameter if specified', () async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]); final FakeDevice device = FakeDevice(); - final DefaultResidentCompiler? residentCompiler = (await FlutterDevice.create( device, buildInfo: const BuildInfo( @@ -2020,19 +1927,20 @@ flutter: '', treeShakeIcons: false, extraFrontEndOptions: <String>[], + initializeFromDill: '/foo/bar.dill', ), target: null, platform: FakePlatform(), )).generator as DefaultResidentCompiler?; - expect(residentCompiler!.extraFrontEndOptions, - contains('--enable-experiment=alternative-invalidation-strategy')); + expect(residentCompiler!.initializeFromDill, '/foo/bar.dill'); + expect(residentCompiler.assumeInitializeFromDillUpToDate, false); }, overrides: <Type, Generator>{ Artifacts: () => Artifacts.test(), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('FlutterDevice passes initializeFromDill parameter if specified', () async { + testUsingContext('FlutterDevice passes assumeInitializeFromDillUpToDate parameter if specified', () async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]); final FakeDevice device = FakeDevice(); @@ -2043,20 +1951,19 @@ flutter: '', treeShakeIcons: false, extraFrontEndOptions: <String>[], - initializeFromDill: '/foo/bar.dill', + assumeInitializeFromDillUpToDate: true, ), target: null, platform: FakePlatform(), )).generator as DefaultResidentCompiler?; - expect(residentCompiler!.initializeFromDill, '/foo/bar.dill'); - expect(residentCompiler.assumeInitializeFromDillUpToDate, false); + expect(residentCompiler!.assumeInitializeFromDillUpToDate, true); }, overrides: <Type, Generator>{ Artifacts: () => Artifacts.test(), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('FlutterDevice passes assumeInitializeFromDillUpToDate parameter if specified', () async { + testUsingContext('FlutterDevice passes frontendServerStarterPath parameter if specified', () async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]); final FakeDevice device = FakeDevice(); @@ -2066,13 +1973,12 @@ flutter: BuildMode.debug, '', treeShakeIcons: false, - extraFrontEndOptions: <String>[], - assumeInitializeFromDillUpToDate: true, + frontendServerStarterPath: '/foo/bar/frontend_server_starter.dart', ), target: null, platform: FakePlatform(), )).generator as DefaultResidentCompiler?; - expect(residentCompiler!.assumeInitializeFromDillUpToDate, true); + expect(residentCompiler!.frontendServerStarterPath, '/foo/bar/frontend_server_starter.dart'); }, overrides: <Type, Generator>{ Artifacts: () => Artifacts.test(), FileSystem: () => MemoryFileSystem.test(), @@ -2332,9 +2238,11 @@ flutter: testUsingContext('nextPlatform moves through expected platforms', () { expect(nextPlatform('android'), 'iOS'); - expect(nextPlatform('iOS'), 'fuchsia'); - expect(nextPlatform('fuchsia'), 'macOS'); - expect(nextPlatform('macOS'), 'android'); + expect(nextPlatform('iOS'), 'windows'); + expect(nextPlatform('windows'), 'macOS'); + expect(nextPlatform('macOS'), 'linux'); + expect(nextPlatform('linux'), 'fuchsia'); + expect(nextPlatform('fuchsia'), 'android'); expect(() => nextPlatform('unknown'), throwsAssertionError); }); @@ -2442,6 +2350,82 @@ flutter: expect(flutterDevice.devFS!.hasSetAssetDirectory, true); expect(fakeVmServiceHost!.hasRemainingExpectations, false); })); + + testUsingContext( + 'native assets', + () => testbed.run(() async { + final FileSystem fileSystem = globals.fs; + final Environment environment = Environment.test( + fileSystem.currentDirectory, + inputs: <String, String>{}, + artifacts: Artifacts.test(), + processManager: FakeProcessManager.empty(), + fileSystem: fileSystem, + logger: BufferLogger.test(), + ); + final Uri projectUri = environment.projectDir.uri; + + final FakeDevice device = FakeDevice( + targetPlatform: TargetPlatform.darwin, + sdkNameAndVersion: 'Macos', + ); + final FakeFlutterDevice flutterDevice = FakeFlutterDevice() + ..testUri = testUri + ..vmServiceHost = (() => fakeVmServiceHost) + ..device = device + .._devFS = devFS + ..targetPlatform = TargetPlatform.darwin; + + fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ + listViews, + listViews, + ]); + globals.fs + .file(globals.fs.path.join('lib', 'main.dart')) + .createSync(recursive: true); + final FakeNativeAssetsBuildRunner buildRunner = FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.macOSArm64, + path: AssetAbsolutePath(Uri.file('bar.dylib')), + ), + ], + ), + ); + residentRunner = HotRunner( + <FlutterDevice>[ + flutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(const BuildInfo( + BuildMode.debug, + '', + treeShakeIcons: false, + trackWidgetCreation: true, + )), + target: 'main.dart', + devtoolsHandler: createNoOpHandler, + buildRunner: buildRunner, + ); + + final int? result = await residentRunner.run(); + expect(result, 0); + + expect(buildRunner.buildInvocations, 0); + expect(buildRunner.dryRunInvocations, 1); + expect(buildRunner.hasPackageConfigInvocations, 1); + expect(buildRunner.packagesWithNativeAssetsInvocations, 1); + }), + overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true, isMacOSEnabled: true), + }); } // This implements [dds.DartDevelopmentService], not the [DartDevelopmentService] @@ -2506,7 +2490,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice { DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler(); @override - TargetPlatform get targetPlatform => TargetPlatform.android; + TargetPlatform targetPlatform = TargetPlatform.android; @override Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri); @@ -2641,6 +2625,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { didSuppressErrors = suppressErrors; return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]); diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index ff803f7bbc7f3..f7b63a0f9364d 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -39,6 +39,7 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../src/common.dart'; import '../src/context.dart'; +import '../src/fake_process_manager.dart'; import '../src/fake_vm_services.dart'; const List<VmServiceExpectation> kAttachLogExpectations = @@ -74,22 +75,6 @@ const List<VmServiceExpectation> kAttachIsolateExpectations = 'service': kFlutterMemoryInfoServiceName, 'alias': kFlutterToolAlias, }), - FakeVmServiceRequest(method: 'registerService', args: <String, Object>{ - 'service': kFlutterGetIOSBuildOptionsServiceName, - 'alias': kFlutterToolAlias, - }), - FakeVmServiceRequest(method: 'registerService', args: <String, Object>{ - 'service': kFlutterGetAndroidBuildVariantsServiceName, - 'alias': kFlutterToolAlias, - }), - FakeVmServiceRequest(method: 'registerService', args: <String, Object>{ - 'service': kFlutterGetIOSUniversalLinkSettingsServiceName, - 'alias': kFlutterToolAlias, - }), - FakeVmServiceRequest(method: 'registerService', args: <String, Object>{ - 'service': kFlutterGetAndroidAppLinkSettingsName, - 'alias': kFlutterToolAlias, - }), FakeVmServiceRequest( method: 'streamListen', args: <String, Object>{ @@ -636,8 +621,9 @@ void main() { ]); setupMocks(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); + final FakeProcess process = FakeProcess(); final Chromium chrome = - Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); + Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( @@ -703,8 +689,9 @@ void main() { ]); setupMocks(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); + final FakeProcess process = FakeProcess(); final Chromium chrome = - Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); + Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( @@ -1041,8 +1028,9 @@ void main() { setupMocks(); final FakeChromeConnection chromeConnection = FakeChromeConnection(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); + final FakeProcess process = FakeProcess(); final Chromium chrome = - Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); + Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( @@ -1460,6 +1448,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { return const CompilerOutput('foo.dill', 0, <Uri>[]); } diff --git a/packages/flutter_tools/test/general.shard/run_hot_test.dart b/packages/flutter_tools/test/general.shard/run_hot_test.dart new file mode 100644 index 0000000000000..df83239d8003b --- /dev/null +++ b/packages/flutter_tools/test/general.shard/run_hot_test.dart @@ -0,0 +1,65 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/devfs.dart'; +import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/run_hot.dart'; +import 'package:flutter_tools/src/vmservice.dart'; +import 'package:test/fake.dart'; +import 'package:vm_service/vm_service.dart' as vm_service; + +//import '../src/context.dart'; +import '../src/common.dart'; + +void main() { + testWithoutContext('defaultReloadSourcesHelper() handles empty DeviceReloadReports)', () { + defaultReloadSourcesHelper( + _FakeHotRunner(), + <FlutterDevice?>[_FakeFlutterDevice()], + false, + const <String, dynamic>{}, + 'android', + 'flutter-sdk', + false, + 'test-reason', + TestUsage(), + ); + }); +} + +class _FakeHotRunner extends Fake implements HotRunner {} + +class _FakeDevFS extends Fake implements DevFS { + @override + final Uri? baseUri = Uri(); + + @override + void resetLastCompiled() {} +} + +class _FakeFlutterDevice extends Fake implements FlutterDevice { + @override + final DevFS? devFS = _FakeDevFS(); + + @override + final FlutterVmService? vmService = _FakeFlutterVmService(); +} + +class _FakeFlutterVmService extends Fake implements FlutterVmService { + @override + final vm_service.VmService service = _FakeVmService(); +} + +class _FakeVmService extends Fake implements vm_service.VmService { + @override + Future<_FakeVm> getVM() async => _FakeVm(); +} + +class _FakeVm extends Fake implements vm_service.VM { + final List<vm_service.IsolateRef> _isolates = <vm_service.IsolateRef>[]; + + @override + List<vm_service.IsolateRef>? get isolates => _isolates; +} diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart index 810c8ef3379a6..9f6d9c5e2c17e 100644 --- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart @@ -11,11 +11,14 @@ import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/error_handling_io.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; @@ -700,6 +703,67 @@ void main() { expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDevice)); }); }); + + group('--flavor', () { + late _TestDeviceManager testDeviceManager; + late Logger logger; + late FileSystem fileSystem; + + setUp(() { + logger = BufferLogger.test(); + testDeviceManager = _TestDeviceManager(logger: logger); + fileSystem = MemoryFileSystem.test(); + }); + + testUsingContext("tool exits when FLUTTER_APP_FLAVOR is already set in user's environment", () async { + fileSystem.file('lib/main.dart').createSync(recursive: true); + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('.packages').createSync(); + + final FakeDevice device = FakeDevice('name', 'id'); + testDeviceManager.devices = <Device>[device]; + final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + expect(runner.run(<String>['run', '--no-pub', '--no-hot', '--flavor=strawberry']), + throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.')); + + }, overrides: <Type, Generator>{ + DeviceManager: () => testDeviceManager, + Platform: () => FakePlatform( + environment: <String, String>{ + 'FLUTTER_APP_FLAVOR': 'I was already set' + } + ), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('tool exits when FLUTTER_APP_FLAVOR is set in --dart-define or --dart-define-from-file', () async { + fileSystem.file('lib/main.dart').createSync(recursive: true); + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('.packages').createSync(); + fileSystem.file('config.json')..createSync()..writeAsStringSync('{"FLUTTER_APP_FLAVOR": "strawberry"}'); + + final FakeDevice device = FakeDevice('name', 'id'); + testDeviceManager.devices = <Device>[device]; + final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); + final CommandRunner<void> runner = createTestCommandRunner(command); + + expect(runner.run(<String>['run', '--dart-define=FLUTTER_APP_FLAVOR=strawberry', '--no-pub', '--no-hot', '--flavor=strawberry']), + throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file')); + + expect(runner.run(<String>['run', '--dart-define-from-file=config.json', '--no-pub', '--no-hot', '--flavor=strawberry']), + throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file')); + }, overrides: <Type, Generator>{ + DeviceManager: () => testDeviceManager, + Platform: () => FakePlatform(), + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + }); }); } @@ -853,3 +917,22 @@ class FakePub extends Fake implements Pub { PubOutputMode outputMode = PubOutputMode.all, }) async { } } + +class _TestDeviceManager extends DeviceManager { + _TestDeviceManager({required super.logger}); + List<Device> devices = <Device>[]; + + @override + List<DeviceDiscovery> get deviceDiscoverers { + final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); + devices.forEach(discoverer.addDevice); + return <DeviceDiscovery>[discoverer]; + } +} + +class _TestRunCommandThatOnlyValidates extends RunCommand { + @override + Future<FlutterCommandResult> runCommand() async { + return FlutterCommandResult.success(); + } +} diff --git a/packages/flutter_tools/test/general.shard/runner/local_engine_test.dart b/packages/flutter_tools/test/general.shard/runner/local_engine_test.dart index 2845922dcf44f..d84d5438ec32d 100644 --- a/packages/flutter_tools/test/general.shard/runner/local_engine_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/local_engine_test.dart @@ -43,7 +43,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: 'ios_debug'), + await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug', targetEngine: '/arbitrary/engine/src/out/ios_debug', @@ -58,7 +58,7 @@ void main() { .writeAsStringSync('sky_engine:file:///symlink/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/'); expect( - await localEngineLocator.findEnginePath(localEngine: 'ios_debug'), + await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: '/symlink/src/out/host_debug', targetEngine: '/symlink/src/out/ios_debug', @@ -84,7 +84,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(engineSourcePath: '$kArbitraryEngineRoot/src', localEngine: 'ios_debug'), + await localEngineLocator.findEnginePath(engineSourcePath: '$kArbitraryEngineRoot/src', localEngine: 'ios_debug', localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug', targetEngine: '/arbitrary/engine/src/out/ios_debug', @@ -93,6 +93,54 @@ void main() { expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src')); }); + testWithoutContext('works if --local-engine is specified and --local-engine-host is specified', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory localEngine = fileSystem + .directory('$kArbitraryEngineRoot/src/out/android_debug_unopt_arm64/') + ..createSync(recursive: true); + fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug_unopt_arm64/').createSync(recursive: true); + + final BufferLogger logger = BufferLogger.test(); + final LocalEngineLocator localEngineLocator = LocalEngineLocator( + fileSystem: fileSystem, + flutterRoot: 'flutter/flutter', + logger: logger, + userMessages: UserMessages(), + platform: FakePlatform(environment: <String, String>{}), + ); + + expect( + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug_unopt_arm64'), + matchesEngineBuildPaths( + hostEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64', + targetEngine: '/arbitrary/engine/src/out/android_debug_unopt_arm64', + ), + ); + expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src')); + }); + + testWithoutContext('fails if --local-engine-host is omitted', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory localEngine = fileSystem + .directory('$kArbitraryEngineRoot/src/out/android_debug_unopt_arm64/') + ..createSync(recursive: true); + fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug_unopt/').createSync(recursive: true); + + final BufferLogger logger = BufferLogger.test(); + final LocalEngineLocator localEngineLocator = LocalEngineLocator( + fileSystem: fileSystem, + flutterRoot: 'flutter/flutter', + logger: logger, + userMessages: UserMessages(), + platform: FakePlatform(environment: <String, String>{}), + ); + + await expectLater( + localEngineLocator.findEnginePath(localEngine: localEngine.path), + throwsToolExit(message: 'You are using a locally built engine (--local-engine) but have not specified --local-engine-host'), + ); + }); + testWithoutContext('works if --local-engine is specified and --local-engine-src-path ' 'is determined by --local-engine', () async { final FileSystem fileSystem = MemoryFileSystem.test(); @@ -111,7 +159,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: localEngine.path), + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug', targetEngine: '/arbitrary/engine/src/out/ios_debug', @@ -137,7 +185,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: localEngine.path), + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: localEngine.path), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug', targetEngine: '/arbitrary/engine/src/out/host_debug', @@ -162,7 +210,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: localEngine.path), + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: localEngine.path), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64', targetEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64', @@ -189,7 +237,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: localEngine.path), + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug', targetEngine: '/arbitrary/engine/src/out/ios_debug_sim', @@ -217,7 +265,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: localEngine.path), + await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug_unopt'), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/host_debug_unopt', targetEngine: '/arbitrary/engine/src/out/ios_debug_sim_unopt', @@ -240,7 +288,7 @@ void main() { ); await expectToolExitLater( - localEngineLocator.findEnginePath(localEngine: localEngine.path), + localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'), contains('No Flutter engine build found at /arbitrary/engine/src/out/host_debug'), ); }); @@ -269,7 +317,7 @@ void main() { ); expect( - await localEngineLocator.findEnginePath(localEngine: 'ios_debug'), + await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'), matchesEngineBuildPaths( hostEngine: 'flutter/engine/src/out/host_debug', targetEngine: 'flutter/engine/src/out/ios_debug', @@ -291,7 +339,7 @@ void main() { ); await expectToolExitLater( - localEngineLocator.findEnginePath(localEngine: '/path/to/nothing'), + localEngineLocator.findEnginePath(localEngine: '/path/to/nothing', localHostEngine: '/path/to/nothing'), contains('Unable to detect local Flutter engine src directory'), ); }); @@ -315,7 +363,7 @@ void main() { ); expect( - await localWasmEngineLocator.findEnginePath(localEngine: localWasmEngine.path), + await localWasmEngineLocator.findEnginePath(localEngine: localWasmEngine.path, localHostEngine: localWasmEngine.path), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/wasm_whatever', targetEngine: '/arbitrary/engine/src/out/wasm_whatever', @@ -333,7 +381,7 @@ void main() { ); expect( - await localWebEngineLocator.findEnginePath(localEngine: localWebEngine.path), + await localWebEngineLocator.findEnginePath(localEngine: localWebEngine.path, localHostEngine: localWebEngine.path), matchesEngineBuildPaths( hostEngine: '/arbitrary/engine/src/out/web_whatever', targetEngine: '/arbitrary/engine/src/out/web_whatever', diff --git a/packages/flutter_tools/test/general.shard/runner/runner_test.dart b/packages/flutter_tools/test/general.shard/runner/runner_test.dart index bd8f56a446556..4bf6a18a7c910 100644 --- a/packages/flutter_tools/test/general.shard/runner/runner_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/runner_test.dart @@ -326,7 +326,7 @@ void main() { expect(globals.analytics.shouldShowMessage, true); await runner.run( - <String>['--disable-telemetry'], + <String>['--disable-analytics'], () => <FlutterCommand>[], // This flutterVersion disables crash reporting. flutterVersion: '[user-branch]/', @@ -343,7 +343,7 @@ void main() { ); testUsingContext( - 'runner enabling telemetry with flag', + 'runner enabling analytics with flag', () async { io.setExitFunctionForTests((int exitCode) {}); @@ -351,7 +351,7 @@ void main() { expect(globals.analytics.shouldShowMessage, false); await runner.run( - <String>['--enable-telemetry'], + <String>['--enable-analytics'], () => <FlutterCommand>[], // This flutterVersion disables crash reporting. flutterVersion: '[user-branch]/', @@ -377,8 +377,8 @@ void main() { final int exitCode = await runner.run( <String>[ - '--disable-telemetry', - '--enable-telemetry', + '--disable-analytics', + '--enable-analytics', ], () => <FlutterCommand>[], // This flutterVersion disables crash reporting. @@ -538,7 +538,7 @@ class WaitingCrashReporter implements CrashReporter { } /// A fake [Analytics] that will be used to test -/// the --disable-telemetry flag +/// the --disable-analytics flag class FakeAnalytics extends Fake implements Analytics { FakeAnalytics({bool fakeTelemetryStatusOverride = true}) diff --git a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart index 6c89db8fa8e3c..7befa7793c947 100644 --- a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart @@ -2884,6 +2884,9 @@ class FakeTerminal extends Fake implements AnsiTerminal { @override final bool supportsColor; + @override + bool get isCliAnimationEnabled => supportsColor; + @override bool usesTerminalUi = true; diff --git a/packages/flutter_tools/test/general.shard/template_test.dart b/packages/flutter_tools/test/general.shard/template_test.dart index 4f3cc55334d66..fa4bf6823df30 100644 --- a/packages/flutter_tools/test/general.shard/template_test.dart +++ b/packages/flutter_tools/test/general.shard/template_test.dart @@ -51,8 +51,9 @@ void main() { FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }; + const TemplatePathProvider templatePathProvider = TemplatePathProvider(); - testUsingContext('templateImageDirectory returns parent template directory if passed null name', () async { + testUsingContext('templatePathProvider.imageDirectory returns parent template directory if passed null name', () async { final String packageConfigPath = globals.fs.path.join( Cache.flutterRoot!, 'packages', @@ -77,7 +78,7 @@ void main() { } '''); expect( - (await templateImageDirectory(null, globals.fs, globals.logger)).path, + (await templatePathProvider.imageDirectory(null, globals.fs, globals.logger)).path, globals.fs.path.absolute( 'flutter_template_images', 'templates', @@ -85,7 +86,7 @@ void main() { ); }, overrides: overrides); - testUsingContext('templateImageDirectory returns the directory containing the `name` template directory', () async { + testUsingContext('templatePathProvider.imageDirectory returns the directory containing the `name` template directory', () async { final String packageConfigPath = globals.fs.path.join( Cache.flutterRoot!, 'packages', @@ -109,7 +110,7 @@ void main() { } '''); expect( - (await templateImageDirectory('app_shared', globals.fs, globals.logger)).path, + (await templatePathProvider.imageDirectory('app_shared', globals.fs, globals.logger)).path, globals.fs.path.absolute( 'flutter_template_images', 'templates', diff --git a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart index e234825ede40e..33f615c85ad75 100644 --- a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart +++ b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart @@ -464,10 +464,10 @@ void main() { method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', - 'value': 'fuchsia', + 'value': 'windows', }, jsonResponse: <String, Object>{ - 'value': 'fuchsia', + 'value': 'windows', }, ), // Request 2. @@ -496,7 +496,7 @@ void main() { await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); - expect(terminalHandler.logger.statusText, contains('Switched operating system to fuchsia')); + expect(terminalHandler.logger.statusText, contains('Switched operating system to windows')); expect(terminalHandler.logger.statusText, contains('Switched operating system to iOS')); }); @@ -518,10 +518,10 @@ void main() { method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', - 'value': 'fuchsia', + 'value': 'windows', }, jsonResponse: <String, Object>{ - 'value': 'fuchsia', + 'value': 'windows', }, ), // Request 2. @@ -550,7 +550,7 @@ void main() { await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); - expect(terminalHandler.logger.statusText, contains('Switched operating system to fuchsia')); + expect(terminalHandler.logger.statusText, contains('Switched operating system to windows')); expect(terminalHandler.logger.statusText, contains('Switched operating system to iOS')); }); @@ -1015,38 +1015,14 @@ void main() { expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); }); - testWithoutContext('s, can take screenshot on debug device that does not support screenshot', () async { + testWithoutContext('s, will not take screenshot on non-web device without screenshot tooling support', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); - final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ - listViews, - FakeVmServiceRequest( - method: 'ext.flutter.debugAllowBanner', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'enabled': 'false', - }, - ), - FakeVmServiceRequest( - method: '_flutter.screenshot', - args: <String, Object>{}, - jsonResponse: <String, Object>{ - 'screenshot': base64.encode(<int>[1, 2, 3, 4]), - }, - ), - FakeVmServiceRequest( - method: 'ext.flutter.debugAllowBanner', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'enabled': 'true', - }, - ), - ], logger: logger, fileSystem: fileSystem); + final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], logger: logger, fileSystem: fileSystem); await terminalHandler.processTerminalInput('s'); - expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); - expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]); + expect(logger.statusText, isNot(contains('Screenshot written to'))); }); testWithoutContext('s, can take screenshot on debug web device that does not support screenshot', () async { @@ -1133,66 +1109,6 @@ void main() { expect(fileSystem.currentDirectory.childFile('flutter_01.png'), isNot(exists)); }); - testWithoutContext('s, bails taking screenshot on debug device if debugAllowBanner throws RpcError', () async { - final BufferLogger logger = BufferLogger.test(); - final FileSystem fileSystem = MemoryFileSystem.test(); - final TerminalHandler terminalHandler = setUpTerminalHandler( - <FakeVmServiceRequest>[ - listViews, - FakeVmServiceRequest( - method: 'ext.flutter.debugAllowBanner', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'enabled': 'false', - }, - // Failed response, - errorCode: RPCErrorCodes.kInternalError, - ), - ], - logger: logger, - fileSystem: fileSystem, - ); - - await terminalHandler.processTerminalInput('s'); - - expect(logger.errorText, contains('Error')); - }); - - testWithoutContext('s, bails taking screenshot on debug device if flutter.screenshot throws RpcError, restoring banner', () async { - final BufferLogger logger = BufferLogger.test(); - final FileSystem fileSystem = MemoryFileSystem.test(); - final TerminalHandler terminalHandler = setUpTerminalHandler( - <FakeVmServiceRequest>[ - listViews, - FakeVmServiceRequest( - method: 'ext.flutter.debugAllowBanner', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'enabled': 'false', - }, - ), - const FakeVmServiceRequest( - method: '_flutter.screenshot', - // Failed response, - errorCode: RPCErrorCodes.kInternalError, - ), - FakeVmServiceRequest( - method: 'ext.flutter.debugAllowBanner', - args: <String, Object?>{ - 'isolateId': fakeUnpausedIsolate.id, - 'enabled': 'true', - }, - ), - ], - logger: logger, - fileSystem: fileSystem, - ); - - await terminalHandler.processTerminalInput('s'); - - expect(logger.errorText, contains('Error')); - }); - testWithoutContext('s, bails taking screenshot on debug device if dwds.screenshot throws RpcError, restoring banner', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); diff --git a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart index 7aa5bbb4ddaa3..58b8f2f3a4cde 100644 --- a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart @@ -166,7 +166,7 @@ flutter: linux: dartPluginClass: APlugin environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=2.5.0" '''); @@ -234,6 +234,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { if (compilerOutput != null) { fileSystem!.file(compilerOutput!.outputFilename).createSync(recursive: true); diff --git a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart index 6cc826e7deed0..165075e067fb7 100644 --- a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart +++ b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart @@ -98,7 +98,6 @@ void main() { artifacts: Artifacts.test(), logger: BufferLogger.test(), flutterVersion: FakeFlutterVersion(), - operatingSystemUtils: FakeOperatingSystemUtils(), ); logLines = <String>[]; device.getLogReader().logLines.listen(logLines.add); @@ -213,7 +212,6 @@ FlutterTesterDevices setUpFlutterTesterDevices() { processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), flutterVersion: FakeFlutterVersion(), - operatingSystemUtils: FakeOperatingSystemUtils(), ); } diff --git a/packages/flutter_tools/test/general.shard/update_packages_test.dart b/packages/flutter_tools/test/general.shard/update_packages_test.dart index b92fe6b15a40e..1bfe39ada9538 100644 --- a/packages/flutter_tools/test/general.shard/update_packages_test.dart +++ b/packages/flutter_tools/test/general.shard/update_packages_test.dart @@ -17,7 +17,7 @@ description: A framework for writing Flutter applications homepage: http://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -51,7 +51,7 @@ description: A dummy pubspec with no dependencies homepage: http://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' '''; const String kInvalidGitPubspec = ''' @@ -60,7 +60,7 @@ description: A framework for writing Flutter applications homepage: http://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: # To update these, use "flutter update-packages --force-upgrade". @@ -106,8 +106,9 @@ void main() { 'flutter_template_images', 'video_player', 'material_color_utilities', - 'url_launcher_android', 'archive', + 'leak_tracker', + 'leak_tracker_flutter_testing', ]), ); }); diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index ab4c730d444d5..54ba0bcc16673 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -133,6 +133,50 @@ void main() { Cache: () => cache, }); + testUsingContext('does not crash when git log outputs malformed output', () async { + const String flutterUpstreamUrl = 'https://github.com/flutter/flutter.git'; + + final String malformedGitLogOutput = '${getChannelUpToDateVersion()}[0x7FF9E2A75000] ANOMALY: meaningless REX prefix used'; + processManager.addCommands(<FakeCommand>[ + const FakeCommand( + command: <String>['git', '-c', 'log.showSignature=false', 'log', '-n', '1', '--pretty=format:%H'], + stdout: '1234abcd', + ), + const FakeCommand( + command: <String>['git', 'tag', '--points-at', '1234abcd'], + ), + const FakeCommand( + command: <String>['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'], + stdout: '0.1.2-3-1234abcd', + ), + FakeCommand( + command: const <String>['git', 'symbolic-ref', '--short', 'HEAD'], + stdout: channel, + ), + FakeCommand( + command: const <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'], + stdout: 'origin/$channel', + ), + const FakeCommand( + command: <String>['git', 'ls-remote', '--get-url', 'origin'], + stdout: flutterUpstreamUrl, + ), + FakeCommand( + command: const <String>['git', '-c', 'log.showSignature=false', 'log', 'HEAD', '-n', '1', '--pretty=format:%ad', '--date=iso'], + stdout: malformedGitLogOutput, + ), + ]); + + final FlutterVersion flutterVersion = FlutterVersion(clock: _testClock, fs: fs, flutterRoot: flutterRoot); + await flutterVersion.checkFlutterVersionFreshness(); + + expect(testLogger.statusText, isEmpty); + expect(processManager, hasNoRemainingExpectations); + }, overrides: <Type, Generator>{ + ProcessManager: () => processManager, + Cache: () => cache, + }); + testWithoutContext('prints nothing when Flutter installation looks out-of-date but is actually up-to-date', () async { final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); @@ -325,6 +369,7 @@ void main() { const String flutterStandardUrlDotGit = 'https://github.com/flutter/flutter.git'; const String flutterNonStandardUrlDotGit = 'https://githubmirror.com/flutter/flutter.git'; const String flutterStandardSshUrlDotGit = 'git@github.com:flutter/flutter.git'; + const String flutterFullSshUrlDotGit = 'ssh://git@github.com/flutter/flutter.git'; VersionCheckError? runUpstreamValidator({ String? versionUpstreamUrl, @@ -394,6 +439,10 @@ void main() { expect(runUpstreamValidator(versionUpstreamUrl: flutterStandardSshUrlDotGit), isNull); }); + testWithoutContext('does not return error at full ssh url with FLUTTER_GIT_URL unset', () { + expect(runUpstreamValidator(versionUpstreamUrl: flutterFullSshUrlDotGit), isNull); + }); + testWithoutContext('stripDotGit removes ".git" suffix if any', () { expect(VersionUpstreamValidator.stripDotGit('https://github.com/flutter/flutter.git'), 'https://github.com/flutter/flutter'); expect(VersionUpstreamValidator.stripDotGit('https://github.com/flutter/flutter'), 'https://github.com/flutter/flutter'); @@ -549,6 +598,43 @@ void main() { Cache: () => cache, }); + testUsingContext('_FlutterVersionFromFile.ensureVersionFile ensures legacy version file exists', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final Directory flutterRoot = fs.directory('/path/to/flutter'); + final Directory cacheDir = flutterRoot + .childDirectory('bin') + .childDirectory('cache') + ..createSync(recursive: true); + const String devToolsVersion = '0000000'; + final File legacyVersionFile = flutterRoot.childFile('version'); + const Map<String, Object> versionJson = <String, Object>{ + 'channel': 'stable', + 'frameworkVersion': '1.2.3', + 'repositoryUrl': 'https://github.com/flutter/flutter.git', + 'frameworkRevision': '1234abcd', + 'frameworkCommitDate': '2023-04-28 12:34:56 -0400', + 'engineRevision': 'deadbeef', + 'dartSdkVersion': 'deadbeef2', + 'devToolsVersion': devToolsVersion, + 'flutterVersion': 'foo', + }; + cacheDir.childFile('flutter.version.json').writeAsStringSync( + jsonEncode(versionJson), + ); + expect(legacyVersionFile.existsSync(), isFalse); + final FlutterVersion flutterVersion = FlutterVersion( + clock: _testClock, + fs: fs, + flutterRoot: flutterRoot.path, + ); + flutterVersion.ensureVersionFile(); + expect(legacyVersionFile.existsSync(), isTrue); + expect(legacyVersionFile.readAsStringSync(), '1.2.3'); + }, overrides: <Type, Generator>{ + ProcessManager: () => processManager, + Cache: () => cache, + }); + testUsingContext('FlutterVersion() falls back to git if .version.json is malformed', () async { final MemoryFileSystem fs = MemoryFileSystem.test(); final Directory flutterRoot = fs.directory(fs.path.join('path', 'to', 'flutter')); diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index 841e06b51040d..2557b3a289758 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart @@ -9,8 +9,6 @@ import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; -import 'package:flutter_tools/src/ios/xcodeproj.dart'; -import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -85,28 +83,6 @@ void main() { expect(mockVMService.services, containsPair(kFlutterMemoryInfoServiceName, kFlutterToolAlias)); }); - testWithoutContext('VmService registers flutterGetIOSBuildOptions service', () async { - final MockVMService mockVMService = MockVMService(); - final FlutterProject mockedFlutterProject = MockFlutterProject(); - await setUpVmService( - flutterProject: mockedFlutterProject, - vmService: mockVMService, - ); - - expect(mockVMService.services, containsPair(kFlutterGetIOSBuildOptionsServiceName, kFlutterToolAlias)); - }); - - testWithoutContext('VmService registers flutterGetAndroidBuildVariants service', () async { - final MockVMService mockVMService = MockVMService(); - final FlutterProject mockedFlutterProject = MockFlutterProject(); - await setUpVmService( - flutterProject: mockedFlutterProject, - vmService: mockVMService, - ); - - expect(mockVMService.services, containsPair(kFlutterGetAndroidBuildVariantsServiceName, kFlutterToolAlias)); - }); - testWithoutContext('VM Service registers flutterGetSkSL service', () async { final MockVMService mockVMService = MockVMService(); await setUpVmService( @@ -288,68 +264,6 @@ void main() { ])); }); - testWithoutContext('VmService forward flutterGetIOSBuildOptions request and response correctly', () async { - final MockVMService vmService = MockVMService(); - final XcodeProjectInfo expectedProjectInfo = XcodeProjectInfo( - <String>['target1', 'target2'], - <String>['config1', 'config2'], - <String>['scheme1', 'scheme2'], - MockLogger(), - ); - final FlutterProject mockedFlutterProject = MockFlutterProject( - mockedIos: MockIosProject(mockedInfo: expectedProjectInfo), - ); - await setUpVmService( - flutterProject: mockedFlutterProject, - vmService: vmService - ); - final vm_service.ServiceCallback cb = vmService.serviceCallBacks[kFlutterGetIOSBuildOptionsServiceName]!; - - final Map<String, dynamic> response = await cb(<String, dynamic>{}); - final Map<String, dynamic> result = response['result']! as Map<String, dynamic>; - expect(result[kResultType], kResultTypeSuccess); - expect(result['targets'], expectedProjectInfo.targets); - expect(result['buildConfigurations'], expectedProjectInfo.buildConfigurations); - expect(result['schemes'], expectedProjectInfo.schemes); - }); - - testWithoutContext('VmService forward flutterGetAndroidBuildVariants request and response correctly', () async { - final MockVMService vmService = MockVMService(); - final List<String> expectedOptions = <String>['debug', 'release', 'profile']; - final FlutterProject mockedFlutterProject = MockFlutterProject( - mockedAndroid: MockAndroidProject(mockedOptions: expectedOptions), - ); - await setUpVmService( - flutterProject: mockedFlutterProject, - vmService: vmService - ); - final vm_service.ServiceCallback cb = vmService.serviceCallBacks[kFlutterGetAndroidBuildVariantsServiceName]!; - - final Map<String, dynamic> response = await cb(<String, dynamic>{}); - final Map<String, dynamic> result = response['result']! as Map<String, dynamic>; - expect(result[kResultType], kResultTypeSuccess); - expect(result['variants'], expectedOptions); - }); - - testWithoutContext('VmService forward flutterGetIOSBuildOptions request and response correctly when no iOS project', () async { - final MockVMService vmService = MockVMService(); - final FlutterProject mockedFlutterProject = MockFlutterProject( - mockedIos: MockIosProject(), - ); - await setUpVmService( - flutterProject: mockedFlutterProject, - vmService: vmService - ); - final vm_service.ServiceCallback cb = vmService.serviceCallBacks[kFlutterGetIOSBuildOptionsServiceName]!; - - final Map<String, dynamic> response = await cb(<String, dynamic>{}); - final Map<String, dynamic> result = response['result']! as Map<String, dynamic>; - expect(result[kResultType], kResultTypeSuccess); - expect(result['targets'], isNull); - expect(result['buildConfigurations'], isNull); - expect(result['schemes'], isNull); - }); - testWithoutContext('runInView forwards arguments correctly', () async { final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( requests: <VmServiceExpectation>[ @@ -528,10 +442,6 @@ void main() { method: kListViewsMethod, errorCode: RPCErrorCodes.kServiceDisappeared, ), - const FakeVmServiceRequest( - method: kScreenshotMethod, - errorCode: RPCErrorCodes.kServiceDisappeared, - ), const FakeVmServiceRequest( method: kScreenshotSkpMethod, errorCode: RPCErrorCodes.kServiceDisappeared, @@ -566,9 +476,6 @@ void main() { final List<FlutterView> views = await fakeVmServiceHost.vmService.getFlutterViews(); expect(views, isEmpty); - final vm_service.Response? screenshot = await fakeVmServiceHost.vmService.screenshot(); - expect(screenshot, isNull); - final vm_service.Response? screenshotSkp = await fakeVmServiceHost.vmService.screenshotSkp(); expect(screenshotSkp, isNull); @@ -937,40 +844,6 @@ void main() { }); } -class MockFlutterProject extends Fake implements FlutterProject { - MockFlutterProject({ - IosProject? mockedIos, - AndroidProject? mockedAndroid, - }) : ios = mockedIos ?? MockIosProject(), - android = mockedAndroid ?? MockAndroidProject(); - - @override - final IosProject ios; - - @override - final AndroidProject android; -} - -class MockIosProject extends Fake implements IosProject { - MockIosProject({this.mockedInfo}); - - final XcodeProjectInfo? mockedInfo; - - @override - Future<XcodeProjectInfo?> projectInfo() async => mockedInfo; -} - -class MockAndroidProject extends Fake implements AndroidProject { - MockAndroidProject({this.mockedOptions = const <String>[]}); - - final List<String> mockedOptions; - - @override - Future<List<String>> getBuildVariants() async => mockedOptions; -} - -class MockLogger extends Fake implements Logger { } - class MockVMService extends Fake implements vm_service.VmService { final Map<String, String> services = <String, String>{}; final Map<String, vm_service.ServiceCallback> serviceCallBacks = <String, vm_service.ServiceCallback>{}; diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index e457408c05f08..6936829941b06 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -1190,6 +1190,7 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler { bool suppressErrors = false, bool checkDartPluginRegistry = false, File? dartPluginRegistrant, + Uri? nativeAssetsYaml, }) async { return output; } diff --git a/packages/flutter_tools/test/general.shard/windows/migrations/build_architecture_migration_test.dart b/packages/flutter_tools/test/general.shard/windows/migrations/build_architecture_migration_test.dart new file mode 100644 index 0000000000000..0cc1e9d7873b5 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/windows/migrations/build_architecture_migration_test.dart @@ -0,0 +1,299 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cmake_project.dart'; +import 'package:flutter_tools/src/windows/migrations/build_architecture_migration.dart'; +import 'package:test/fake.dart'; + +import '../../../src/common.dart'; + +void main () { + group('Windows Flutter build architecture migration', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeWindowsProject mockProject; + late File cmakeFile; + late Directory buildDirectory; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + cmakeFile = memoryFileSystem.file('CMakeLists.txt'); + buildDirectory = memoryFileSystem.directory('x64'); + + testLogger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences.test(), + ); + + mockProject = FakeWindowsProject(cmakeFile); + }); + + testWithoutContext('delete old runner directory', () { + buildDirectory.createSync(); + final Directory oldRunnerDirectory = + buildDirectory + .parent + .childDirectory('runner'); + oldRunnerDirectory.createSync(); + final File executable = oldRunnerDirectory.childFile('program.exe'); + executable.createSync(); + expect(oldRunnerDirectory.existsSync(), isTrue); + + final BuildArchitectureMigration migration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + migration.migrate(); + + expect(oldRunnerDirectory.existsSync(), isFalse); + expect(testLogger.traceText, + contains( + 'Deleting previous build folder ./runner.\n' + 'New binaries can be found in x64/runner.\n' + ) + ); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if CMake file is missing', () { + final BuildArchitectureMigration migration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + migration.migrate(); + expect(cmakeFile.existsSync(), isFalse); + + expect(testLogger.traceText, + contains('windows/flutter/CMakeLists.txt file not found, skipping build architecture migration')); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if nothing to migrate', () { + const String cmakeFileContents = 'Nothing to migrate'; + + cmakeFile.writeAsStringSync(cmakeFileContents); + + final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync(); + + final BuildArchitectureMigration buildArchitectureMigration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + buildArchitectureMigration.migrate(); + + expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt); + expect(cmakeFile.readAsStringSync(), cmakeFileContents); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if already migrated', () { + const String cmakeFileContents = + '# TODO: Move the rest of this into files in ephemeral. See\n' + '# https://github.com/flutter/flutter/issues/57146.\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\n' + '\n' + '# Set fallback configurations for older versions of the flutter tool.\n' + 'if (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n' + ' set(FLUTTER_TARGET_PLATFORM "windows-x64")\n' + 'endif()\n' + '\n' + '# === Flutter Library ===\n' + '...\n' + 'add_custom_command(\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\n' + ' \${CPP_WRAPPER_SOURCES_APP}\n' + ' \${PHONY_OUTPUT}\n' + ' COMMAND \${CMAKE_COMMAND} -E env\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\n' + ' \${FLUTTER_TARGET_PLATFORM} \$<CONFIG>\n' + ' VERBATIM\n' + ')\n'; + + cmakeFile.writeAsStringSync(cmakeFileContents); + + final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync(); + + final BuildArchitectureMigration buildArchitectureMigration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + buildArchitectureMigration.migrate(); + + expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt); + expect(cmakeFile.readAsStringSync(), cmakeFileContents); + + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if already migrated (CRLF)', () { + const String cmakeFileContents = + '# TODO: Move the rest of this into files in ephemeral. See\r\n' + '# https://github.com/flutter/flutter/issues/57146.\r\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\r\n' + '\r\n' + '# Set fallback configurations for older versions of the flutter tool.\r\n' + 'if (NOT DEFINED FLUTTER_TARGET_PLATFORM)\r\n' + ' set(FLUTTER_TARGET_PLATFORM "windows-x64")\r\n' + 'endif()\r\n' + '\r\n' + '# === Flutter Library ===\r\n' + '...\r\n' + 'add_custom_command(\r\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\r\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\r\n' + ' \${CPP_WRAPPER_SOURCES_APP}\r\n' + ' \${PHONY_OUTPUT}\r\n' + ' COMMAND \${CMAKE_COMMAND} -E env\r\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\r\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\r\n' + ' \${FLUTTER_TARGET_PLATFORM} \$<CONFIG>\r\n' + ' VERBATIM\r\n' + ')\r\n'; + + cmakeFile.writeAsStringSync(cmakeFileContents); + + final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync(); + + final BuildArchitectureMigration buildArchitectureMigration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + buildArchitectureMigration.migrate(); + + expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt); + expect(cmakeFile.readAsStringSync(), cmakeFileContents); + + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('migrates project to set the target platform', () { + cmakeFile.writeAsStringSync( + '# TODO: Move the rest of this into files in ephemeral. See\n' + '# https://github.com/flutter/flutter/issues/57146.\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\n' + '\n' + '# === Flutter Library ===\n' + '...\n' + 'add_custom_command(\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\n' + ' \${CPP_WRAPPER_SOURCES_APP}\n' + ' \${PHONY_OUTPUT}\n' + ' COMMAND \${CMAKE_COMMAND} -E env\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\n' + ' windows-x64 \$<CONFIG>\n' + ' VERBATIM\n' + ')\n' + ); + final BuildArchitectureMigration buildArchitectureMigration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + buildArchitectureMigration.migrate(); + + expect(cmakeFile.readAsStringSync(), + '# TODO: Move the rest of this into files in ephemeral. See\n' + '# https://github.com/flutter/flutter/issues/57146.\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\n' + '\n' + '# Set fallback configurations for older versions of the flutter tool.\n' + 'if (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n' + ' set(FLUTTER_TARGET_PLATFORM "windows-x64")\n' + 'endif()\n' + '\n' + '# === Flutter Library ===\n' + '...\n' + 'add_custom_command(\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\n' + ' \${CPP_WRAPPER_SOURCES_APP}\n' + ' \${PHONY_OUTPUT}\n' + ' COMMAND \${CMAKE_COMMAND} -E env\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\n' + ' \${FLUTTER_TARGET_PLATFORM} \$<CONFIG>\n' + ' VERBATIM\n' + ')\n' + ); + + expect(testLogger.statusText, contains('windows/flutter/CMakeLists.txt does not use FLUTTER_TARGET_PLATFORM, updating.')); + }); + + testWithoutContext('migrates project to set the target platform (CRLF)', () { + cmakeFile.writeAsStringSync( + '# TODO: Move the rest of this into files in ephemeral. See\r\n' + '# https://github.com/flutter/flutter/issues/57146.\r\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\r\n' + '\r\n' + '# === Flutter Library ===\r\n' + '...\r\n' + 'add_custom_command(\r\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\r\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\r\n' + ' \${CPP_WRAPPER_SOURCES_APP}\r\n' + ' \${PHONY_OUTPUT}\r\n' + ' COMMAND \${CMAKE_COMMAND} -E env\r\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\r\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\r\n' + ' windows-x64 \$<CONFIG>\r\n' + ' VERBATIM\r\n' + ')\r\n' + ); + + final BuildArchitectureMigration buildArchitectureMigration = BuildArchitectureMigration( + mockProject, + buildDirectory, + testLogger, + ); + buildArchitectureMigration.migrate(); + + expect(cmakeFile.readAsStringSync(), + '# TODO: Move the rest of this into files in ephemeral. See\r\n' + '# https://github.com/flutter/flutter/issues/57146.\r\n' + 'set(WRAPPER_ROOT "\${EPHEMERAL_DIR}/cpp_client_wrapper")\r\n' + '\r\n' + '# Set fallback configurations for older versions of the flutter tool.\r\n' + 'if (NOT DEFINED FLUTTER_TARGET_PLATFORM)\r\n' + ' set(FLUTTER_TARGET_PLATFORM "windows-x64")\r\n' + 'endif()\r\n' + '\r\n' + '# === Flutter Library ===\r\n' + '...\r\n' + 'add_custom_command(\r\n' + ' OUTPUT \${FLUTTER_LIBRARY} \${FLUTTER_LIBRARY_HEADERS}\r\n' + ' \${CPP_WRAPPER_SOURCES_CORE} \${CPP_WRAPPER_SOURCES_PLUGIN}\r\n' + ' \${CPP_WRAPPER_SOURCES_APP}\r\n' + ' \${PHONY_OUTPUT}\r\n' + ' COMMAND \${CMAKE_COMMAND} -E env\r\n' + ' \${FLUTTER_TOOL_ENVIRONMENT}\r\n' + ' "\${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"\r\n' + ' \${FLUTTER_TARGET_PLATFORM} \$<CONFIG>\r\n' + ' VERBATIM\r\n' + ')\r\n' + ); + + expect(testLogger.statusText, contains('windows/flutter/CMakeLists.txt does not use FLUTTER_TARGET_PLATFORM, updating.')); + }); + }); +} + +class FakeWindowsProject extends Fake implements WindowsProject { + FakeWindowsProject(this.managedCmakeFile); + + @override + final File managedCmakeFile; +} diff --git a/packages/flutter_tools/test/general.shard/windows/native_assets_test.dart b/packages/flutter_tools/test/general.shard/windows/native_assets_test.dart new file mode 100644 index 0000000000000..71a52fed23559 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/windows/native_assets_test.dart @@ -0,0 +1,439 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/dart/package_map.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/native_assets.dart'; +import 'package:flutter_tools/src/windows/native_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart' hide BuildMode, Target; +import 'package:native_assets_cli/native_assets_cli.dart' as native_assets_cli; +import 'package:package_config/package_config_types.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; +import '../fake_native_assets_build_runner.dart'; + +void main() { + late FakeProcessManager processManager; + late Environment environment; + late Artifacts artifacts; + late FileSystem fileSystem; + late BufferLogger logger; + late Uri projectUri; + + setUp(() { + processManager = FakeProcessManager.empty(); + logger = BufferLogger.test(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + environment = Environment.test( + fileSystem.currentDirectory, + inputs: <String, String>{}, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + environment.buildDir.createSync(recursive: true); + projectUri = environment.projectDir.uri; + }); + + testUsingContext('dry run with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + expect( + await dryRunNativeAssetsWindows( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ), + null, + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('build with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await buildNativeAssetsWindows( + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run for multiple OSes with no package config', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + await dryRunNativeAssetsMultipeOSes( + projectUri: projectUri, + fileSystem: fileSystem, + targetPlatforms: <TargetPlatform>[ + TargetPlatform.windows_x64, + ], + buildRunner: FakeNativeAssetsBuildRunner( + hasPackageConfigResult: false, + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + contains('No package config found. Skipping native assets compilation.'), + ); + }); + + testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsWindows( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('dry run with assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final Uri? nativeAssetsYaml = await dryRunNativeAssetsWindows( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.windowsX64, + path: AssetAbsolutePath(Uri.file('bar.dll')), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Dry running native assets for windows.', + 'Dry running native assets for windows done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/windows/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + contains('package:bar/bar.dart'), + ); + }); + + testUsingContext('build with assets but not enabled', overrides: <Type, Generator>{ + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => buildNativeAssetsWindows( + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ), + throwsToolExit( + message: 'Package(s) bar require the native assets feature to be enabled. ' + 'Enable using `flutter config --enable-native-assets`.', + ), + ); + }); + + testUsingContext('build no assets', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsWindows( + targetPlatform: TargetPlatform.windows_x64, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + ), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/windows/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + isNot(contains('package:bar/bar.dart')), + ); + expect( + environment.projectDir.childDirectory('build').childDirectory('native_assets').childDirectory('windows'), + exists, + ); + }); + + for (final bool flutterTester in <bool>[false, true]) { + String testName = ''; + if (flutterTester) { + testName += ' flutter tester'; + } + testUsingContext('build with assets$testName', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childDirectory('.dart_tool').childFile('package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + final File dylibAfterCompiling = fileSystem.file('bar.dll'); + // The mock doesn't create the file, so create it here. + await dylibAfterCompiling.create(); + final (Uri? nativeAssetsYaml, _) = await buildNativeAssetsWindows( + targetPlatform: TargetPlatform.windows_x64, + projectUri: projectUri, + buildMode: BuildMode.debug, + fileSystem: fileSystem, + flutterTester: flutterTester, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + buildResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.dynamic, + target: native_assets_cli.Target.windowsX64, + path: AssetAbsolutePath(dylibAfterCompiling.uri), + ), + ], + ), + ), + ); + expect( + (globals.logger as BufferLogger).traceText, + stringContainsInOrder(<String>[ + 'Building native assets for windows_x64 debug.', + 'Building native assets for windows_x64 done.', + ]), + ); + expect( + nativeAssetsYaml, + projectUri.resolve('build/native_assets/windows/native_assets.yaml'), + ); + expect( + await fileSystem.file(nativeAssetsYaml).readAsString(), + stringContainsInOrder(<String>[ + 'package:bar/bar.dart', + if (flutterTester) + // Tests run on host system, so the have the full path on the system. + '- ${projectUri.resolve('build/native_assets/windows/bar.dll').toFilePath()}' + else + // Apps are a bundle with the dylibs on their dlopen path. + '- bar.dll', + ]), + ); + }); + } + + testUsingContext('static libs not supported', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.empty(), + }, () async { + final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); + await packageConfig.parent.create(); + await packageConfig.create(); + expect( + () => dryRunNativeAssetsWindows( + projectUri: projectUri, + fileSystem: fileSystem, + buildRunner: FakeNativeAssetsBuildRunner( + packagesWithNativeAssetsResult: <Package>[ + Package('bar', projectUri), + ], + dryRunResult: FakeNativeAssetsBuilderResult( + assets: <Asset>[ + Asset( + id: 'package:bar/bar.dart', + linkMode: LinkMode.static, + target: native_assets_cli.Target.windowsX64, + path: AssetAbsolutePath(Uri.file(OS.windows.staticlibFileName('bar'))), + ), + ], + ), + ), + ), + throwsToolExit( + message: 'Native asset(s) package:bar/bar.dart have their link mode set to ' + 'static, but this is not yet supported. ' + 'For more info see https://github.com/dart-lang/sdk/issues/49418.', + ), + ); + }); + + // This logic is mocked in the other tests to avoid having test order + // randomization causing issues with what processes are invoked. + // Exercise the parsing of the process output in this separate test. + testUsingContext('NativeAssetsBuildRunnerImpl.cCompilerConfig', overrides: <Type, Generator>{ + FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), + ProcessManager: () => FakeProcessManager.list( + <FakeCommand>[ + FakeCommand( + command: <Pattern>[ + RegExp(r'(.*)vswhere.exe'), + '-format', + 'json', + '-products', + '*', + '-utf8', + '-latest', + '-version', + '16', + '-requires', + 'Microsoft.VisualStudio.Workload.NativeDesktop', + 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + 'Microsoft.VisualStudio.Component.VC.CMake.Project', + ], + stdout: r''' +[ + { + "instanceId": "491ec752", + "installDate": "2023-04-21T08:17:11Z", + "installationName": "VisualStudio/17.5.4+33530.505", + "installationPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community", + "installationVersion": "17.5.33530.505", + "productId": "Microsoft.VisualStudio.Product.Community", + "productPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\devenv.exe", + "state": 4294967295, + "isComplete": true, + "isLaunchable": true, + "isPrerelease": false, + "isRebootRequired": false, + "displayName": "Visual Studio Community 2022", + "description": "Powerful IDE, free for students, open-source contributors, and individuals", + "channelId": "VisualStudio.17.Release", + "channelUri": "https://aka.ms/vs/17/release/channel", + "enginePath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app\\ServiceHub\\Services\\Microsoft.VisualStudio.Setup.Service", + "installedChannelId": "VisualStudio.17.Release", + "installedChannelUri": "https://aka.ms/vs/17/release/channel", + "releaseNotes": "https://docs.microsoft.com/en-us/visualstudio/releases/2022/release-notes-v17.5#17.5.4", + "thirdPartyNotices": "https://go.microsoft.com/fwlink/?LinkId=661288", + "updateDate": "2023-04-21T08:17:11.2249473Z", + "catalog": { + "buildBranch": "d17.5", + "buildVersion": "17.5.33530.505", + "id": "VisualStudio/17.5.4+33530.505", + "localBuild": "build-lab", + "manifestName": "VisualStudio", + "manifestType": "installer", + "productDisplayVersion": "17.5.4", + "productLine": "Dev17", + "productLineVersion": "2022", + "productMilestone": "RTW", + "productMilestoneIsPreRelease": "False", + "productName": "Visual Studio", + "productPatchVersion": "4", + "productPreReleaseMilestoneSuffix": "1.0", + "productSemanticVersion": "17.5.4+33530.505", + "requiredEngineVersion": "3.5.2150.18781" + }, + "properties": { + "campaignId": "2060:abb99c5d1ecc4013acf2e1814b10b690", + "channelManifestId": "VisualStudio.17.Release/17.5.4+33530.505", + "nickname": "", + "setupEngineFilePath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\setup.exe" + } + } +] +''', // Newline at the end of the string. + ) + ], + ), + FileSystem: () => fileSystem, + }, () async { + if (!const LocalPlatform().isWindows) { + return; + } + + final Directory msvcBinDir = + fileSystem.directory(r'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64'); + await msvcBinDir.create(recursive: true); + + final File packagesFile = fileSystem + .directory(projectUri) + .childDirectory('.dart_tool') + .childFile('package_config.json'); + await packagesFile.parent.create(); + await packagesFile.create(); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + packagesFile, + logger: environment.logger, + ); + final NativeAssetsBuildRunner runner = NativeAssetsBuildRunnerImpl( + projectUri, + packageConfig, + fileSystem, + logger, + ); + final CCompilerConfig result = await runner.cCompilerConfig; + expect(result.cc?.toFilePath(), msvcBinDir.childFile('cl.exe').uri.toFilePath()); + expect(result.ar?.toFilePath(), msvcBinDir.childFile('lib.exe').uri.toFilePath()); + expect(result.ld?.toFilePath(), msvcBinDir.childFile('link.exe').uri.toFilePath()); + expect(result.envScript, isNotNull); + expect(result.envScriptArgs, isNotNull); + }); +} diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart index 6f2aa45411a32..57b07e929e280 100644 --- a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart @@ -11,12 +11,16 @@ import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/windows/visual_studio.dart'; import '../../src/common.dart'; -import '../../src/fake_process_manager.dart'; +import '../../src/context.dart'; const String programFilesPath = r'C:\Program Files (x86)'; const String visualStudioPath = programFilesPath + r'\Microsoft Visual Studio\2017\Community'; const String cmakePath = visualStudioPath + r'\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'; const String vswherePath = programFilesPath + r'\Microsoft Visual Studio\Installer\vswhere.exe'; +const String clPath = visualStudioPath + r'\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64\cl.exe'; +const String libPath = visualStudioPath + r'\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64\lib.exe'; +const String linkPath = visualStudioPath + r'\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64\link.exe'; +const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat'; final Platform windowsPlatform = FakePlatform( operatingSystem: 'windows', @@ -140,6 +144,7 @@ void setMockVswhereResponse( ]) { fileSystem.file(vswherePath).createSync(recursive: true); fileSystem.file(cmakePath).createSync(recursive: true); + fileSystem.file(clPath).createSync(recursive: true); final String finalResponse = responseOverride ?? (response != null ? json.encode(<Map<String, dynamic>>[response]) : '[]'); final List<String> requirementArguments = requiredComponents == null @@ -300,11 +305,11 @@ void setMockSdkRegResponse( const String registryKey = r'InstallationFolder'; const String installationPath = r'C:\Program Files (x86)\Windows Kits\10\'; final String stdout = registryPresent - ? ''' + ? ''' $registryPath $registryKey REG_SZ $installationPath ''' - : ''' + : ''' ERROR: The system was unable to find the specified registry key or value. '''; @@ -776,6 +781,10 @@ void main() { expect(visualStudio.hasNecessaryComponents, true); expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); }); testWithoutContext('Everything returns good values when Build Tools is present with all components', () { @@ -814,6 +823,10 @@ void main() { expect(visualStudio.hasNecessaryComponents, true); expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 17 2022')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); }); testWithoutContext('Metadata is for compatible version when latest is missing components', () { @@ -905,8 +918,7 @@ void main() { }); testWithoutContext('Ignores unicode replacement char in unused properties', () { - final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse) - ..['unused'] = 'Bad UTF8 \u{FFFD}'; + final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)..['unused'] = 'Bad UTF8 \u{FFFD}'; setMockCompatibleVisualStudioInstallation( response, @@ -919,6 +931,10 @@ void main() { expect(visualStudio.hasNecessaryComponents, true); expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); }); testWithoutContext('Throws ToolExit on bad UTF-8 in installationPath', () { @@ -953,13 +969,16 @@ void main() { expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); expect(visualStudio.displayName, equals('\u{FFFD}')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); }); testWithoutContext("Ignores bad UTF-8 in catalog's productDisplayVersion", () { final Map<String, dynamic> catalog = Map<String, dynamic>.of(_defaultResponse['catalog'] as Map<String, dynamic>) ..['productDisplayVersion'] = '\u{FFFD}'; - final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse) - ..['catalog'] = catalog; + final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)..['catalog'] = catalog; setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager); @@ -969,6 +988,10 @@ void main() { expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); expect(visualStudio.displayVersion, equals('\u{FFFD}')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); }); testWithoutContext('Ignores malformed JSON in description property', () { @@ -987,101 +1010,109 @@ void main() { expect(visualStudio.cmakePath, equals(cmakePath)); expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); expect(visualStudio.displayVersion, equals('16.2.5')); + expect(visualStudio.clPath, equals(clPath)); + expect(visualStudio.libPath, equals(libPath)); + expect(visualStudio.linkPath, equals(linkPath)); + expect(visualStudio.vcvarsPath, equals(vcvarsPath)); expect(fixture.logger.warningText, isEmpty); }); }); group(VswhereDetails, () { - test('Accepts empty JSON', () { - const bool meetsRequirements = true; - final Map<String, dynamic> json = <String, dynamic>{}; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json); - - expect(result.installationPath, null); - expect(result.displayName, null); - expect(result.fullVersion, null); - expect(result.isComplete, null); - expect(result.isLaunchable, null); - expect(result.isRebootRequired, null); - expect(result.isPrerelease, null); - expect(result.catalogDisplayVersion, null); - expect(result.isUsable, isTrue); - }); - - test('Ignores unknown JSON properties', () { - const bool meetsRequirements = true; - final Map<String, dynamic> json = <String, dynamic>{ - 'hello': 'world', - }; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json); - - expect(result.installationPath, null); - expect(result.displayName, null); - expect(result.fullVersion, null); - expect(result.isComplete, null); - expect(result.isLaunchable, null); - expect(result.isRebootRequired, null); - expect(result.isPrerelease, null); - expect(result.catalogDisplayVersion, null); - expect(result.isUsable, isTrue); - }); - - test('Accepts JSON', () { - const bool meetsRequirements = true; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse); - - expect(result.installationPath, visualStudioPath); - expect(result.displayName, 'Visual Studio Community 2019'); - expect(result.fullVersion, '16.2.29306.81'); - expect(result.isComplete, true); - expect(result.isLaunchable, true); - expect(result.isRebootRequired, false); - expect(result.isPrerelease, false); - expect(result.catalogDisplayVersion, '16.2.5'); - expect(result.isUsable, isTrue); - }); - - test('Installation that does not satisfy requirements is not usable', () { - const bool meetsRequirements = false; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse); - - expect(result.isUsable, isFalse); - }); - - test('Incomplete installation is not usable', () { - const bool meetsRequirements = true; - final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse) - ..['isComplete'] = false; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json); - - expect(result.isUsable, isFalse); - }); - - test('Unlaunchable installation is not usable', () { - const bool meetsRequirements = true; - final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse) - ..['isLaunchable'] = false; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json); - - expect(result.isUsable, isFalse); - }); - - test('Installation that requires reboot is not usable', () { - const bool meetsRequirements = true; - final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse) - ..['isRebootRequired'] = true; - - final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json); - - expect(result.isUsable, isFalse); - }); + test('Accepts empty JSON', () { + const bool meetsRequirements = true; + final Map<String, dynamic> json = <String, dynamic>{}; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json, msvcVersion); + + expect(result.installationPath, null); + expect(result.displayName, null); + expect(result.fullVersion, null); + expect(result.isComplete, null); + expect(result.isLaunchable, null); + expect(result.isRebootRequired, null); + expect(result.isPrerelease, null); + expect(result.catalogDisplayVersion, null); + expect(result.isUsable, isTrue); + }); + + test('Ignores unknown JSON properties', () { + const bool meetsRequirements = true; + final Map<String, dynamic> json = <String, dynamic>{ + 'hello': 'world', + }; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json, msvcVersion); + + expect(result.installationPath, null); + expect(result.displayName, null); + expect(result.fullVersion, null); + expect(result.isComplete, null); + expect(result.isLaunchable, null); + expect(result.isRebootRequired, null); + expect(result.isPrerelease, null); + expect(result.catalogDisplayVersion, null); + expect(result.isUsable, isTrue); + }); + + test('Accepts JSON', () { + const bool meetsRequirements = true; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse, msvcVersion); + + expect(result.installationPath, visualStudioPath); + expect(result.displayName, 'Visual Studio Community 2019'); + expect(result.fullVersion, '16.2.29306.81'); + expect(result.isComplete, true); + expect(result.isLaunchable, true); + expect(result.isRebootRequired, false); + expect(result.isPrerelease, false); + expect(result.catalogDisplayVersion, '16.2.5'); + expect(result.isUsable, isTrue); + }); + + test('Installation that does not satisfy requirements is not usable', () { + const bool meetsRequirements = false; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse, msvcVersion); + + expect(result.isUsable, isFalse); + }); + + test('Incomplete installation is not usable', () { + const bool meetsRequirements = true; + final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)..['isComplete'] = false; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json, msvcVersion); + + expect(result.isUsable, isFalse); + }); + + test('Unlaunchable installation is not usable', () { + const bool meetsRequirements = true; + final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)..['isLaunchable'] = false; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json, msvcVersion); + + expect(result.isUsable, isFalse); + }); + + test('Installation that requires reboot is not usable', () { + const bool meetsRequirements = true; + final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)..['isRebootRequired'] = true; + const String msvcVersion = ''; + + final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json, msvcVersion); + + expect(result.isUsable, isFalse); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart index b501b5e901906..2858d7d25cd88 100644 --- a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart @@ -60,9 +60,12 @@ void main() { fakeVisualStudio.isPrerelease = true; final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage(userMessages.visualStudioIsPrerelease); + const ValidationMessage expectedMessage = ValidationMessage( + 'The current Visual Studio installation is a pre-release version. ' + 'It may not be supported by Flutter yet.', + ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); }); testWithoutContext('Emits a partial status when Visual Studio installation is incomplete', () async { @@ -74,9 +77,12 @@ void main() { fakeVisualStudio.isComplete = false; final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage.error(userMessages.visualStudioIsIncomplete); + const ValidationMessage expectedMessage = ValidationMessage.error( + 'The current Visual Studio installation is incomplete.\n' + 'Please use Visual Studio Installer to complete the installation or reinstall Visual Studio.', + ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); expect(result.type, ValidationType.partial); }); @@ -89,9 +95,11 @@ void main() { fakeVisualStudio.isRebootRequired = true; final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage.error(userMessages.visualStudioRebootRequired); + const ValidationMessage expectedMessage = ValidationMessage.error( + 'Visual Studio requires a reboot of your system to complete installation.', + ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); expect(result.type, ValidationType.partial); }); @@ -104,9 +112,11 @@ void main() { fakeVisualStudio.isLaunchable = false; final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage.error(userMessages.visualStudioNotLaunchable); + const ValidationMessage expectedMessage = ValidationMessage.error( + 'The current Visual Studio installation is not launchable. Please reinstall Visual Studio.', + ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); expect(result.type, ValidationType.partial); }); @@ -118,14 +128,13 @@ void main() { configureMockVisualStudioAsTooOld(); final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage.error( - userMessages.visualStudioTooOld( - fakeVisualStudio.minimumVersionDescription, - fakeVisualStudio.workloadDescription, - ), + const ValidationMessage expectedMessage = ValidationMessage.error( + 'Visual Studio 2019 or later is required.\n' + 'Download at https://visualstudio.microsoft.com/downloads/.\n' + 'Please install the "Desktop development" workload, including all of its default components', ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); expect(result.type, ValidationType.partial); }); @@ -161,10 +170,11 @@ void main() { configureMockVisualStudioAsInstalled(); final ValidationResult result = await validator.validate(); - final ValidationMessage expectedDisplayNameMessage = ValidationMessage( - userMessages.visualStudioVersion(fakeVisualStudio.displayName!, fakeVisualStudio.fullVersion!)); + const ValidationMessage expectedDisplayNameMessage = ValidationMessage( + 'Visual Studio Community 2019 version 16.2', + ); - expect(result.messages.contains(expectedDisplayNameMessage), true); + expect(result.messages, contains(expectedDisplayNameMessage)); expect(result.type, ValidationType.success); }); @@ -176,13 +186,13 @@ void main() { configureMockVisualStudioAsNotInstalled(); final ValidationResult result = await validator.validate(); - final ValidationMessage expectedMessage = ValidationMessage.error( - userMessages.visualStudioMissing( - fakeVisualStudio.workloadDescription, - ), + const ValidationMessage expectedMessage = ValidationMessage.error( + 'Visual Studio not installed; this is necessary to develop Windows apps.\n' + 'Download at https://visualstudio.microsoft.com/downloads/.\n' + 'Please install the "Desktop development" workload, including all of its default components' ); - expect(result.messages.contains(expectedMessage), true); + expect(result.messages, contains(expectedMessage)); expect(result.type, ValidationType.missing); }); }); diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index 482e4ede33d37..5215644437607 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -7,7 +7,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/io.dart'; import '../../bin/xcode_backend.dart'; -import '../src/common.dart'; +import '../src/common.dart' hide Context; import '../src/fake_process_manager.dart'; void main() { @@ -51,6 +51,7 @@ void main() { '-dTrackWidgetCreation=', '-dDartObfuscation=', '-dAction=build', + '-dFrontendServerStarterPath=', '--ExtraGenSnapshotOptions=', '--DartDefines=', '--ExtraFrontEndOptions=', @@ -103,6 +104,7 @@ void main() { '-dTrackWidgetCreation=', '-dDartObfuscation=', '-dAction=', + '-dFrontendServerStarterPath=', '--ExtraGenSnapshotOptions=', '--DartDefines=', '--ExtraFrontEndOptions=', @@ -136,6 +138,7 @@ void main() { const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2'; const String extraFrontEndOptions = '--some-option'; const String extraGenSnapshotOptions = '--obfuscate'; + const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart'; const String sdkRoot = '/path/to/sdk'; const String splitDebugInfo = '/path/to/split/debug/info'; const String trackWidgetCreation = 'true'; @@ -154,6 +157,7 @@ void main() { 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions, 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions, 'FLUTTER_ROOT': flutterRoot.path, + 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, 'INFOPLIST_PATH': 'Info.plist', 'SDKROOT': sdkRoot, 'SPLIT_DEBUG_INFO': splitDebugInfo, @@ -177,6 +181,7 @@ void main() { '-dTrackWidgetCreation=$trackWidgetCreation', '-dDartObfuscation=$dartObfuscation', '-dAction=install', + '-dFrontendServerStarterPath=$frontendServerStarterPath', '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions', '--DartDefines=$dartDefines', '--ExtraFrontEndOptions=$extraFrontEndOptions', diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index 3b3de5f7921ee..c35287c6e048d 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -254,6 +254,7 @@ void main() { 'VERBOSE_SCRIPT_LOGGING': '1', 'FLUTTER_BUILD_MODE': 'release', 'ACTION': 'install', + 'FLUTTER_BUILD_DIR': 'build', // Skip bitcode stripping since we just checked that above. }, ); diff --git a/packages/flutter_tools/test/integration.shard/analyze_once_test.dart b/packages/flutter_tools/test/integration.shard/analyze_once_test.dart index 6c993dd0dae46..b9588a5555624 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_once_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_once_test.dart @@ -481,7 +481,7 @@ class _MyHomePageState extends State<MyHomePage> { const String pubspecYamlSrc = r''' name: flutter_project environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart similarity index 66% rename from packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_test.dart rename to packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart index eed136cc72c0c..7a1bf43babe9a 100644 --- a/packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'dart:io' as io; -import 'package:collection/collection.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart' show getGradlewFileName; @@ -136,8 +135,15 @@ void main() { tryToDelete(tempDir); }); + void testDeeplink(dynamic deeplink, String scheme, String host, String path) { + deeplink as Map<String, dynamic>; + expect(deeplink['scheme'], scheme); + expect(deeplink['host'], host); + expect(deeplink['path'], path); + } + testWithoutContext( - 'gradle task exists named print<mode>AppLinkDomains that prints app link domains', () async { + 'gradle task outputs<mode>AppLinkSettings works when a project has app links', () async { // Create a new flutter project. final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); @@ -147,7 +153,7 @@ void main() { tempDir.path, '--project-name=testapp', ], workingDirectory: tempDir.path); - expect(result.exitCode, 0); + expect(result, const ProcessResultMatcher()); // Adds intent filters for app links final String androidManifestPath = fileSystem.path.join(tempDir.path, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); final io.File androidManifestFile = io.File(androidManifestPath); @@ -166,24 +172,68 @@ void main() { 'apk', '--config-only', ], workingDirectory: tempDir.path); - expect(result.exitCode, 0); + expect(result, const ProcessResultMatcher()); + + final Directory androidApp = tempDir.childDirectory('android'); + result = await processManager.run(<String>[ + '.${platform.pathSeparator}${getGradlewFileName(platform)}', + ...getLocalEngineArguments(), + '-q', // quiet output. + 'outputDebugAppLinkSettings', + ], workingDirectory: androidApp.path); + + expect(result, const ProcessResultMatcher()); + + final io.File fileDump = tempDir.childDirectory('build').childDirectory('app').childFile('app-link-settings-debug.json'); + expect(fileDump.existsSync(), true); + final Map<String, dynamic> json = jsonDecode(fileDump.readAsStringSync()) as Map<String, dynamic>; + expect(json['applicationId'], 'com.example.testapp'); + final List<dynamic> deeplinks = json['deeplinks']! as List<dynamic>; + expect(deeplinks.length, 5); + testDeeplink(deeplinks[0], 'http', 'pure-http.com', '.*'); + testDeeplink(deeplinks[1], 'custom', 'custom.com', '.*'); + testDeeplink(deeplinks[2], 'custom', 'hybrid.com', '.*'); + testDeeplink(deeplinks[3], 'http', 'hybrid.com', '.*'); + testDeeplink(deeplinks[4], 'http', 'non-auto-verify.com', '.*'); + }); + + testWithoutContext( + 'gradle task outputs<mode>AppLinkSettings works when a project does not have app link', () async { + // Create a new flutter project. + final String flutterBin = + fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); + ProcessResult result = await processManager.run(<String>[ + flutterBin, + 'create', + tempDir.path, + '--project-name=testapp', + ], workingDirectory: tempDir.path); + expect(result, const ProcessResultMatcher()); + + // Ensure that gradle files exists from templates. + result = await processManager.run(<String>[ + flutterBin, + 'build', + 'apk', + '--config-only', + ], workingDirectory: tempDir.path); + expect(result, const ProcessResultMatcher()); final Directory androidApp = tempDir.childDirectory('android'); result = await processManager.run(<String>[ '.${platform.pathSeparator}${getGradlewFileName(platform)}', ...getLocalEngineArguments(), '-q', // quiet output. - 'printDebugAppLinkDomains', + 'outputDebugAppLinkSettings', ], workingDirectory: androidApp.path); - expect(result.exitCode, 0); + expect(result, const ProcessResultMatcher()); - const List<String> expectedLines = <String>[ - // Should only pick up the pure and hybrid intent filters - 'Domain: pure-http.com', - 'Domain: hybrid.com', - ]; - final List<String> actualLines = LineSplitter.split(result.stdout.toString()).toList(); - expect(const ListEquality<String>().equals(actualLines, expectedLines), isTrue); + final io.File fileDump = tempDir.childDirectory('build').childDirectory('app').childFile('app-link-settings-debug.json'); + expect(fileDump.existsSync(), true); + final Map<String, dynamic> json = jsonDecode(fileDump.readAsStringSync()) as Map<String, dynamic>; + expect(json['applicationId'], 'com.example.testapp'); + final List<dynamic> deeplinks = json['deeplinks']! as List<dynamic>; + expect(deeplinks.length, 0); }); } diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart deleted file mode 100644 index 563344660e067..0000000000000 --- a/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:file/file.dart'; -import 'package:flutter_tools/src/android/gradle_utils.dart' - show getGradlewFileName; -import 'package:flutter_tools/src/base/io.dart'; - -import '../src/common.dart'; -import 'test_utils.dart'; - -void main() { - late Directory tempDir; - - setUp(() async { - tempDir = createResolvedTempDirectorySync('run_test.'); - }); - - tearDown(() async { - tryToDelete(tempDir); - }); - - testWithoutContext( - 'gradle task exists named print<mode>ApplicationId that prints application id', () async { - // Create a new flutter project. - final String flutterBin = - fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); - ProcessResult result = await processManager.run(<String>[ - flutterBin, - 'create', - tempDir.path, - '--project-name=testapp', - ], workingDirectory: tempDir.path); - expect(result.exitCode, 0); - // Ensure that gradle files exists from templates. - result = await processManager.run(<String>[ - flutterBin, - 'build', - 'apk', - '--config-only', - ], workingDirectory: tempDir.path); - expect(result.exitCode, 0); - - final Directory androidApp = tempDir.childDirectory('android'); - result = await processManager.run(<String>[ - '.${platform.pathSeparator}${getGradlewFileName(platform)}', - ...getLocalEngineArguments(), - '-q', // quiet output. - 'printDebugApplicationId', - ], workingDirectory: androidApp.path); - // Verify that gradlew has a javaVersion task. - expect(result.exitCode, 0); - // Verify the format is a number on its own line. - const List<String> expectedLines = <String>[ - 'ApplicationId: com.example.testapp', - ]; - final List<String> actualLines = LineSplitter.split(result.stdout.toString()).toList(); - expect(const ListEquality<String>().equals(actualLines, expectedLines), isTrue); - }); -} diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart index 9c91e16d2767c..3a4d27477f6b3 100644 --- a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart @@ -112,7 +112,7 @@ void main() { expect(gradleProperties, exists); gradleProperties.writeAsStringSync(''' -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true'''); diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart index c567c08e4782b..2580c4d16ea8d 100644 --- a/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart @@ -46,7 +46,7 @@ void main() { final String pluginBuildGradle = pluginGradleFile.readAsStringSync(); // Bump up plugin ndkVersion to 21.4.7075529. - final RegExp androidNdkVersionRegExp = RegExp(r'ndkVersion (\"[0-9\.]+\"|flutter.ndkVersion)'); + final RegExp androidNdkVersionRegExp = RegExp(r'ndkVersion (\"[0-9\.]+\"|flutter.ndkVersion|android.ndkVersion)'); final String newPluginGradleFile = pluginBuildGradle.replaceAll(androidNdkVersionRegExp, 'ndkVersion "21.4.7075529"'); expect(newPluginGradleFile, contains('21.4.7075529')); pluginGradleFile.writeAsStringSync(newPluginGradleFile); diff --git a/packages/flutter_tools/test/integration.shard/batch_entrypoint_test.dart b/packages/flutter_tools/test/integration.shard/batch_entrypoint_test.dart new file mode 100644 index 0000000000000..233f978a3ca36 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/batch_entrypoint_test.dart @@ -0,0 +1,109 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/io.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +final String flutterRootPath = getFlutterRoot(); +final Directory flutterRoot = fileSystem.directory(flutterRootPath); + +Future<void> main() async { + // Regression test for https://github.com/flutter/flutter/issues/132592 + test('flutter/bin/dart updates the Dart SDK without hanging', () async { + // Run the Dart entrypoint once to ensure the Dart SDK is downloaded. + await runDartBatch(); + + expect(dartSdkStamp.existsSync(), true); + + // Remove the Dart SDK stamp and run the Dart entrypoint again to trigger + // the Dart SDK update. + dartSdkStamp.deleteSync(); + final Future<String> runFuture = runDartBatch(); + final Timer timer = Timer(const Duration(minutes: 5), () { + // This print is useful for people debugging this test. Normally we would + // avoid printing in a test but this is an exception because it's useful + // ambient information. + // ignore: avoid_print + print( + 'The Dart batch entrypoint did not complete after 5 minutes. ' + 'Historically this is a sign that 7-Zip zip extraction is waiting for ' + 'the user to confirm they would like to overwrite files. ' + "This likely means the test isn't a flake and will fail. " + 'See: https://github.com/flutter/flutter/issues/132592' + ); + }); + + final String output = await runFuture; + timer.cancel(); + + // Check the Dart SDK was re-downloaded and extracted. + // If 7-Zip is installed, unexpected overwrites causes this to hang. + // If 7-Zip is not installed, unexpected overwrites results in error messages. + // See: https://github.com/flutter/flutter/issues/132592 + expect(dartSdkStamp.existsSync(), true); + expect(output, contains('Downloading Dart SDK from Flutter engine ...')); + // Do not assert on the exact unzipping method, as this could change on CI + expect(output, contains(RegExp(r'Expanding downloaded archive with (.*)...'))); + expect(output, isNot(contains('Use the -Force parameter' /* Luke */))); + }, + skip: !platform.isWindows); // [intended] Only Windows uses the batch entrypoint +} + +Future<String> runDartBatch() async { + String output = ''; + final Process process = await processManager.start( + <String>[ + dartBatch.path + ], + ); + final Future<Object?> stdoutFuture = process.stdout + .transform<String>(utf8.decoder) + .forEach((String str) { + output += str; + }); + final Future<Object?> stderrFuture = process.stderr + .transform<String>(utf8.decoder) + .forEach((String str) { + output += str; + }); + + // Wait for the output to complete + await Future.wait(<Future<Object?>>[stdoutFuture, stderrFuture]); + // Ensure child exited successfully + expect( + await process.exitCode, + 0, + reason: 'child process exited with code ${await process.exitCode}, and ' + 'output:\n$output', + ); + + // Check the Dart tool prints the expected output. + expect(output, contains('A command-line utility for Dart development.')); + expect(output, contains('Usage: dart <command|dart-file> [arguments]')); + + return output; +} + +// The executable batch entrypoint for the Dart binary. +File get dartBatch { + return flutterRoot + .childDirectory('bin') + .childFile('dart.bat') + .absolute; +} + +// The Dart SDK's stamp file. +File get dartSdkStamp { + return flutterRoot + .childDirectory('bin') + .childDirectory('cache') + .childFile('engine-dart-sdk.stamp') + .absolute; +} diff --git a/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart b/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart index d9419765f3ec6..ea59152b81ecd 100644 --- a/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart +++ b/packages/flutter_tools/test/integration.shard/break_on_framework_exceptions_test.dart @@ -637,7 +637,7 @@ class TestProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/build_macos_config_only_test.dart b/packages/flutter_tools/test/integration.shard/build_macos_config_only_test.dart index cb27c4025600d..6fd271c55aa15 100644 --- a/packages/flutter_tools/test/integration.shard/build_macos_config_only_test.dart +++ b/packages/flutter_tools/test/integration.shard/build_macos_config_only_test.dart @@ -36,13 +36,7 @@ void main() { ]; final ProcessResult firstRunResult = await processManager.run(buildCommand, workingDirectory: workingDirectory); - printOnFailure('Output of flutter build macOS:'); - final String firstRunStdout = firstRunResult.stdout.toString(); - printOnFailure('First run stdout: $firstRunStdout'); - printOnFailure('First run stderr: ${firstRunResult.stderr}'); - - expect(firstRunResult.exitCode, 0); - expect(firstRunStdout, contains('Running pod install')); + expect(firstRunResult, const ProcessResultMatcher(stdoutPattern: 'Running pod install')); final File generatedConfig = fileSystem.file(fileSystem.path.join( workingDirectory, @@ -73,10 +67,7 @@ void main() { // Run again with no changes. final ProcessResult secondRunResult = await processManager.run(buildCommand, workingDirectory: workingDirectory); - final String secondRunStdout = secondRunResult.stdout.toString(); - printOnFailure('Second run stdout: $secondRunStdout'); - printOnFailure('Second run stderr: ${secondRunResult.stderr}'); - expect(secondRunResult.exitCode, 0); + expect(secondRunResult, const ProcessResultMatcher()); }, skip: !platform.isMacOS); // [intended] macOS builds only work on macos. } diff --git a/packages/flutter_tools/test/integration.shard/command_output_test.dart b/packages/flutter_tools/test/integration.shard/command_output_test.dart index e693ff1fe59c7..60c34113dec4f 100644 --- a/packages/flutter_tools/test/integration.shard/command_output_test.dart +++ b/packages/flutter_tools/test/integration.shard/command_output_test.dart @@ -71,11 +71,12 @@ void main() { expect(result.stdout, contains('Shutdown hooks complete')); }); - testWithoutContext('flutter config contains all features', () async { + testWithoutContext('flutter config --list contains all features', () async { final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); final ProcessResult result = await processManager.run(<String>[ flutterBin, 'config', + '--list' ]); // contains all of the experiments in features.dart @@ -158,8 +159,10 @@ void main() { '--debug-url=http://127.0.0.1:3333*/', ], workingDirectory: helloWorld); - expect(result.exitCode, 1); - expect(result.stderr, contains('Invalid `--debug-url`: http://127.0.0.1:3333*/')); + expect( + result, + const ProcessResultMatcher(exitCode: 1, stderrPattern: 'Invalid `--debug-url`: http://127.0.0.1:3333*/'), + ); }); testWithoutContext('--debug-uri is an alias for --debug-url', () async { @@ -175,8 +178,14 @@ void main() { '--debug-uri=http://127.0.0.1:3333*/', // "uri" not "url" ], workingDirectory: helloWorld); - expect(result.exitCode, 1); - expect(result.stderr, contains('Invalid `--debug-url`: http://127.0.0.1:3333*/')); // _"url"_ not "uri"! + expect( + result, + const ProcessResultMatcher( + exitCode: 1, + // _"url"_ not "uri"! + stderrPattern: 'Invalid `--debug-url`: http://127.0.0.1:3333*/', + ), + ); }); testWithoutContext('will load bootstrap script before starting', () async { @@ -211,8 +220,10 @@ void main() { '--bundle-sksl-path=foo/bar/baz.json', // This file does not exist. ], workingDirectory: helloWorld); - expect(result.exitCode, 1); - expect(result.stderr, contains('No SkSL shader bundle found at foo/bar/baz.json')); + expect(result, const ProcessResultMatcher( + exitCode: 1, + stderrPattern: 'No SkSL shader bundle found at foo/bar/baz.json'), + ); }); testWithoutContext('flutter attach does not support --release', () async { @@ -257,7 +268,7 @@ void main() { 'json', ], workingDirectory: helloWorld); - expect(result.exitCode, 0); + expect(result, const ProcessResultMatcher()); expect(result.stderr, isEmpty); }); } diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart index a717a6dfdb72d..8de1a2bddf638 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/globals.dart' as globals; import '../../src/common.dart'; import '../test_data/basic_project.dart'; import '../test_data/compile_error_project.dart'; +import '../test_data/project.dart'; import '../test_utils.dart'; import 'test_client.dart'; import 'test_server.dart'; @@ -187,13 +188,18 @@ void main() { expect(output, contains('Exited (1)')); }); - /// Helper that tests exception output in either debug or noDebug mode. - Future<void> testExceptionOutput({required bool noDebug}) async { - final BasicProjectThatThrows project = BasicProjectThatThrows(); + group('structured errors', () { + /// Helper that runs [project] and collects the output. + /// + /// Line and column numbers are replaced with "1" to avoid fragile tests. + Future<String> getExceptionOutput( + Project project, { + required bool noDebug, + required bool ansiColors, + }) async { await project.setUpIn(tempDir); - final List<OutputEventBody> outputEvents = - await dap.client.collectAllOutput(launch: () { + final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(launch: () { // Terminate the app after we see the exception because otherwise // it will keep running and `collectAllOutput` won't end. dap.client.output @@ -203,23 +209,85 @@ void main() { noDebug: noDebug, cwd: project.dir.path, toolArgs: <String>['-d', 'flutter-tester'], + allowAnsiColorOutput: ansiColors, ); }); - final String output = _uniqueOutputLines(outputEvents); - final List<String> outputLines = output.split('\n'); - expect( outputLines, containsAllInOrder(<String>[ - '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════', - 'The following _Exception was thrown building App(dirty):', - 'Exception: c', - 'The relevant error-causing widget was:', - ])); - expect(output, contains('App:${Uri.file(project.dir.path)}/lib/main.dart:24:12')); - } + String output = _uniqueOutputLines(outputEvents); + + // Replace out any line/columns to make tests less fragile. + output = output.replaceAll(RegExp(r'\.dart:\d+:\d+'), '.dart:1:1'); + + return output; + } + + testWithoutContext('correctly outputs exceptions in debug mode', () async { + final BasicProjectThatThrows project = BasicProjectThatThrows(); + final String output = await getExceptionOutput(project, noDebug: false, ansiColors: false); - testWithoutContext('correctly outputs exceptions in debug mode', () => testExceptionOutput(noDebug: false)); + expect( + output, + contains(''' +════════ Exception caught by widgets library ═══════════════════════════════════ +The following _Exception was thrown building App(dirty): +Exception: c - testWithoutContext('correctly outputs exceptions in noDebug mode', () => testExceptionOutput(noDebug: true)); +The relevant error-causing widget was: + App App:${Uri.file(project.dir.path)}/lib/main.dart:1:1'''), + ); + }); + + testWithoutContext('correctly outputs colored exceptions when supported', () async { + final BasicProjectThatThrows project = BasicProjectThatThrows(); + final String output = await getExceptionOutput(project, noDebug: false, ansiColors: true); + + // Frames in the stack trace that are the users own code will be unformatted, but + // frames from the framework are faint (starting with `\x1B[2m`). + + expect( + output, + contains(''' +════════ Exception caught by widgets library ═══════════════════════════════════ +The following _Exception was thrown building App(dirty): +Exception: c + +The relevant error-causing widget was: + App App:${Uri.file(project.dir.path)}/lib/main.dart:1:1 + +When the exception was thrown, this was the stack: +#0 c (package:test/main.dart:1:1) + ^ source: package:test/main.dart +#1 App.build (package:test/main.dart:1:1) + ^ source: package:test/main.dart +\x1B[2m#2 StatelessElement.build (package:flutter/src/widgets/framework.dart:1:1)\x1B[0m + ^ source: package:flutter/src/widgets/framework.dart +\x1B[2m#3 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:1:1)\x1B[0m + ^ source: package:flutter/src/widgets/framework.dart'''), + ); + }); + + testWithoutContext('correctly outputs exceptions in noDebug mode', () async { + final BasicProjectThatThrows project = BasicProjectThatThrows(); + final String output = await getExceptionOutput(project, noDebug: true, ansiColors: false); + + // When running in noDebug mode, we don't get the Flutter.Error event so + // we get the basic Flutter-formatted version of the error. + expect( + output, + contains(''' +══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ +The following _Exception was thrown building App(dirty): +Exception: c + +The relevant error-causing widget was: + App'''), + ); + expect( + output, + contains('App:${Uri.file(project.dir.path)}/lib/main.dart:1:1'), + ); + }); + }); testWithoutContext('can hot reload', () async { final BasicProject project = BasicProject(); @@ -630,10 +698,19 @@ void main() { /// Extracts the output from a set of [OutputEventBody], removing any /// adjacent duplicates and combining into a single string. +/// +/// If the output event contains a [Source], the name will be shown on the +/// following line indented and prefixed with `^ source:`. String _uniqueOutputLines(List<OutputEventBody> outputEvents) { String? lastItem; return outputEvents - .map((OutputEventBody e) => e.output) + .map((OutputEventBody e) { + final String output = e.output; + final Source? source = e.source; + return source != null + ? '$output ^ source: ${source.name}\n' + : output; + }) .where((String output) { // Skip the item if it's the same as the previous one. final bool isDupe = output == lastItem; diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart index 8857d4dd2dfb2..2e211f075ad34 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart @@ -155,6 +155,7 @@ class DapTestClient { String? cwd, bool? noDebug, List<String>? additionalProjectPaths, + bool? allowAnsiColorOutput, bool? debugSdkLibraries, bool? debugExternalPackageLibraries, bool? evaluateGettersInDebugViews, @@ -169,6 +170,7 @@ class DapTestClient { args: args, toolArgs: toolArgs, additionalProjectPaths: additionalProjectPaths, + allowAnsiColorOutput: allowAnsiColorOutput, debugSdkLibraries: debugSdkLibraries, debugExternalPackageLibraries: debugExternalPackageLibraries, evaluateGettersInDebugViews: evaluateGettersInDebugViews, diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart index f92da208d4100..49ca2cebf3f13 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_server.dart @@ -19,7 +19,7 @@ abstract class DapTestServer { Future<void> stop(); StreamSink<List<int>> get sink; Stream<List<int>> get stream; - Function(String message)? onStderrOutput; + void Function(String message)? onStderrOutput; } /// An instance of a DAP server running in-process (to aid debugging). @@ -83,7 +83,7 @@ class OutOfProcessDapTestServer extends DapTestServer { .listen((String error) { logger?.call(error); if (!_isShuttingDown) { - final Function(String message)? stderrHandler = onStderrOutput; + final void Function(String message)? stderrHandler = onStderrOutput; if (stderrHandler != null) { stderrHandler(error); } else { diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart index 35147c6dfc934..baa66bc630a33 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_support.dart @@ -31,7 +31,7 @@ final bool useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true' /// Service traffic (wrapped in a custom 'dart.log' event). final bool verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true'; -const String endOfErrorOutputMarker = '════════════════════════════════════════════════════════════════════════════════════════════════════'; +const String endOfErrorOutputMarker = '════════════════════════════════════════════════════════════════════════════════'; /// Expects the lines in [actual] to match the relevant matcher in [expected], /// ignoring differences in line endings and trailing whitespace. diff --git a/packages/flutter_tools/test/integration.shard/deferred_components_test.dart b/packages/flutter_tools/test/integration.shard/deferred_components_test.dart index 6cf038b5a0a47..f01ea24bf5976 100644 --- a/packages/flutter_tools/test/integration.shard/deferred_components_test.dart +++ b/packages/flutter_tools/test/integration.shard/deferred_components_test.dart @@ -39,10 +39,7 @@ void main() { '--target-platform=android-arm64', ], workingDirectory: tempDir.path); - printOnFailure('stdout:\n${result.stdout}'); - printOnFailure('stderr:\n${result.stderr}'); - expect(result.exitCode, 0); - expect(result.stdout.toString(), contains('app-release.aab')); + expect(result, const ProcessResultMatcher(stdoutPattern: 'app-release.aab')); expect(result.stdout.toString(), contains('Deferred components prebuild validation passed.')); expect(result.stdout.toString(), contains('Deferred components gen_snapshot validation passed.')); @@ -106,7 +103,7 @@ void main() { expect(archive.findFile('component1/assets/flutter_assets/test_assets/asset2.txt') != null, true); expect(archive.findFile('base/assets/flutter_assets/test_assets/asset1.txt') != null, true); - expect(result.exitCode, 0); + expect(result, const ProcessResultMatcher()); }); testWithoutContext('simple build appbundle no-deferred-components succeeds', () async { @@ -121,11 +118,9 @@ void main() { '--no-deferred-components', ], workingDirectory: tempDir.path); - printOnFailure('stdout:\n${result.stdout}'); - printOnFailure('stderr:\n${result.stderr}'); - expect(result.stdout.toString().contains('app-release.aab'), true); - expect(result.stdout.toString().contains('Deferred components prebuild validation passed.'), false); - expect(result.stdout.toString().contains('Deferred components gen_snapshot validation passed.'), false); + expect(result, const ProcessResultMatcher(stdoutPattern: 'app-release.aab')); + expect(result.stdout.toString(), isNot(contains('Deferred components prebuild validation passed.'))); + expect(result.stdout.toString(), isNot(contains('Deferred components gen_snapshot validation passed.'))); final String line = result.stdout.toString() .split('\n') @@ -153,8 +148,6 @@ void main() { expect(archive.findFile('component1/assets/flutter_assets/test_assets/asset2.txt') != null, false); expect(archive.findFile('base/assets/flutter_assets/test_assets/asset2.txt') != null, true); expect(archive.findFile('base/assets/flutter_assets/test_assets/asset1.txt') != null, true); - - expect(result.exitCode, 0); }); testWithoutContext('simple build appbundle mismatched golden no-validate-deferred-components succeeds', () async { @@ -169,14 +162,13 @@ void main() { '--no-validate-deferred-components', ], workingDirectory: tempDir.path); + expect(result, const ProcessResultMatcher(stdoutPattern: 'app-release.aab')); printOnFailure('stdout:\n${result.stdout}'); printOnFailure('stderr:\n${result.stderr}'); - expect(result.stdout.toString().contains('app-release.aab'), true); - expect(result.stdout.toString().contains('Deferred components prebuild validation passed.'), false); - expect(result.stdout.toString().contains('Deferred components gen_snapshot validation passed.'), false); - - expect(result.stdout.toString().contains('New loading units were found:'), false); - expect(result.stdout.toString().contains('Previously existing loading units no longer exist:'), false); + expect(result.stdout.toString(), isNot(contains('Deferred components prebuild validation passed.'))); + expect(result.stdout.toString(), isNot(contains('Deferred components gen_snapshot validation passed.'))); + expect(result.stdout.toString(), isNot(contains('New loading units were found:'))); + expect(result.stdout.toString(), isNot(contains('Previously existing loading units no longer exist:'))); final String line = result.stdout.toString() .split('\n') @@ -202,8 +194,6 @@ void main() { expect(archive.findFile('component1/assets/flutter_assets/test_assets/asset2.txt') != null, true); expect(archive.findFile('base/assets/flutter_assets/test_assets/asset1.txt') != null, true); - - expect(result.exitCode, 0); }); testWithoutContext('simple build appbundle missing android dynamic feature module fails', () async { @@ -217,16 +207,15 @@ void main() { 'appbundle', ], workingDirectory: tempDir.path); - expect(result.stdout.toString().contains('app-release.aab'), false); - expect(result.stdout.toString().contains('Deferred components prebuild validation passed.'), false); - expect(result.stdout.toString().contains('Deferred components gen_snapshot validation passed.'), false); + expect(result, const ProcessResultMatcher(exitCode: 1, stdoutPattern: 'Newly generated android files:')); + + expect(result.stdout.toString(), isNot(contains('app-release.aab'))); + expect(result.stdout.toString(), isNot(contains('Deferred components prebuild validation passed.'))); + expect(result.stdout.toString(), isNot(contains('Deferred components gen_snapshot validation passed.'))); - expect(result.stdout.toString(), contains('Newly generated android files:')); final String pathSeparator = fileSystem.path.separator; expect(result.stdout.toString(), contains('build${pathSeparator}android_deferred_components_setup_files${pathSeparator}component1${pathSeparator}build.gradle')); expect(result.stdout.toString(), contains('build${pathSeparator}android_deferred_components_setup_files${pathSeparator}component1${pathSeparator}src${pathSeparator}main${pathSeparator}AndroidManifest.xml')); - - expect(result.exitCode, 1); }); testWithoutContext('simple build appbundle missing golden fails', () async { @@ -240,16 +229,15 @@ void main() { 'appbundle', ], workingDirectory: tempDir.path); - expect(result.stdout.toString().contains('app-release.aab'), false); - expect(result.stdout.toString().contains('Deferred components prebuild validation passed.'), true); - expect(result.stdout.toString().contains('Deferred components gen_snapshot validation passed.'), false); + expect(result, const ProcessResultMatcher(exitCode: 1)); + expect(result.stdout.toString(), isNot(contains('app-release.aab'))); + expect(result.stdout.toString(), contains('Deferred components prebuild validation passed.')); + expect(result.stdout.toString(), isNot(contains('Deferred components gen_snapshot validation passed.'))); expect(result.stdout.toString(), contains('New loading units were found:')); expect(result.stdout.toString(), contains('- package:test/deferred_library.dart')); - expect(result.stdout.toString().contains('Previously existing loading units no longer exist:'), false); - - expect(result.exitCode, 1); + expect(result.stdout.toString(), isNot(contains('Previously existing loading units no longer exist:'))); }); testWithoutContext('simple build appbundle mismatched golden fails', () async { @@ -263,9 +251,15 @@ void main() { 'appbundle', ], workingDirectory: tempDir.path); - expect(result.stdout.toString().contains('app-release.aab'), false); - expect(result.stdout.toString().contains('Deferred components prebuild validation passed.'), true); - expect(result.stdout.toString().contains('Deferred components gen_snapshot validation passed.'), false); + expect( + result, + const ProcessResultMatcher( + exitCode: 1, + stdoutPattern: 'Deferred components prebuild validation passed.', + ), + ); + expect(result.stdout.toString(), isNot(contains('app-release.aab'))); + expect(result.stdout.toString(), isNot(contains('Deferred components gen_snapshot validation passed.'))); expect(result.stdout.toString(), contains('New loading units were found:')); expect(result.stdout.toString(), contains('- package:test/deferred_library.dart')); @@ -275,6 +269,5 @@ void main() { expect(result.stdout.toString(), contains('This loading unit check will not fail again on the next build attempt')); - expect(result.exitCode, 1); }); } diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_windows_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_windows_test.dart index c90a940e92d5d..d238bb226a75e 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_build_windows_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_build_windows_test.dart @@ -45,6 +45,7 @@ void main() { projectRoot.path, 'build', 'windows', + 'x64', 'runner', 'Release', )); diff --git a/packages/flutter_tools/test/integration.shard/native_assets_test.dart b/packages/flutter_tools/test/integration.shard/native_assets_test.dart new file mode 100644 index 0000000000000..3456c6a224b6f --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/native_assets_test.dart @@ -0,0 +1,407 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This test exercises the embedding of the native assets mapping in dill files. +// An initial dill file is created by `flutter assemble` and used for running +// the application. This dill must contain the mapping. +// When doing hot reload, this mapping must stay in place. +// When doing a hot restart, a new dill file is pushed. This dill file must also +// contain the native assets mapping. +// When doing a hot reload, this mapping must stay in place. + +@Timeout(Duration(minutes: 10)) +library; + +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../src/common.dart'; +import 'test_utils.dart' show fileSystem, platform; +import 'transition_test_utils.dart'; + +final String hostOs = platform.operatingSystem; + +final List<String> devices = <String>[ + 'flutter-tester', + hostOs, +]; + +final List<String> buildSubcommands = <String>[ + hostOs, + if (hostOs == 'macos') 'ios', +]; + +final List<String> add2appBuildSubcommands = <String>[ + if (hostOs == 'macos') ...<String>[ + 'macos-framework', + 'ios-framework', + ], +]; + +/// The build modes to target for each flutter command that supports passing +/// a build mode. +/// +/// The flow of compiling kernel as well as bundling dylibs can differ based on +/// build mode, so we should cover this. +const List<String> buildModes = <String>[ + 'debug', + 'profile', + 'release', +]; + +const String packageName = 'package_with_native_assets'; + +const String exampleAppName = '${packageName}_example'; + +void main() { + if (!platform.isMacOS && !platform.isLinux && !platform.isWindows) { + // TODO(dacoharkes): Implement Fuchsia. https://github.com/flutter/flutter/issues/129757 + return; + } + + setUpAll(() { + processManager.runSync(<String>[ + flutterBin, + 'config', + '--enable-native-assets', + ]); + }); + + for (final String device in devices) { + for (final String buildMode in buildModes) { + if (device == 'flutter-tester' && buildMode != 'debug') { + continue; + } + final String hotReload = buildMode == 'debug' ? ' hot reload and hot restart' : ''; + testWithoutContext('flutter run$hotReload with native assets $device $buildMode', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessTestResult result = await runFlutter( + <String>[ + 'run', + '-d$device', + '--$buildMode', + ], + exampleDirectory.path, + <Transition>[ + Multiple(<Pattern>[ + 'Flutter run key commands.', + ], handler: (String line) { + if (buildMode == 'debug') { + // Do a hot reload diff on the initial dill file. + return 'r'; + } else { + // No hot reload and hot restart in release mode. + return 'q'; + } + }), + if (buildMode == 'debug') ...<Transition>[ + Barrier( + 'Performing hot reload...'.padRight(progressMessageWidth), + logging: true, + ), + Multiple(<Pattern>[ + RegExp('Reloaded .*'), + ], handler: (String line) { + // Do a hot restart, pushing a new complete dill file. + return 'R'; + }), + Barrier('Performing hot restart...'.padRight(progressMessageWidth)), + Multiple(<Pattern>[ + RegExp('Restarted application .*'), + ], handler: (String line) { + // Do another hot reload, pushing a diff to the second dill file. + return 'r'; + }), + Barrier( + 'Performing hot reload...'.padRight(progressMessageWidth), + logging: true, + ), + Multiple(<Pattern>[ + RegExp('Reloaded .*'), + ], handler: (String line) { + return 'q'; + }), + ], + const Barrier('Application finished.'), + ], + logging: false, + ); + if (result.exitCode != 0) { + throw Exception('flutter run failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + final String stdout = result.stdout.join('\n'); + // Check that we did not fail to resolve the native function in the + // dynamic library. + expect(stdout, isNot(contains("Invalid argument(s): Couldn't resolve native function 'sum'"))); + // And also check that we did not have any other exceptions that might + // shadow the exception we would have gotten. + expect(stdout, isNot(contains('EXCEPTION CAUGHT BY WIDGETS LIBRARY'))); + + if (device == 'macos') { + expectDylibIsBundledMacOS(exampleDirectory, buildMode); + } else if (device == 'linux') { + expectDylibIsBundledLinux(exampleDirectory, buildMode); + } else if (device == 'windows') { + expectDylibIsBundledWindows(exampleDirectory, buildMode); + } + if (device == hostOs) { + expectCCompilerIsConfigured(exampleDirectory); + } + }); + }); + } + } + + testWithoutContext('flutter test with native assets', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + + final ProcessTestResult result = await runFlutter( + <String>[ + 'test', + ], + packageDirectory.path, + <Transition>[ + Barrier(RegExp('.* All tests passed!')), + ], + logging: false, + ); + if (result.exitCode != 0) { + throw Exception('flutter test failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + }); + }); + + for (final String buildSubcommand in buildSubcommands) { + for (final String buildMode in buildModes) { + testWithoutContext('flutter build $buildSubcommand with native assets $buildMode', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + <String>[ + flutterBin, + 'build', + buildSubcommand, + '--$buildMode', + if (buildSubcommand == 'ios') '--no-codesign', + ], + workingDirectory: exampleDirectory.path, + ); + if (result.exitCode != 0) { + throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + if (buildSubcommand == 'macos') { + expectDylibIsBundledMacOS(exampleDirectory, buildMode); + } else if (buildSubcommand == 'ios') { + expectDylibIsBundledIos(exampleDirectory, buildMode); + } else if (buildSubcommand == 'linux') { + expectDylibIsBundledLinux(exampleDirectory, buildMode); + } else if (buildSubcommand == 'windows') { + expectDylibIsBundledWindows(exampleDirectory, buildMode); + } + expectCCompilerIsConfigured(exampleDirectory); + }); + }); + } + + // This could be an hermetic unit test if the native_assets_builder + // could mock process runs and file system. + // https://github.com/dart-lang/native/issues/90. + testWithoutContext('flutter build $buildSubcommand error on static libraries', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final File buildDotDart = packageDirectory.childFile('build.dart'); + final String buildDotDartContents = await buildDotDart.readAsString(); + // Overrides the build to output static libraries. + final String buildDotDartContentsNew = buildDotDartContents.replaceFirst( + 'final buildConfig = await BuildConfig.fromArgs(args);', + r''' + final buildConfig = await BuildConfig.fromArgs([ + '-D${LinkModePreference.configKey}=${LinkModePreference.static}', + ...args, + ]); +''', + ); + expect(buildDotDartContentsNew, isNot(buildDotDartContents)); + await buildDotDart.writeAsString(buildDotDartContentsNew); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + <String>[ + flutterBin, + 'build', + buildSubcommand, + if (buildSubcommand == 'ios') '--no-codesign', + if (buildSubcommand == 'windows') '-v' // Requires verbose mode for error. + ], + workingDirectory: exampleDirectory.path, + ); + expect(result.exitCode, isNot(0)); + expect( + (result.stdout as String) + (result.stderr as String), + contains('link mode set to static, but this is not yet supported'), + ); + }); + }); + } + + for (final String add2appBuildSubcommand in add2appBuildSubcommands) { + testWithoutContext('flutter build $add2appBuildSubcommand with native assets', () async { + await inTempDir((Directory tempDirectory) async { + final Directory packageDirectory = await createTestProject(packageName, tempDirectory); + final Directory exampleDirectory = packageDirectory.childDirectory('example'); + + final ProcessResult result = processManager.runSync( + <String>[ + flutterBin, + 'build', + add2appBuildSubcommand, + ], + workingDirectory: exampleDirectory.path, + ); + if (result.exitCode != 0) { + throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + for (final String buildMode in buildModes) { + expectDylibIsBundledWithFrameworks(exampleDirectory, buildMode, add2appBuildSubcommand.replaceAll('-framework', '')); + } + expectCCompilerIsConfigured(exampleDirectory); + }); + }); + } +} + +/// For `flutter build` we can't easily test whether running the app works. +/// Check that we have the dylibs in the app. +void expectDylibIsBundledMacOS(Directory appDirectory, String buildMode) { + final Directory appBundle = appDirectory.childDirectory('build/$hostOs/Build/Products/${buildMode.upperCaseFirst()}/$exampleAppName.app'); + expect(appBundle, exists); + final Directory dylibsFolder = appBundle.childDirectory('Contents/Frameworks'); + expect(dylibsFolder, exists); + final File dylib = dylibsFolder.childFile(OS.macOS.dylibFileName(packageName)); + expect(dylib, exists); +} + +void expectDylibIsBundledIos(Directory appDirectory, String buildMode) { + final Directory appBundle = appDirectory.childDirectory('build/ios/${buildMode.upperCaseFirst()}-iphoneos/Runner.app'); + expect(appBundle, exists); + final Directory dylibsFolder = appBundle.childDirectory('Frameworks'); + expect(dylibsFolder, exists); + final File dylib = dylibsFolder.childFile(OS.iOS.dylibFileName(packageName)); + expect(dylib, exists); +} + +/// Checks that dylibs are bundled. +/// +/// Sample path: build/linux/x64/release/bundle/lib/libmy_package.so +void expectDylibIsBundledLinux(Directory appDirectory, String buildMode) { + // Linux does not support cross compilation, so always only check current architecture. + final String architecture = Architecture.current.dartPlatform; + final Directory appBundle = appDirectory + .childDirectory('build') + .childDirectory(hostOs) + .childDirectory(architecture) + .childDirectory(buildMode) + .childDirectory('bundle'); + expect(appBundle, exists); + final Directory dylibsFolder = appBundle.childDirectory('lib'); + expect(dylibsFolder, exists); + final File dylib = dylibsFolder.childFile(OS.linux.dylibFileName(packageName)); + expect(dylib, exists); +} + +/// Checks that dylibs are bundled. +/// +/// Sample path: build\windows\x64\runner\Debug\my_package_example.exe +void expectDylibIsBundledWindows(Directory appDirectory, String buildMode) { + // Linux does not support cross compilation, so always only check current architecture. + final String architecture = Architecture.current.dartPlatform; + final Directory appBundle = appDirectory + .childDirectory('build') + .childDirectory(hostOs) + .childDirectory(architecture) + .childDirectory('runner') + .childDirectory(buildMode.upperCaseFirst()); + expect(appBundle, exists); + final File dylib = appBundle.childFile(OS.windows.dylibFileName(packageName)); + expect(dylib, exists); +} + +/// For `flutter build` we can't easily test whether running the app works. +/// Check that we have the dylibs in the app. +void expectDylibIsBundledWithFrameworks(Directory appDirectory, String buildMode, String os) { + final Directory frameworksFolder = appDirectory.childDirectory('build/$os/framework/${buildMode.upperCaseFirst()}'); + expect(frameworksFolder, exists); + final File dylib = frameworksFolder.childFile(OS.macOS.dylibFileName(packageName)); + expect(dylib, exists); +} + +/// Check that the native assets are built with the C Compiler that Flutter uses. +/// +/// This inspects the build configuration to see if the C compiler was configured. +void expectCCompilerIsConfigured(Directory appDirectory) { + final Directory nativeAssetsBuilderDir = appDirectory.childDirectory('.dart_tool/native_assets_builder/'); + for (final Directory subDir in nativeAssetsBuilderDir.listSync().whereType<Directory>()) { + final File config = subDir.childFile('config.yaml'); + expect(config, exists); + final String contents = config.readAsStringSync(); + // Dry run does not pass compiler info. + if (contents.contains('dry_run: true')) { + continue; + } + expect(contents, contains('cc: ')); + } +} + +extension on String { + String upperCaseFirst() { + return replaceFirst(this[0], this[0].toUpperCase()); + } +} + +Future<Directory> createTestProject(String packageName, Directory tempDirectory) async { + final ProcessResult result = processManager.runSync( + <String>[ + flutterBin, + 'create', + '--template=package_ffi', + packageName, + ], + workingDirectory: tempDirectory.path, + ); + + if (result.exitCode != 0) { + throw Exception('flutter create failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}'); + } + + final Directory packageDirectory = tempDirectory.childDirectory(packageName); + + // No platform-specific boilerplate files. + expect(packageDirectory.childDirectory('android/'), isNot(exists)); + expect(packageDirectory.childDirectory('ios/'), isNot(exists)); + expect(packageDirectory.childDirectory('linux/'), isNot(exists)); + expect(packageDirectory.childDirectory('macos/'), isNot(exists)); + expect(packageDirectory.childDirectory('windows/'), isNot(exists)); + + return packageDirectory; +} + +Future<void> inTempDir(Future<void> Function(Directory tempDirectory) fun) async { + final Directory tempDirectory = fileSystem.directory(fileSystem.systemTempDirectory.createTempSync().resolveSymbolicLinksSync()); + try { + await fun(tempDirectory); + } finally { + tryToDelete(tempDirectory); + } +} diff --git a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart index e9071b5a5573c..8cc4fff7e374e 100644 --- a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart +++ b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart @@ -26,310 +26,11 @@ @Tags(<String>['no-shuffle']) library; -import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'package:meta/meta.dart'; -import 'package:process/process.dart'; - import '../src/common.dart'; import 'test_utils.dart' show fileSystem; - -const ProcessManager processManager = LocalProcessManager(); -final String flutterRoot = getFlutterRoot(); -final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter'); - -void debugPrint(String message) { - // This is called to intentionally print debugging output when a test is - // either taking too long or has failed. - // ignore: avoid_print - print(message); -} - -typedef LineHandler = String? Function(String line); - -abstract class Transition { - const Transition({this.handler, this.logging}); - - /// Callback that is invoked when the transition matches. - /// - /// This should not throw, even if the test is failing. (For example, don't use "expect" - /// in these callbacks.) Throwing here would prevent the [runFlutter] function from running - /// to completion, which would leave zombie `flutter` processes around. - final LineHandler? handler; - - /// Whether to enable or disable logging when this transition is matched. - /// - /// The default value, null, leaves the logging state unaffected. - final bool? logging; - - bool matches(String line); - - @protected - bool lineMatchesPattern(String line, Pattern pattern) { - if (pattern is String) { - return line == pattern; - } - return line.contains(pattern); - } - - @protected - String describe(Pattern pattern) { - if (pattern is String) { - return '"$pattern"'; - } - if (pattern is RegExp) { - return '/${pattern.pattern}/'; - } - return '$pattern'; - } -} - -class Barrier extends Transition { - const Barrier(this.pattern, {super.handler, super.logging}); - final Pattern pattern; - - @override - bool matches(String line) => lineMatchesPattern(line, pattern); - - @override - String toString() => describe(pattern); -} - -class Multiple extends Transition { - Multiple(List<Pattern> patterns, { - super.handler, - super.logging, - }) : _originalPatterns = patterns, - patterns = patterns.toList(); - - final List<Pattern> _originalPatterns; - final List<Pattern> patterns; - - @override - bool matches(String line) { - for (int index = 0; index < patterns.length; index += 1) { - if (lineMatchesPattern(line, patterns[index])) { - patterns.removeAt(index); - break; - } - } - return patterns.isEmpty; - } - - @override - String toString() { - if (patterns.isEmpty) { - return '${_originalPatterns.map(describe).join(', ')} (all matched)'; - } - return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)'; - } -} - -class LogLine { - const LogLine(this.channel, this.stamp, this.message); - final String channel; - final String stamp; - final String message; - - bool get couldBeCrash => message.contains('Oops; flutter has exited unexpectedly:'); - - @override - String toString() => '$stamp $channel: $message'; - - void printClearly() { - debugPrint('$stamp $channel: ${clarify(message)}'); - } - - static String clarify(String line) { - return line.runes.map<String>((int rune) { - if (rune >= 0x20 && rune <= 0x7F) { - return String.fromCharCode(rune); - } - switch (rune) { - case 0x00: return '<NUL>'; - case 0x07: return '<BEL>'; - case 0x08: return '<TAB>'; - case 0x09: return '<BS>'; - case 0x0A: return '<LF>'; - case 0x0D: return '<CR>'; - } - return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>'; - }).join(); - } -} - -class ProcessTestResult { - const ProcessTestResult(this.exitCode, this.logs); - final int exitCode; - final List<LogLine> logs; - - List<String> get stdout { - return logs - .where((LogLine log) => log.channel == 'stdout') - .map<String>((LogLine log) => log.message) - .toList(); - } - - List<String> get stderr { - return logs - .where((LogLine log) => log.channel == 'stderr') - .map<String>((LogLine log) => log.message) - .toList(); - } - - @override - String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n'; -} - -Future<ProcessTestResult> runFlutter( - List<String> arguments, - String workingDirectory, - List<Transition> transitions, { - bool debug = false, - bool logging = true, - Duration expectedMaxDuration = const Duration(minutes: 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml. -}) async { - final Stopwatch clock = Stopwatch()..start(); - final Process process = await processManager.start( - <String>[flutterBin, ...arguments], - workingDirectory: workingDirectory, - ); - final List<LogLine> logs = <LogLine>[]; - int nextTransition = 0; - void describeStatus() { - if (transitions.isNotEmpty) { - debugPrint('Expected state transitions:'); - for (int index = 0; index < transitions.length; index += 1) { - debugPrint( - '${index.toString().padLeft(5)} ' - '${index < nextTransition ? 'ALREADY MATCHED ' : - index == nextTransition ? 'NOW WAITING FOR>' : - ' '} ${transitions[index]}'); - } - } - if (logs.isEmpty) { - debugPrint('So far nothing has been logged${ debug ? "" : "; use debug:true to print all output" }.'); - } else { - debugPrint('Log${ debug ? "" : " (only contains logged lines; use debug:true to print all output)" }:'); - for (final LogLine log in logs) { - log.printClearly(); - } - } - } - bool streamingLogs = false; - Timer? timeout; - void processTimeout() { - if (!streamingLogs) { - streamingLogs = true; - if (!debug) { - debugPrint('Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).'); - } - describeStatus(); - debugPrint('(streaming all logs from this point on...)'); - } else { - debugPrint('(taking a long time...)'); - } - } - String stamp() => '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]'; - void processStdout(String line) { - final LogLine log = LogLine('stdout', stamp(), line); - if (logging) { - logs.add(log); - } - if (streamingLogs) { - log.printClearly(); - } - if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) { - if (streamingLogs) { - debugPrint('(matched ${transitions[nextTransition]})'); - } - if (transitions[nextTransition].logging != null) { - if (!logging && transitions[nextTransition].logging!) { - logs.add(log); - } - logging = transitions[nextTransition].logging!; - if (streamingLogs) { - if (logging) { - debugPrint('(enabled logging)'); - } else { - debugPrint('(disabled logging)'); - } - } - } - if (transitions[nextTransition].handler != null) { - final String? command = transitions[nextTransition].handler!(line); - if (command != null) { - final LogLine inLog = LogLine('stdin', stamp(), command); - logs.add(inLog); - if (streamingLogs) { - inLog.printClearly(); - } - process.stdin.write(command); - } - } - nextTransition += 1; - timeout?.cancel(); - timeout = Timer(expectedMaxDuration ~/ 5, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. - } - } - void processStderr(String line) { - final LogLine log = LogLine('stdout', stamp(), line); - logs.add(log); - if (streamingLogs) { - log.printClearly(); - } - } - if (debug) { - processTimeout(); - } else { - timeout = Timer(expectedMaxDuration ~/ 2, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. - } - process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStdout); - process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStderr); - unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { // This is a failure timeout, must not be short. - debugPrint('${stamp()} (process is not quitting, trying to send a "q" just in case that helps)'); - debugPrint('(a functional test should never reach this point)'); - final LogLine inLog = LogLine('stdin', stamp(), 'q'); - logs.add(inLog); - if (streamingLogs) { - inLog.printClearly(); - } - process.stdin.write('q'); - return -1; // discarded - }).then( - (int i) => i, - onError: (Object error) { - // ignore errors here, they will be reported on the next line - return -1; // discarded - }, - )); - final int exitCode = await process.exitCode; - if (streamingLogs) { - debugPrint('${stamp()} (process terminated with exit code $exitCode)'); - } - timeout?.cancel(); - if (nextTransition < transitions.length) { - debugPrint('The subprocess terminated before all the expected transitions had been matched.'); - if (logs.any((LogLine line) => line.couldBeCrash)) { - debugPrint('The subprocess may in fact have crashed. Check the stderr logs below.'); - } - debugPrint('The transition that we were hoping to see next but that we never saw was:'); - debugPrint('${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}'); - if (!streamingLogs) { - describeStatus(); - debugPrint('(process terminated with exit code $exitCode)'); - } - throw TestFailure('Missed some expected transitions.'); - } - if (streamingLogs) { - debugPrint('${stamp()} (completed execution successfully!)'); - } - return ProcessTestResult(exitCode, logs); -} - -const int progressMessageWidth = 64; +import 'transition_test_utils.dart'; void main() { testWithoutContext('flutter run writes and clears pidfile appropriately', () async { diff --git a/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart b/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart deleted file mode 100644 index 85ec32c59e08e..0000000000000 --- a/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_tools/src/base/file_system.dart'; - -import '../src/common.dart'; -import 'test_data/single_widget_reload_project.dart'; -import 'test_driver.dart'; -import 'test_utils.dart'; - -void main() { - late Directory tempDir; - final SingleWidgetReloadProject project = SingleWidgetReloadProject(); - late FlutterRunTestDriver flutter; - - setUp(() async { - tempDir = createResolvedTempDirectorySync('hot_reload_test.'); - await project.setUpIn(tempDir); - flutter = FlutterRunTestDriver(tempDir); - }); - - tearDown(() async { - await flutter.stop(); - tryToDelete(tempDir); - }); - - testWithoutContext('newly added code executes during hot reload with single widget reloads, but only invalidated widget', () async { - final StringBuffer stdout = StringBuffer(); - final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln); - await flutter.run(singleWidgetReloads: true); - project.uncommentHotReloadPrint(); - try { - await flutter.hotReload(); - expect(stdout.toString(), allOf( - contains('(((TICK 1)))'), - contains('(((((RELOAD WORKED)))))'), - // Does not invalidate parent widget, so second tick is not output. - isNot(contains('(((TICK 2)))'), - ))); - } finally { - await subscription.cancel(); - } - }); - - testWithoutContext('changes outside of the class body triggers a full reload', () async { - final StringBuffer stdout = StringBuffer(); - final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln); - await flutter.run(singleWidgetReloads: true); - project.modifyFunction(); - try { - await flutter.hotReload(); - expect(stdout.toString(), allOf( - contains('(((TICK 1)))'), - contains('(((TICK 2)))'), - )); - } finally { - await subscription.cancel(); - } - }); -} diff --git a/packages/flutter_tools/test/integration.shard/test_data/background_project.dart b/packages/flutter_tools/test/integration.shard/test_data/background_project.dart index 783004b55e674..fea73341f67b7 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/background_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/background_project.dart @@ -12,7 +12,7 @@ class BackgroundProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -63,7 +63,7 @@ class RepeatingBackgroundProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart index 93a2d090cf275..7db831fcf685a 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart @@ -10,7 +10,7 @@ class BasicProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -63,7 +63,7 @@ class BasicProjectThatThrows extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -120,7 +120,7 @@ class BasicProjectWithTimelineTraces extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -169,7 +169,7 @@ class BasicProjectWithFlutterGen extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -194,7 +194,7 @@ class BasicProjectWithUnaryMain extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter diff --git a/packages/flutter_tools/test/integration.shard/test_data/compile_error_project.dart b/packages/flutter_tools/test/integration.shard/test_data/compile_error_project.dart index 3232d7f44c76d..879c7f66b938a 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/compile_error_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/compile_error_project.dart @@ -10,7 +10,7 @@ class CompileErrorProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_config.dart b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_config.dart index 859cc69e31a37..f3377e04078c7 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_config.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_config.dart @@ -77,7 +77,7 @@ class DeferredComponentModule { apply plugin: "com.android.dynamic-feature" android { - compileSdkVersion 31 + compileSdkVersion 33 sourceSets { applicationVariants.all { variant -> @@ -88,7 +88,7 @@ class DeferredComponentModule { defaultConfig { minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart index 35a7c1cbc26e4..4a0ec4be1cd30 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart @@ -13,7 +13,7 @@ class DeferredComponentsProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -232,7 +232,7 @@ class BasicDeferredComponentsConfig extends DeferredComponentsConfig { @override String get androidGradleProperties => ''' - org.gradle.jvmargs=-Xmx1536M + org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true @@ -508,15 +508,6 @@ class BasicDeferredComponentsConfig extends DeferredComponentsConfig { android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> - <!-- Displays an Android View that continues showing the launch screen - Drawable until Flutter paints its first frame, then this splash - screen fades out. A splash screen is useful to avoid any visual - gap between the end of Android's launch screen and the painting of - Flutter's first frame. --> - <meta-data - android:name="io.flutter.embedding.android.SplashScreenDrawable" - android:resource="@drawable/launch_background" - /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> diff --git a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart index 68cc153f92fe9..3444979b77dcd 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart @@ -34,7 +34,7 @@ class GenL10nProject extends Project { final String pubspec = ''' name: test_l10n_project environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_const_project.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_const_project.dart index 355919fd90415..f51e2d8defa49 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_const_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_const_project.dart @@ -10,7 +10,7 @@ class HotReloadConstProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_project.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_project.dart index 7e188c0b2608a..643832696e110 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_project.dart @@ -12,7 +12,7 @@ class HotReloadProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset.dart index 27d3665968d02..4c2f8950aaf8e 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/hot_reload_with_asset.dart @@ -10,7 +10,7 @@ class HotReloadWithAssetProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/integration_tests_project.dart b/packages/flutter_tools/test/integration.shard/test_data/integration_tests_project.dart index a67f1bbc900ef..62a1313170d27 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/integration_tests_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/integration_tests_project.dart @@ -14,7 +14,7 @@ class IntegrationTestsProject extends Project implements TestsProject { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart b/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart index d4a3ca385986d..a5d6d77c5c55a 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart @@ -175,7 +175,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart index 5773666b73e2d..80ffcec941603 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart @@ -38,7 +38,7 @@ class MultidexProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -144,7 +144,7 @@ class MultidexProject extends Project { apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -163,7 +163,7 @@ class MultidexProject extends Project { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.multidextest2" minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -194,7 +194,7 @@ class MultidexProject extends Project { '''; String get androidGradleProperties => ''' - org.gradle.jvmargs=-Xmx1536M + org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/flutter_tools/test/integration.shard/test_data/project_with_early_error.dart b/packages/flutter_tools/test/integration.shard/test_data/project_with_early_error.dart index 6d37629f4c365..7ab284fcf2200 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/project_with_early_error.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/project_with_early_error.dart @@ -10,7 +10,7 @@ class ProjectWithEarlyError extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart b/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart deleted file mode 100644 index 2a48a090ea084..0000000000000 --- a/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import '../test_utils.dart'; -import 'project.dart'; - -class SingleWidgetReloadProject extends Project { - @override - final String pubspec = ''' - name: test - environment: - sdk: '>=3.0.0-0 <4.0.0' - - dependencies: - flutter: - sdk: flutter - '''; - - @override - final String main = r''' - import 'package:flutter/material.dart'; - import 'package:flutter/scheduler.dart'; - import 'package:flutter/services.dart'; - import 'package:flutter/widgets.dart'; - - void main() async { - WidgetsFlutterBinding.ensureInitialized(); - final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.resumed')!; - await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage('flutter/lifecycle', message, (_) { }); - runApp(MyApp()); - } - - int count = 1; - - class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - // PARENT WIDGET - - print('((((TICK $count))))'); - count += 1; - - return MaterialApp( - title: 'Flutter Demo', - home: SecondWidget(), - ); - } - } - - class SecondWidget extends StatelessWidget { - @override - Widget build(BuildContext context) { - // Do not remove the next line, it's uncommented by a test to verify that - // hot reloading worked: - // printHotReloadWorked(); - return Container(); - } - } - - void printHotReloadWorked() { - // The call to this function is uncommented by a test to verify that hot - // reloading worked. - print('(((((RELOAD WORKED)))))'); - } - '''; - - Uri get parentWidgetUri => mainDart; - int get parentWidgetLine => lineContaining(main, '// PARENT WIDGET'); - - void uncommentHotReloadPrint() { - final String newMainContents = main.replaceAll( - '// printHotReloadWorked();', - 'printHotReloadWorked();', - ); - writeFile( - fileSystem.path.join(dir.path, 'lib', 'main.dart'), - newMainContents, - writeFutureModifiedDate: true, - ); - } - - void modifyFunction() { - final String newMainContents = main.replaceAll( - '(((((RELOAD WORKED)))))', - '(((((RELOAD WORKED 2)))))', - ); - writeFile( - fileSystem.path.join(dir.path, 'lib', 'main.dart'), - newMainContents, - writeFutureModifiedDate: true, - ); - } -} diff --git a/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_project.dart b/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_project.dart index ecf1834be50cc..a8a8232870f7b 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/stateless_stateful_project.dart @@ -10,7 +10,7 @@ class HotReloadProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/stepping_project.dart b/packages/flutter_tools/test/integration.shard/test_data/stepping_project.dart index 87bc6a29c8129..9eb8e1325f384 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/stepping_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/stepping_project.dart @@ -9,7 +9,7 @@ class SteppingProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter @@ -65,7 +65,7 @@ class WebSteppingProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter diff --git a/packages/flutter_tools/test/integration.shard/test_data/test_project.dart b/packages/flutter_tools/test/integration.shard/test_data/test_project.dart index 436a86e009974..a87386ebc1a5f 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/test_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/test_project.dart @@ -10,7 +10,7 @@ class TestProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart index 45ee250104a30..2f7390d9f5fbc 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart @@ -13,7 +13,7 @@ class TestsProject extends Project { final String pubspec = ''' name: test environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart index 24a8e59575a09..6be229d2d5e91 100644 --- a/packages/flutter_tools/test/integration.shard/test_driver.dart +++ b/packages/flutter_tools/test/integration.shard/test_driver.dart @@ -90,7 +90,6 @@ abstract class FlutterTestDriver { List<String> arguments, { String? script, bool withDebugger = false, - bool singleWidgetReloads = false, }) async { final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); if (withDebugger) { @@ -114,8 +113,6 @@ abstract class FlutterTestDriver { environment: <String, String>{ 'FLUTTER_TEST': 'true', 'FLUTTER_WEB': 'true', - if (singleWidgetReloads) - 'FLUTTER_SINGLE_WIDGET_RELOAD': 'true', }, ); @@ -511,7 +508,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { bool chrome = false, bool expressionEvaluation = true, bool structuredErrors = false, - bool singleWidgetReloads = false, bool serveObservatory = false, String? script, List<String>? additionalCommandArgs, @@ -542,7 +538,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { startPaused: startPaused, pauseOnExceptions: pauseOnExceptions, script: script, - singleWidgetReloads: singleWidgetReloads, ); } @@ -551,7 +546,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { bool withDebugger = false, bool startPaused = false, bool pauseOnExceptions = false, - bool singleWidgetReloads = false, bool serveObservatory = false, List<String>? additionalCommandArgs, }) async { @@ -573,7 +567,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { withDebugger: withDebugger, startPaused: startPaused, pauseOnExceptions: pauseOnExceptions, - singleWidgetReloads: singleWidgetReloads, attachPort: port, ); } @@ -585,7 +578,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { bool withDebugger = false, bool startPaused = false, bool pauseOnExceptions = false, - bool singleWidgetReloads = false, int? attachPort, }) async { assert(!startPaused || withDebugger); @@ -593,7 +585,6 @@ class FlutterRunTestDriver extends FlutterTestDriver { args, script: script, withDebugger: withDebugger, - singleWidgetReloads: singleWidgetReloads, ); final Completer<void> prematureExitGuard = Completer<void>(); @@ -806,13 +797,11 @@ class FlutterTestTestDriver extends FlutterTestDriver { bool withDebugger = false, bool pauseOnExceptions = false, Future<void> Function()? beforeStart, - bool singleWidgetReloads = false, }) async { await super._setupProcess( args, script: script, withDebugger: withDebugger, - singleWidgetReloads: singleWidgetReloads, ); // Stash the PID so that we can terminate the VM more reliably than using diff --git a/packages/flutter_tools/test/integration.shard/test_test.dart b/packages/flutter_tools/test/integration.shard/test_test.dart index 86e0d11bdc2b3..c48d354c09331 100644 --- a/packages/flutter_tools/test/integration.shard/test_test.dart +++ b/packages/flutter_tools/test/integration.shard/test_test.dart @@ -335,7 +335,10 @@ Future<void> _testFile( reason: '"$testName" returned code ${exec.exitCode}\n\nstdout:\n' '${exec.stdout}\nstderr:\n${exec.stderr}', ); - final List<String> output = (exec.stdout as String).split('\n'); + List<String> output = (exec.stdout as String).split('\n'); + + output = _removeMacFontServerWarning(output); + if (output.first.startsWith('Waiting for another flutter command to release the startup lock...')) { output.removeAt(0); } @@ -398,6 +401,26 @@ Future<void> _testFile( } } +final RegExp _fontServerProtocolPattern = RegExp(r'flutter_tester.*Font server protocol version mismatch'); +final RegExp _unableToConnectToFontDaemonPattern = RegExp(r'flutter_tester.*XType: unable to make a connection to the font daemon!'); +final RegExp _xtFontStaticRegistryPattern = RegExp(r'flutter_tester.*XType: XTFontStaticRegistry is enabled as fontd is not available'); + +// https://github.com/flutter/flutter/issues/132990 +List<String> _removeMacFontServerWarning(List<String> output) { + return output.where((String line) { + if (_fontServerProtocolPattern.hasMatch(line)) { + return false; + } + if (_unableToConnectToFontDaemonPattern.hasMatch(line)) { + return false; + } + if (_xtFontStaticRegistryPattern.hasMatch(line)) { + return false; + } + return true; + }).toList(); +} + Future<ProcessResult> _runFlutterTest( String? testName, String workingDirectory, diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index 101828206776f..b13452209a059 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -67,6 +67,7 @@ Future<void> getPackages(String folder) async { } const String kLocalEngineEnvironment = 'FLUTTER_LOCAL_ENGINE'; +const String kLocalEngineHostEnvironment = 'FLUTTER_LOCAL_ENGINE_HOST'; const String kLocalEngineLocation = 'FLUTTER_LOCAL_ENGINE_SRC_PATH'; List<String> getLocalEngineArguments() { @@ -75,6 +76,8 @@ List<String> getLocalEngineArguments() { '--local-engine=${platform.environment[kLocalEngineEnvironment]}', if (platform.environment.containsKey(kLocalEngineLocation)) '--local-engine-src-path=${platform.environment[kLocalEngineLocation]}', + if (platform.environment.containsKey(kLocalEngineHostEnvironment)) + '--local-engine-host=${platform.environment[kLocalEngineHostEnvironment]}', ]; } diff --git a/packages/flutter_tools/test/integration.shard/tool_backend_test.dart b/packages/flutter_tools/test/integration.shard/tool_backend_test.dart index 1beb8916aad40..7353bf9d617d8 100644 --- a/packages/flutter_tools/test/integration.shard/tool_backend_test.dart +++ b/packages/flutter_tools/test/integration.shard/tool_backend_test.dart @@ -70,4 +70,25 @@ void main() { ), ); }); + + testWithoutContext('tool_backend.dart exits if local engine host does not match build mode', () async { + final ProcessResult result = await processManager.run(<String>[ + dart, + toolBackend, + 'linux-x64', + 'debug', + ], environment: <String, String>{ + 'PROJECT_DIR': examplePath, + 'LOCAL_ENGINE': 'debug_foo_bar', // OK + 'LOCAL_ENGINE_HOST': 'release_foo_bar', // Does not contain "debug", + }); + + expect( + result, + const ProcessResultMatcher( + exitCode: 1, + stderrPattern: "ERROR: Requested build with Flutter local engine host at 'release_foo_bar'", + ), + ); + }); } diff --git a/packages/flutter_tools/test/integration.shard/transition_test_utils.dart b/packages/flutter_tools/test/integration.shard/transition_test_utils.dart new file mode 100644 index 0000000000000..fa3a7f3b9e5b8 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/transition_test_utils.dart @@ -0,0 +1,345 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:meta/meta.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'test_utils.dart' show fileSystem; + +const ProcessManager processManager = LocalProcessManager(); +final String flutterRoot = getFlutterRoot(); +final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter'); + +void debugPrint(String message) { + // This is called to intentionally print debugging output when a test is + // either taking too long or has failed. + // ignore: avoid_print + print(message); +} + +typedef LineHandler = String? Function(String line); + +abstract class Transition { + const Transition({this.handler, this.logging}); + + /// Callback that is invoked when the transition matches. + /// + /// This should not throw, even if the test is failing. (For example, don't use "expect" + /// in these callbacks.) Throwing here would prevent the [runFlutter] function from running + /// to completion, which would leave zombie `flutter` processes around. + final LineHandler? handler; + + /// Whether to enable or disable logging when this transition is matched. + /// + /// The default value, null, leaves the logging state unaffected. + final bool? logging; + + bool matches(String line); + + @protected + bool lineMatchesPattern(String line, Pattern pattern) { + if (pattern is String) { + return line == pattern; + } + return line.contains(pattern); + } + + @protected + String describe(Pattern pattern) { + if (pattern is String) { + return '"$pattern"'; + } + if (pattern is RegExp) { + return '/${pattern.pattern}/'; + } + return '$pattern'; + } +} + +class Barrier extends Transition { + const Barrier(this.pattern, {super.handler, super.logging}); + final Pattern pattern; + + @override + bool matches(String line) => lineMatchesPattern(line, pattern); + + @override + String toString() => describe(pattern); +} + +class Multiple extends Transition { + Multiple( + List<Pattern> patterns, { + super.handler, + super.logging, + }) : _originalPatterns = patterns, + patterns = patterns.toList(); + + final List<Pattern> _originalPatterns; + final List<Pattern> patterns; + + @override + bool matches(String line) { + for (int index = 0; index < patterns.length; index += 1) { + if (lineMatchesPattern(line, patterns[index])) { + patterns.removeAt(index); + break; + } + } + return patterns.isEmpty; + } + + @override + String toString() { + if (patterns.isEmpty) { + return '${_originalPatterns.map(describe).join(', ')} (all matched)'; + } + return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)'; + } +} + +class LogLine { + const LogLine(this.channel, this.stamp, this.message); + final String channel; + final String stamp; + final String message; + + bool get couldBeCrash => + message.contains('Oops; flutter has exited unexpectedly:'); + + @override + String toString() => '$stamp $channel: $message'; + + void printClearly() { + debugPrint('$stamp $channel: ${clarify(message)}'); + } + + static String clarify(String line) { + return line.runes.map<String>((int rune) { + if (rune >= 0x20 && rune <= 0x7F) { + return String.fromCharCode(rune); + } + switch (rune) { + case 0x00: + return '<NUL>'; + case 0x07: + return '<BEL>'; + case 0x08: + return '<TAB>'; + case 0x09: + return '<BS>'; + case 0x0A: + return '<LF>'; + case 0x0D: + return '<CR>'; + } + return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>'; + }).join(); + } +} + +class ProcessTestResult { + const ProcessTestResult(this.exitCode, this.logs); + final int exitCode; + final List<LogLine> logs; + + List<String> get stdout { + return logs + .where((LogLine log) => log.channel == 'stdout') + .map<String>((LogLine log) => log.message) + .toList(); + } + + List<String> get stderr { + return logs + .where((LogLine log) => log.channel == 'stderr') + .map<String>((LogLine log) => log.message) + .toList(); + } + + @override + String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n'; +} + +Future<ProcessTestResult> runFlutter( + List<String> arguments, + String workingDirectory, + List<Transition> transitions, { + bool debug = false, + bool logging = true, + Duration expectedMaxDuration = const Duration( + minutes: + 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml. +}) async { + const LocalPlatform platform = LocalPlatform(); + final Stopwatch clock = Stopwatch()..start(); + final Process process = await processManager.start( + <String>[ + // In a container with no X display, use the virtual framebuffer. + if (platform.isLinux && (platform.environment['DISPLAY'] ?? '').isEmpty) '/usr/bin/xvfb-run', + flutterBin, + ...arguments, + ], + workingDirectory: workingDirectory, + ); + final List<LogLine> logs = <LogLine>[]; + int nextTransition = 0; + void describeStatus() { + if (transitions.isNotEmpty) { + debugPrint('Expected state transitions:'); + for (int index = 0; index < transitions.length; index += 1) { + debugPrint('${index.toString().padLeft(5)} ' + '${index < nextTransition ? 'ALREADY MATCHED ' : index == nextTransition ? 'NOW WAITING FOR>' : ' '} ${transitions[index]}'); + } + } + if (logs.isEmpty) { + debugPrint( + 'So far nothing has been logged${debug ? "" : "; use debug:true to print all output"}.'); + } else { + debugPrint( + 'Log${debug ? "" : " (only contains logged lines; use debug:true to print all output)"}:'); + for (final LogLine log in logs) { + log.printClearly(); + } + } + } + + bool streamingLogs = false; + Timer? timeout; + void processTimeout() { + if (!streamingLogs) { + streamingLogs = true; + if (!debug) { + debugPrint( + 'Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).'); + } + describeStatus(); + debugPrint('(streaming all logs from this point on...)'); + } else { + debugPrint('(taking a long time...)'); + } + } + + String stamp() => + '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]'; + void processStdout(String line) { + final LogLine log = LogLine('stdout', stamp(), line); + if (logging) { + logs.add(log); + } + if (streamingLogs) { + log.printClearly(); + } + if (nextTransition < transitions.length && + transitions[nextTransition].matches(line)) { + if (streamingLogs) { + debugPrint('(matched ${transitions[nextTransition]})'); + } + if (transitions[nextTransition].logging != null) { + if (!logging && transitions[nextTransition].logging!) { + logs.add(log); + } + logging = transitions[nextTransition].logging!; + if (streamingLogs) { + if (logging) { + debugPrint('(enabled logging)'); + } else { + debugPrint('(disabled logging)'); + } + } + } + if (transitions[nextTransition].handler != null) { + final String? command = transitions[nextTransition].handler!(line); + if (command != null) { + final LogLine inLog = LogLine('stdin', stamp(), command); + logs.add(inLog); + if (streamingLogs) { + inLog.printClearly(); + } + process.stdin.write(command); + } + } + nextTransition += 1; + timeout?.cancel(); + timeout = Timer(expectedMaxDuration ~/ 5, + processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. + } + } + + void processStderr(String line) { + final LogLine log = LogLine('stdout', stamp(), line); + logs.add(log); + if (streamingLogs) { + log.printClearly(); + } + } + + if (debug) { + processTimeout(); + } else { + timeout = Timer(expectedMaxDuration ~/ 2, + processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. + } + process.stdout + .transform<String>(utf8.decoder) + .transform<String>(const LineSplitter()) + .listen(processStdout); + process.stderr + .transform<String>(utf8.decoder) + .transform<String>(const LineSplitter()) + .listen(processStderr); + unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { + // This is a failure timeout, must not be short. + debugPrint( + '${stamp()} (process is not quitting, trying to send a "q" just in case that helps)'); + debugPrint('(a functional test should never reach this point)'); + final LogLine inLog = LogLine('stdin', stamp(), 'q'); + logs.add(inLog); + if (streamingLogs) { + inLog.printClearly(); + } + process.stdin.write('q'); + return -1; // discarded + }).then( + (int i) => i, + onError: (Object error) { + // ignore errors here, they will be reported on the next line + return -1; // discarded + }, + )); + final int exitCode = await process.exitCode; + if (streamingLogs) { + debugPrint('${stamp()} (process terminated with exit code $exitCode)'); + } + timeout?.cancel(); + if (nextTransition < transitions.length) { + debugPrint( + 'The subprocess terminated before all the expected transitions had been matched.'); + if (logs.any((LogLine line) => line.couldBeCrash)) { + debugPrint( + 'The subprocess may in fact have crashed. Check the stderr logs below.'); + } + debugPrint( + 'The transition that we were hoping to see next but that we never saw was:'); + debugPrint( + '${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}'); + if (!streamingLogs) { + describeStatus(); + debugPrint('(process terminated with exit code $exitCode)'); + } + throw TestFailure('Missed some expected transitions.'); + } + if (streamingLogs) { + debugPrint('${stamp()} (completed execution successfully!)'); + } + return ProcessTestResult(exitCode, logs); +} + +const int progressMessageWidth = 64; diff --git a/packages/flutter_tools/test/integration.shard/vmservice_integration_test.dart b/packages/flutter_tools/test/integration.shard/vmservice_integration_test.dart index 7768150930a00..45a0e86ebd89f 100644 --- a/packages/flutter_tools/test/integration.shard/vmservice_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/vmservice_integration_test.dart @@ -59,12 +59,6 @@ void main() { expect(response.type, 'Success'); }); - testWithoutContext('flutterGetIOSBuildOptions can be called', () async { - final Response response = - await vmService.callServiceExtension('s0.flutterGetIOSBuildOptions'); - expect(response.type, 'Success'); - }); - testWithoutContext('reloadSources can be called', () async { final VM vm = await vmService.getVM(); final IsolateRef? isolateRef = vm.isolates?.first; diff --git a/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart b/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart index 47c07e70bd0e7..f3907b530fccd 100644 --- a/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart +++ b/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/globals.dart' as globals; import '../src/common.dart'; import '../src/context.dart'; import '../src/test_flutter_command_runner.dart'; +import 'test_utils.dart'; void main() { late Directory tempDir; @@ -275,7 +276,7 @@ Future<void> _ensureFlutterToolsSnapshot() async { printOnFailure('Output of dart ${snapshotArgs.join(" ")}:'); printOnFailure(snapshotResult.stdout.toString()); printOnFailure(snapshotResult.stderr.toString()); - expect(snapshotResult.exitCode, 0); + expect(snapshotResult, const ProcessResultMatcher()); } Future<void> _restoreFlutterToolsSnapshot() async { @@ -415,10 +416,7 @@ Future<void> _analyzeEntity(FileSystemEntity target) async { args, workingDirectory: target is Directory ? target.path : target.dirname, ); - printOnFailure('Output of flutter analyze:'); - printOnFailure(exec.stdout.toString()); - printOnFailure(exec.stderr.toString()); - expect(exec.exitCode, 0); + expect(exec, const ProcessResultMatcher()); } Future<void> _buildWebProject(Directory workingDir) async { @@ -445,17 +443,14 @@ Future<void> _runFlutterSnapshot(List<String> flutterCommandArgs, Directory work ); final List<String> args = <String>[ + globals.artifacts!.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript), flutterToolsSnapshotPath, ...flutterCommandArgs ]; - final ProcessResult exec = await Process.run( - globals.artifacts!.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript), + final ProcessResult exec = await globals.processManager.run( args, workingDirectory: workingDir.path, ); - printOnFailure('Output of flutter ${flutterCommandArgs.join(" ")}:'); - printOnFailure(exec.stdout.toString()); - printOnFailure(exec.stderr.toString()); - expect(exec.exitCode, 0); + expect(exec, const ProcessResultMatcher()); } diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart index 109eb0ef6d338..2b1624d6a9bf9 100644 --- a/packages/flutter_tools/test/src/android_common.dart +++ b/packages/flutter_tools/test/src/android_common.dart @@ -41,10 +41,11 @@ class FakeAndroidBuilder implements AndroidBuilder { Future<List<String>> getBuildVariants({required FlutterProject project}) async => const <String>[]; @override - Future<List<String>> getAppLinkDomainsForVariant(String buildVariant, {required FlutterProject project}) async => const <String>[]; + Future<void> outputsAppLinkSettings( + String buildVariant, { + required FlutterProject project, + }) async {} - @override - Future<String> getApplicationIdForVariant(String buildVariant, {required FlutterProject project}) async => ''; } /// Creates a [FlutterProject] in a directory named [flutter_project] diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index e97fb955e2563..ca005f21e521a 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -17,6 +17,7 @@ import 'package:path/path.dart' as path; // flutter_ignore: package_path_import import 'package:test/test.dart' as test_package show test; import 'package:test/test.dart' hide test; +export 'package:path/path.dart' show Context; // flutter_ignore: package_path_import export 'package:test/test.dart' hide isInstanceOf, test; void tryToDelete(FileSystemEntity fileEntity) { diff --git a/packages/flutter_tools/test/src/fake_http_client.dart b/packages/flutter_tools/test/src/fake_http_client.dart index eea9b6ae9cd89..e21db1cd399c9 100644 --- a/packages/flutter_tools/test/src/fake_http_client.dart +++ b/packages/flutter_tools/test/src/fake_http_client.dart @@ -147,7 +147,7 @@ class FakeHttpClient implements HttpClient { bool Function(X509Certificate cert, String host, int port)? badCertificateCallback; @override - Function(String line)? keyLog; + void Function(String line)? keyLog; @override void close({bool force = false}) { } @@ -341,7 +341,7 @@ class _FakeHttpClientRequest implements HttpClientRequest { }); await completer.future; if (_responseError != null) { - return Future<HttpClientResponse>.error(_responseError!); + return Future<HttpClientResponse>.error(_responseError); } return _FakeHttpClientResponse(_response); } diff --git a/packages/flutter_tools/test/src/fake_process_manager.dart b/packages/flutter_tools/test/src/fake_process_manager.dart index 5486b959e5698..305a37cbf968f 100644 --- a/packages/flutter_tools/test/src/fake_process_manager.dart +++ b/packages/flutter_tools/test/src/fake_process_manager.dart @@ -213,10 +213,17 @@ class FakeProcess implements io.Process { /// The raw byte content of stdout. final List<int> _stdout; + /// The list of [kill] signals this process received so far. + @visibleForTesting + List<io.ProcessSignal> get signals => _signals; + final List<io.ProcessSignal> _signals = <io.ProcessSignal>[]; + @override bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) { + _signals.add(signal); + // Killing a fake process has no effect. - return false; + return true; } } @@ -400,8 +407,9 @@ abstract class FakeProcessManager implements ProcessManager { if (fakeProcess == null) { return false; } + fakeProcess.kill(signal); if (fakeProcess._completer != null) { - fakeProcess._completer!.complete(); + fakeProcess._completer.complete(); } return true; } diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 65fb5578cf185..9f3bb754271db 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -253,8 +253,20 @@ class FakeStdio extends Stdio { class FakeStdin extends Fake implements Stdin { final StreamController<List<int>> controller = StreamController<List<int>>(); + void Function(bool mode)? echoModeCallback; + + bool _echoMode = true; + + @override + bool get echoMode => _echoMode; + @override - bool echoMode = true; + set echoMode(bool mode) { + _echoMode = mode; + if (echoModeCallback != null) { + echoModeCallback!(mode); + } + } @override bool lineMode = true; @@ -443,12 +455,13 @@ class TestFeatureFlags implements FeatureFlags { this.isMacOSEnabled = false, this.isWebEnabled = false, this.isWindowsEnabled = false, - this.isSingleWidgetReloadEnabled = false, this.isAndroidEnabled = true, this.isIOSEnabled = true, this.isFuchsiaEnabled = false, this.areCustomDevicesEnabled = false, this.isFlutterWebWasmEnabled = false, + this.isCliAnimationEnabled = true, + this.isNativeAssetsEnabled = false, }); @override @@ -463,9 +476,6 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowsEnabled; - @override - final bool isSingleWidgetReloadEnabled; - @override final bool isAndroidEnabled; @@ -481,6 +491,12 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isFlutterWebWasmEnabled; + @override + final bool isCliAnimationEnabled; + + @override + final bool isNativeAssetsEnabled; + @override bool isEnabled(Feature feature) { switch (feature) { @@ -492,8 +508,6 @@ class TestFeatureFlags implements FeatureFlags { return isMacOSEnabled; case flutterWindowsDesktopFeature: return isWindowsEnabled; - case singleWidgetReload: - return isSingleWidgetReloadEnabled; case flutterAndroidFeature: return isAndroidEnabled; case flutterIOSFeature: @@ -502,6 +516,10 @@ class TestFeatureFlags implements FeatureFlags { return isFuchsiaEnabled; case flutterCustomDevicesFeature: return areCustomDevicesEnabled; + case cliAnimation: + return isCliAnimationEnabled; + case nativeAssets: + return isNativeAssetsEnabled; } return false; } diff --git a/packages/flutter_tools/test/src/io.dart b/packages/flutter_tools/test/src/io.dart index 43cf2689e4b1f..db4eb37e7fc33 100644 --- a/packages/flutter_tools/test/src/io.dart +++ b/packages/flutter_tools/test/src/io.dart @@ -27,7 +27,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.createDirectory(path); } - return _fileSystemDelegate!.directory(path); + return _fileSystemDelegate.directory(path); } @override @@ -35,7 +35,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.createFile(path); } - return _fileSystemDelegate!.file(path); + return _fileSystemDelegate.file(path); } @override @@ -43,7 +43,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.createLink(path); } - return _fileSystemDelegate!.link(path); + return _fileSystemDelegate.link(path); } @override @@ -51,7 +51,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fsWatch(path, events, recursive); } - return _fileSystemDelegate!.file(path).watch(events: events, recursive: recursive); + return _fileSystemDelegate.file(path).watch(events: events, recursive: recursive); } @override @@ -59,7 +59,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fsWatchIsSupported(); } - return _fileSystemDelegate!.isWatchSupported; + return _fileSystemDelegate.isWatchSupported; } @override @@ -67,7 +67,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fseGetType(path, followLinks); } - return _fileSystemDelegate!.type(path, followLinks: followLinks); + return _fileSystemDelegate.type(path, followLinks: followLinks); } @override @@ -75,7 +75,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fseGetTypeSync(path, followLinks); } - return _fileSystemDelegate!.typeSync(path, followLinks: followLinks); + return _fileSystemDelegate.typeSync(path, followLinks: followLinks); } @override @@ -83,7 +83,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fseIdentical(path1, path2); } - return _fileSystemDelegate!.identical(path1, path2); + return _fileSystemDelegate.identical(path1, path2); } @override @@ -91,7 +91,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.fseIdenticalSync(path1, path2); } - return _fileSystemDelegate!.identicalSync(path1, path2); + return _fileSystemDelegate.identicalSync(path1, path2); } @override @@ -99,7 +99,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.getCurrentDirectory(); } - return _fileSystemDelegate!.currentDirectory; + return _fileSystemDelegate.currentDirectory; } @override @@ -107,7 +107,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.getSystemTempDirectory(); } - return _fileSystemDelegate!.systemTempDirectory; + return _fileSystemDelegate.systemTempDirectory; } @override @@ -115,7 +115,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.setCurrentDirectory(path); } - _fileSystemDelegate!.currentDirectory = path; + _fileSystemDelegate.currentDirectory = path; } @override @@ -123,7 +123,7 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.stat(path); } - return _fileSystemDelegate!.stat(path); + return _fileSystemDelegate.stat(path); } @override @@ -131,6 +131,6 @@ class FlutterIOOverrides extends io.IOOverrides { if (_fileSystemDelegate == null) { return super.statSync(path); } - return _fileSystemDelegate!.statSync(path); + return _fileSystemDelegate.statSync(path); } } diff --git a/packages/flutter_tools/test/src/test_build_system.dart b/packages/flutter_tools/test/src/test_build_system.dart index 88e0db9524e24..e47e98c65e53a 100644 --- a/packages/flutter_tools/test/src/test_build_system.dart +++ b/packages/flutter_tools/test/src/test_build_system.dart @@ -34,13 +34,13 @@ class TestBuildSystem implements BuildSystem { @override Future<BuildResult> build(Target target, Environment environment, {BuildSystemConfig buildSystemConfig = const BuildSystemConfig()}) async { if (_onRun != null) { - _onRun?.call(target, environment); + _onRun.call(target, environment); } if (_exception != null) { - throw _exception!; + throw _exception; } if (_singleResult != null) { - return _singleResult!; + return _singleResult; } if (_nextResult >= _results.length) { throw StateError('Unexpected build request of ${target.name}'); @@ -51,13 +51,13 @@ class TestBuildSystem implements BuildSystem { @override Future<BuildResult> buildIncremental(Target target, Environment environment, BuildResult? previousBuild) async { if (_onRun != null) { - _onRun?.call(target, environment); + _onRun.call(target, environment); } if (_exception != null) { - throw _exception!; + throw _exception; } if (_singleResult != null) { - return _singleResult!; + return _singleResult; } if (_nextResult >= _results.length) { throw StateError('Unexpected buildIncremental request of ${target.name}'); diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index ba84512124625..d46cf36c9b0ec 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -126,7 +126,7 @@ class Testbed { body: () async { Cache.flutterRoot = ''; if (_setup != null) { - await _setup?.call(); + await _setup.call(); } await test(); Cache.flutterRoot = originalFlutterRoot; diff --git a/packages/flutter_tools/test/web.shard/chrome_test.dart b/packages/flutter_tools/test/web.shard/chrome_test.dart index dacc69eceddc5..f9611f4e7ee0c 100644 --- a/packages/flutter_tools/test/web.shard/chrome_test.dart +++ b/packages/flutter_tools/test/web.shard/chrome_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io' as io; +import 'package:fake_async/fake_async.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -16,7 +18,7 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../src/common.dart'; import '../src/fake_process_manager.dart'; -import '../src/fakes.dart'; +import '../src/fakes.dart' hide FakeProcess; const List<String> kChromeArgs = <String>[ '--disable-background-timer-throttling', @@ -44,6 +46,7 @@ void main() { late Platform platform; late FakeProcessManager processManager; late OperatingSystemUtils operatingSystemUtils; + late BufferLogger testLogger; setUp(() { exceptionHandler = FileExceptionHandler(); @@ -59,29 +62,86 @@ void main() { processManager: processManager, operatingSystemUtils: operatingSystemUtils, browserFinder: findChromeExecutable, - logger: BufferLogger.test(), + logger: testLogger = BufferLogger.test(), ); }); + Future<Chromium> testLaunchChrome(String userDataDir, FakeProcessManager processManager, ChromiumLauncher chromeLauncher) { + if (testLogger.isVerbose) { + processManager.addCommand(const FakeCommand( + command: <String>[ + 'example_chrome', + '--version', + ], + stdout: 'Chromium 115', + )); + } + + processManager.addCommand(FakeCommand( + command: <String>[ + 'example_chrome', + '--user-data-dir=$userDataDir', + '--remote-debugging-port=12345', + ...kChromeArgs, + 'example_url', + ], + stderr: kDevtoolsStderr, + )); + + return chromeLauncher.launch( + 'example_url', + skipCheck: true, + ); + } + testWithoutContext('can launch chrome and connect to the devtools', () async { await expectReturnsNormallyLater( - _testLaunchChrome( + testLaunchChrome( + '/.tmp_rand0/flutter_tools_chrome_device.rand0', + processManager, + chromeLauncher, + ) + ); + }); + + testWithoutContext('can launch chrome in verbose mode', () async { + chromeLauncher = ChromiumLauncher( + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + operatingSystemUtils: operatingSystemUtils, + browserFinder: findChromeExecutable, + logger: testLogger = BufferLogger.test(verbose: true), + ); + + await expectReturnsNormallyLater( + testLaunchChrome( '/.tmp_rand0/flutter_tools_chrome_device.rand0', processManager, chromeLauncher, ) ); + + expect( + testLogger.traceText.trim(), + 'Launching Chromium (url = example_url, headless = false, skipCheck = true, debugPort = null)\n' + 'Will use Chromium executable at example_chrome\n' + 'Using Chromium 115\n' + '[CHROME]: \n' + '[CHROME]: \n' + '[CHROME]: DevTools listening', + ); }); testWithoutContext('cannot have two concurrent instances of chrome', () async { - await _testLaunchChrome( + await testLaunchChrome( '/.tmp_rand0/flutter_tools_chrome_device.rand0', processManager, chromeLauncher, ); await expectToolExitLater( - _testLaunchChrome( + testLaunchChrome( '/.tmp_rand0/flutter_tools_chrome_device.rand1', processManager, chromeLauncher, @@ -91,7 +151,7 @@ void main() { }); testWithoutContext('can launch new chrome after stopping a previous chrome', () async { - final Chromium chrome = await _testLaunchChrome( + final Chromium chrome = await testLaunchChrome( '/.tmp_rand0/flutter_tools_chrome_device.rand0', processManager, chromeLauncher, @@ -99,7 +159,7 @@ void main() { await chrome.close(); await expectReturnsNormallyLater( - _testLaunchChrome( + testLaunchChrome( '/.tmp_rand0/flutter_tools_chrome_device.rand1', processManager, chromeLauncher, @@ -107,6 +167,116 @@ void main() { ); }); + testWithoutContext('exits normally using SIGTERM', () async { + final BufferLogger logger = BufferLogger.test(); + final FakeAsync fakeAsync = FakeAsync(); + + fakeAsync.run((_) { + () async { + final FakeChromeConnection chromeConnection = FakeChromeConnection(maxRetries: 4); + final ChromiumLauncher chromiumLauncher = ChromiumLauncher( + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + operatingSystemUtils: operatingSystemUtils, + browserFinder: findChromeExecutable, + logger: logger, + ); + + final FakeProcess process = FakeProcess( + duration: const Duration(seconds: 3), + ); + + final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); + + final Future<void> closeFuture = chrome.close(); + fakeAsync.elapse(const Duration(seconds: 4)); + await closeFuture; + + expect(process.signals, <io.ProcessSignal>[io.ProcessSignal.sigterm]); + }(); + }); + + fakeAsync.flushTimers(); + expect(logger.warningText, isEmpty); + }); + + testWithoutContext('falls back to SIGKILL if SIGTERM did not work', () async { + final BufferLogger logger = BufferLogger.test(); + final FakeAsync fakeAsync = FakeAsync(); + + fakeAsync.run((_) { + () async { + final FakeChromeConnection chromeConnection = FakeChromeConnection(maxRetries: 4); + final ChromiumLauncher chromiumLauncher = ChromiumLauncher( + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + operatingSystemUtils: operatingSystemUtils, + browserFinder: findChromeExecutable, + logger: logger, + ); + + final FakeProcess process = FakeProcess( + duration: const Duration(seconds: 6), + ); + + final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); + + final Future<void> closeFuture = chrome.close(); + fakeAsync.elapse(const Duration(seconds: 7)); + await closeFuture; + + expect(process.signals, <io.ProcessSignal>[io.ProcessSignal.sigterm, io.ProcessSignal.sigkill]); + }(); + }); + + fakeAsync.flushTimers(); + expect( + logger.warningText, + 'Failed to exit Chromium (pid: 1234) using SIGTERM. Will try sending SIGKILL instead.\n', + ); + }); + + testWithoutContext('falls back to a warning if SIGKILL did not work', () async { + final BufferLogger logger = BufferLogger.test(); + final FakeAsync fakeAsync = FakeAsync(); + + fakeAsync.run((_) { + () async { + final FakeChromeConnection chromeConnection = FakeChromeConnection(maxRetries: 4); + final ChromiumLauncher chromiumLauncher = ChromiumLauncher( + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + operatingSystemUtils: operatingSystemUtils, + browserFinder: findChromeExecutable, + logger: logger, + ); + + final FakeProcess process = FakeProcess( + duration: const Duration(seconds: 20), + ); + + final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); + + final Future<void> closeFuture = chrome.close(); + fakeAsync.elapse(const Duration(seconds: 30)); + await closeFuture; + expect(process.signals, <io.ProcessSignal>[io.ProcessSignal.sigterm, io.ProcessSignal.sigkill]); + }(); + }); + + fakeAsync.flushTimers(); + expect( + logger.warningText, + 'Failed to exit Chromium (pid: 1234) using SIGTERM. Will try sending SIGKILL instead.\n' + 'Failed to exit Chromium (pid: 1234) using SIGKILL. Giving up. Will continue, assuming ' + 'Chromium has exited successfully, but it is possible that this left a dangling Chromium ' + 'process running on the system.\n', + ); + }); + testWithoutContext('does not crash if saving profile information fails due to a file system exception.', () async { final BufferLogger logger = BufferLogger.test(); chromeLauncher = ChromiumLauncher( @@ -598,7 +768,8 @@ void main() { browserFinder: findChromeExecutable, logger: logger, ); - final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher); + final FakeProcess process = FakeProcess(); + final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); expect(await chromiumLauncher.connect(chrome, false), equals(chrome)); expect(logger.errorText, isEmpty); }); @@ -614,7 +785,8 @@ void main() { browserFinder: findChromeExecutable, logger: logger, ); - final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher); + final FakeProcess process = FakeProcess(); + final Chromium chrome = Chromium(0, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger); await expectToolExitLater( chromiumLauncher.connect(chrome, false), allOf( @@ -630,24 +802,6 @@ void main() { }); } -Future<Chromium> _testLaunchChrome(String userDataDir, FakeProcessManager processManager, ChromiumLauncher chromeLauncher) { - processManager.addCommand(FakeCommand( - command: <String>[ - 'example_chrome', - '--user-data-dir=$userDataDir', - '--remote-debugging-port=12345', - ...kChromeArgs, - 'example_url', - ], - stderr: kDevtoolsStderr, - )); - - return chromeLauncher.launch( - 'example_url', - skipCheck: true, - ); -} - /// Fake chrome connection that fails to get tabs a few times. class FakeChromeConnection extends Fake implements ChromeConnection { diff --git a/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart b/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart index 427942a4a8d32..ff2b08c063538 100644 --- a/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart +++ b/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart @@ -34,8 +34,10 @@ void usePathUrlStrategy() { /// ```dart /// import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// -/// // Somewhere before calling `runApp()` do: -/// setUrlStrategy(PathUrlStrategy()); +/// void main() { +/// // Somewhere before calling `runApp()` do: +/// setUrlStrategy(PathUrlStrategy()); +/// } /// ``` class PathUrlStrategy extends ui_web.HashUrlStrategy { /// Creates an instance of [PathUrlStrategy]. @@ -44,6 +46,7 @@ class PathUrlStrategy extends ui_web.HashUrlStrategy { /// interactions. PathUrlStrategy([ super.platformLocation, + this.includeHash = false, ]) : _platformLocation = platformLocation, _basePath = stripTrailingSlash(extractPathname(checkBaseHref( platformLocation.getBaseHref(), @@ -52,9 +55,20 @@ class PathUrlStrategy extends ui_web.HashUrlStrategy { final ui_web.PlatformLocation _platformLocation; final String _basePath; + /// There were an issue with url #hash which disappears from URL on first start of the web application + /// This flag allows to preserve that hash and was introduced mainly to preserve backward compatibility + /// with existing applications that rely on a full match on the path. If someone navigates to + /// /profile or /profile#foo, they both will work without this flag otherwise /profile#foo won't match + /// with the /profile route name anymore because the hash became part of the path. + /// + /// This flag solves the edge cases when using auth provider which redirects back to the app with + /// token in redirect URL as /#access_token=bla_bla_bla + final bool includeHash; + @override String getPath() { - final String path = _platformLocation.pathname + _platformLocation.search; + final String? hash = includeHash ? _platformLocation.hash : null; + final String path = _platformLocation.pathname + _platformLocation.search + (hash ?? ''); if (_basePath.isNotEmpty && path.startsWith(_basePath)) { return ensureLeadingSlash(path.substring(_basePath.length)); } diff --git a/packages/flutter_web_plugins/lib/src/navigation_non_web/url_strategy.dart b/packages/flutter_web_plugins/lib/src/navigation_non_web/url_strategy.dart index f9622608bdac5..0e7aa1e994759 100644 --- a/packages/flutter_web_plugins/lib/src/navigation_non_web/url_strategy.dart +++ b/packages/flutter_web_plugins/lib/src/navigation_non_web/url_strategy.dart @@ -99,8 +99,10 @@ void usePathUrlStrategy() { /// ```dart /// import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// -/// // Somewhere before calling `runApp()` do: -/// setUrlStrategy(const HashUrlStrategy()); +/// void main() { +/// // Somewhere before calling `runApp()` do: +/// setUrlStrategy(const HashUrlStrategy()); +/// } /// ``` class HashUrlStrategy extends UrlStrategy { /// Creates an instance of [HashUrlStrategy]. @@ -117,13 +119,15 @@ class HashUrlStrategy extends UrlStrategy { /// ```dart /// import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// -/// // Somewhere before calling `runApp()` do: -/// setUrlStrategy(PathUrlStrategy()); +/// void main() { +/// // Somewhere before calling `runApp()` do: +/// setUrlStrategy(PathUrlStrategy()); +/// } /// ``` class PathUrlStrategy extends HashUrlStrategy { /// Creates an instance of [PathUrlStrategy]. /// /// The [PlatformLocation] parameter is useful for testing to mock out browser /// integrations. - const PathUrlStrategy([PlatformLocation? _]); + const PathUrlStrategy([PlatformLocation? _, bool __ = false,]); } diff --git a/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart b/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart index 00bd96a672670..d679e24caee0b 100644 --- a/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart +++ b/packages/flutter_web_plugins/lib/src/plugin_event_channel.dart @@ -15,10 +15,9 @@ import 'plugin_registry.dart'; /// channel sends a stream of events to the handler listening on the /// framework-side. /// -/// The channel [name] must not be null. If no [codec] is provided, then -/// [StandardMethodCodec] is used. If no [binaryMessenger] is provided, then -/// [pluginBinaryMessenger], which sends messages to the framework-side, -/// is used. +/// If no [codec] is provided, then [StandardMethodCodec] is used. If no +/// [binaryMessenger] is provided, then [pluginBinaryMessenger], which sends +/// messages to the framework-side, is used. /// /// Channels created using this class implement two methods for /// subscribing to the event stream. The methods use the encoding of @@ -37,8 +36,6 @@ import 'plugin_registry.dart'; /// subscribed are silently discarded. class PluginEventChannel<T> { /// Creates a new plugin event channel. - /// - /// The [name] and [codec] arguments must not be null. const PluginEventChannel( this.name, [ this.codec = const StandardMethodCodec(), @@ -46,13 +43,11 @@ class PluginEventChannel<T> { ]); /// The logical channel on which communication happens. - /// - /// This must not be null. final String name; /// The message codec used by this channel. /// - /// This must not be null. This defaults to [StandardMethodCodec]. + /// Defaults to [StandardMethodCodec]. final MethodCodec codec; /// The messenger used by this channel to send platform messages. diff --git a/packages/flutter_web_plugins/lib/src/plugin_registry.dart b/packages/flutter_web_plugins/lib/src/plugin_registry.dart index 78108b9699c39..a6ff602a43220 100644 --- a/packages/flutter_web_plugins/lib/src/plugin_registry.dart +++ b/packages/flutter_web_plugins/lib/src/plugin_registry.dart @@ -9,6 +9,12 @@ import 'dart:ui_web' as ui_web; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +// Examples can assume: +// import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +// import 'package:flutter/services.dart'; +// import 'dart:ui_web' as ui_web; +// void handleFrameworkMessage(String name, ByteData? data, PlatformMessageResponseCallback? callback) { } + /// A registrar for Flutter plugins implemented in Dart. /// /// Plugins for the web platform are implemented in Dart and are @@ -32,6 +38,11 @@ import 'package:flutter/services.dart'; /// final MyPlugin instance = MyPlugin(); /// channel.setMethodCallHandler(instance.handleMethodCall); /// } +/// +/// Future<dynamic> handleMethodCall(MethodCall call) async { +/// // ... +/// } +/// /// // ... /// } /// ``` diff --git a/packages/flutter_web_plugins/pubspec.yaml b/packages/flutter_web_plugins/pubspec.yaml index 15883816ca0f3..51e8f22aac920 100644 --- a/packages/flutter_web_plugins/pubspec.yaml +++ b/packages/flutter_web_plugins/pubspec.yaml @@ -3,18 +3,18 @@ description: Library to register Flutter Web plugins homepage: https://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -27,10 +27,10 @@ dev_dependencies: matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: b642 +# PUBSPEC CHECKSUM: dc9e diff --git a/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart b/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart index f5bfa160554bf..d9e0a4d453595 100644 --- a/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart +++ b/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart @@ -80,17 +80,34 @@ void main() { expect(strategy.getPath(), '/bar'); }); - test('gets path correctly in the presence of query params', () { + test('gets path correctly in the presence of query params and omits fragment if no flag specified', () { location.baseHref = 'https://example.com/foo/'; location.pathname = '/foo/bar'; final PathUrlStrategy strategy = PathUrlStrategy(location); + location.search = '?q=1'; + expect(strategy.getPath(), '/bar?q=1'); + + location.search = '?q=1&t=r'; + expect(strategy.getPath(), '/bar?q=1&t=r'); + + location.hash = '#fragment=1'; + expect(strategy.getPath(), '/bar?q=1&t=r'); + }); + + test('gets path correctly in the presence of query params and fragment', () { + location.baseHref = 'https://example.com/foo/'; + location.pathname = '/foo/bar'; + final PathUrlStrategy strategy = PathUrlStrategy(location, true); location.search = '?q=1'; expect(strategy.getPath(), '/bar?q=1'); location.search = '?q=1&t=r'; expect(strategy.getPath(), '/bar?q=1&t=r'); + + location.hash = '#fragment=1'; + expect(strategy.getPath(), '/bar?q=1&t=r#fragment=1'); }); test('empty route name is ok', () { diff --git a/packages/fuchsia_remote_debug_protocol/pubspec.yaml b/packages/fuchsia_remote_debug_protocol/pubspec.yaml index 35ca0afb66185..a6e3d721fbce5 100644 --- a/packages/fuchsia_remote_debug_protocol/pubspec.yaml +++ b/packages/fuchsia_remote_debug_protocol/pubspec.yaml @@ -4,26 +4,26 @@ description: Provides an API to test/debug Flutter applications on remote Fuchsi homepage: https://flutter.dev environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: process: 4.2.4 - vm_service: 11.7.1 + vm_service: 11.10.0 file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - platform: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: - test: 1.24.3 + test: 1.24.6 - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -47,20 +47,20 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: f780 +# PUBSPEC CHECKSUM: a1ad diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 7ba3a031c3fed..305fad32417f4 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -140,7 +140,7 @@ import 'package:integration_test/integration_test_driver_extended.dart'; Future<void> main() async { await integrationDriver( - onScreenshot: (String screenshotName, List<int> screenshotBytes) async { + onScreenshot: (String screenshotName, List<int> screenshotBytes, [Map<String, Object?>? args]) async { final File image = File('$screenshotName.png'); image.writeAsBytesSync(screenshotBytes); // Return false if the screenshot is invalid. diff --git a/packages/integration_test/android/build.gradle b/packages/integration_test/android/build.gradle index ba3e75cde3acb..2f8e2f988feda 100644 --- a/packages/integration_test/android/build.gradle +++ b/packages/integration_test/android/build.gradle @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -group 'com.example.integration_test' +group 'dev.flutter.plugins.integration_test' version '1.0-SNAPSHOT' buildscript { @@ -47,7 +47,8 @@ android { dependencies { // TODO(egarciad): These dependencies should not be added to release builds. // https://github.com/flutter/flutter/issues/56591 - api 'junit:junit:4.12' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:5.0.0' // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 api 'androidx.test:runner:1.2.0' diff --git a/packages/integration_test/android/gradle.properties b/packages/integration_test/android/gradle.properties index 8bd86f6805108..95b4763a84734 100644 --- a/packages/integration_test/android/gradle.properties +++ b/packages/integration_test/android/gradle.properties @@ -1 +1 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G diff --git a/packages/integration_test/android/src/main/AndroidManifest.xml b/packages/integration_test/android/src/main/AndroidManifest.xml index b362178b96554..76ada4d34cdad 100644 --- a/packages/integration_test/android/src/main/AndroidManifest.xml +++ b/packages/integration_test/android/src/main/AndroidManifest.xml @@ -3,5 +3,5 @@ Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="dev.flutter.integration_test"> + package="dev.flutter.integration_test"> </manifest> diff --git a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshot.java b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshot.java index e9494439d7017..5b376b0277be2 100644 --- a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshot.java +++ b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshot.java @@ -19,7 +19,11 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterFragment; +import io.flutter.embedding.android.FlutterFragmentActivity; import io.flutter.embedding.android.FlutterSurfaceView; import io.flutter.embedding.android.FlutterView; import io.flutter.plugin.common.MethodChannel; @@ -51,8 +55,15 @@ class FlutterDeviceScreenshot { * @return the Flutter view. */ @Nullable - private static FlutterView getFlutterView(@NonNull Activity activity) { - return (FlutterView)activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID); + @VisibleForTesting + public static FlutterView getFlutterView(@NonNull Activity activity) { + if (activity instanceof FlutterActivity) { + return (FlutterView)activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID); + } else if (activity instanceof FlutterFragmentActivity) { + return (FlutterView)activity.findViewById(FlutterFragment.FLUTTER_VIEW_ID); + } else { + return null; + } } /** @@ -110,7 +121,7 @@ static void revertFlutterImage(@NonNull Activity activity) { } } - // Handlers use to capture a view. + // Handlers used to capture a view. private static Handler backgroundHandler; private static Handler mainHandler; diff --git a/packages/integration_test/android/src/test/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshotTest.java b/packages/integration_test/android/src/test/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshotTest.java new file mode 100644 index 0000000000000..15dfad615ea7a --- /dev/null +++ b/packages/integration_test/android/src/test/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshotTest.java @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.plugins.integration_test; + +import androidx.test.runner.AndroidJUnitRunner; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Activity; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterFragment; +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.embedding.android.FlutterView; + +public class FlutterDeviceScreenshotTest extends AndroidJUnitRunner { + @Test + public void getFlutterView_returnsNullForNonFlutterActivity() { + Activity mockActivity = mock(Activity.class); + assertNull(FlutterDeviceScreenshot.getFlutterView(mockActivity)); + } + + @Test + public void getFlutterView_returnsFlutterViewForFlutterActivity() { + FlutterView mockFlutterView = mock(FlutterView.class); + FlutterActivity mockFlutterActivity = mock(FlutterActivity.class); + when(mockFlutterActivity.findViewById(FlutterActivity.FLUTTER_VIEW_ID)) + .thenReturn(mockFlutterView); + assertEquals( + FlutterDeviceScreenshot.getFlutterView(mockFlutterActivity), + mockFlutterView + ); + } + + @Test + public void getFlutterView_returnsFlutterViewForFlutterFragmentActivity() { + FlutterView mockFlutterView = mock(FlutterView.class); + FlutterFragmentActivity mockFlutterFragmentActivity = mock(FlutterFragmentActivity.class); + when(mockFlutterFragmentActivity.findViewById(FlutterFragment.FLUTTER_VIEW_ID)) + .thenReturn(mockFlutterView); + assertEquals( + FlutterDeviceScreenshot.getFlutterView(mockFlutterFragmentActivity), + mockFlutterView + ); + } +} diff --git a/packages/integration_test/example/android/gradle.properties b/packages/integration_test/example/android/gradle.properties index 53a43b8d74fff..d513e21975fc1 100644 --- a/packages/integration_test/example/android/gradle.properties +++ b/packages/integration_test/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/integration_test/example/android/project-integration_test.lockfile b/packages/integration_test/example/android/project-integration_test.lockfile index 83502ee6b2a0e..0b94d008cc840 100644 --- a/packages/integration_test/example/android/project-integration_test.lockfile +++ b/packages/integration_test/example/android/project-integration_test.lockfile @@ -82,6 +82,8 @@ javax.activation:javax.activation-api:1.2.0=lintClassPath javax.inject:javax.inject:1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=lintClassPath junit:junit:4.12=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath net.sf.jopt-simple:jopt-simple:4.9=lintClassPath net.sf.kxml:kxml2:2.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.apache.commons:commons-compress:1.12=lintClassPath @@ -118,6 +120,9 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=debugAndroidTestCompileClass org.jetbrains.trove4j:trove4j:20160824=lintClassPath org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jvnet.staxex:stax-ex:1.8=lintClassPath +org.mockito:mockito-core:5.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.mockito:mockito-inline:5.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.objenesis:objenesis:3.3=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:7.0=lintClassPath org.ow2.asm:asm-analysis:9.2=androidJacocoAnt org.ow2.asm:asm-commons:7.0=lintClassPath diff --git a/packages/integration_test/example/pubspec.yaml b/packages/integration_test/example/pubspec.yaml index 8512ef6481637..a69981d37190a 100644 --- a/packages/integration_test/example/pubspec.yaml +++ b/packages/integration_test/example/pubspec.yaml @@ -3,21 +3,21 @@ description: Demonstrates how to use the integration_test plugin. publish_to: 'none' environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' flutter: ">=1.6.7" dependencies: flutter: sdk: flutter - cupertino_icons: 1.0.5 + cupertino_icons: 1.0.6 characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: flutter_test: @@ -28,14 +28,14 @@ dev_dependencies: sdk: flutter integration_test_macos: path: ../integration_test_macos - test: 1.24.3 + test: 1.24.6 pedantic: 1.11.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - _fe_analyzer_shared: 61.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - analyzer: 5.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + _fe_analyzer_shared: 64.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -66,22 +66,22 @@ dev_dependencies: source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_core: 0.5.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.5.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - vm_service: 11.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 11.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - webkit_inspection_protocol: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: e4fc +# PUBSPEC CHECKSUM: 9f5c diff --git a/packages/integration_test/integration_test_macos/pubspec.yaml b/packages/integration_test/integration_test_macos/pubspec.yaml index 5628fc13372fa..535783476eb49 100644 --- a/packages/integration_test/integration_test_macos/pubspec.yaml +++ b/packages/integration_test/integration_test_macos/pubspec.yaml @@ -10,20 +10,20 @@ flutter: pluginClass: IntegrationTestPlugin environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: sdk: flutter characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: pedantic: 1.11.1 -# PUBSPEC CHECKSUM: 4387 +# PUBSPEC CHECKSUM: a2e0 diff --git a/packages/integration_test/lib/common.dart b/packages/integration_test/lib/common.dart index 8e8490a551d1a..e5307f924e2a7 100644 --- a/packages/integration_test/lib/common.dart +++ b/packages/integration_test/lib/common.dart @@ -107,11 +107,11 @@ class Response { /// Create a list of Strings from [_failureDetails]. List<String> _failureDetailsAsString() { final List<String> list = <String>[]; - if (_failureDetails == null || _failureDetails!.isEmpty) { + if (_failureDetails == null || _failureDetails.isEmpty) { return list; } - for (final Failure failure in _failureDetails!) { + for (final Failure failure in _failureDetails) { list.add(failure.toJson()); } diff --git a/packages/integration_test/lib/integration_test.dart b/packages/integration_test/lib/integration_test.dart index d46276d1bd6f1..811f1c244e3d0 100644 --- a/packages/integration_test/lib/integration_test.dart +++ b/packages/integration_test/lib/integration_test.dart @@ -100,10 +100,6 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab // under debug mode. static bool _firstRun = false; - /// Artificially changes the surface size to `size` on the Widget binding, - /// then flushes microtasks. - /// - /// Set to null to use the default surface size. @override Future<void> setSurfaceSize(Size? size) { return TestAsyncUtils.guard<void>(() async { @@ -117,12 +113,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab } @override - ViewConfiguration createViewConfiguration() { - final FlutterView view = platformDispatcher.implicitView!; - final double devicePixelRatio = view.devicePixelRatio; - final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio; + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + final FlutterView view = renderView.flutterView; + final Size? surfaceSize = view == platformDispatcher.implicitView ? _surfaceSize : null; return TestViewConfiguration.fromView( - size: size, + size: surfaceSize ?? view.physicalSize / view.devicePixelRatio, view: view, ); } @@ -331,9 +326,9 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab /// /// Future<void> main() { /// return integrationDriver( - /// responseDataCallback: (data) async { + /// responseDataCallback: (Map<String, dynamic>? data) async { /// if (data != null) { - /// for (var entry in data.entries) { + /// for (final MapEntry<String, dynamic> entry in data.entries) { /// print('Writing ${entry.key} to the disk.'); /// await writeResponseData( /// entry.value as Map<String, dynamic>, @@ -442,11 +437,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab Timeout defaultTestTimeout = Timeout.none; @override - void attachRootWidget(Widget rootWidget) { + Widget wrapWithDefaultView(Widget rootWidget) { // This is a workaround where screenshots of root widgets have incorrect // bounds. // TODO(jiahaog): Remove when https://github.com/flutter/flutter/issues/66006 is fixed. - super.attachRootWidget(RepaintBoundary(child: rootWidget)); + return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget)); } @override diff --git a/packages/integration_test/lib/integration_test_driver_extended.dart b/packages/integration_test/lib/integration_test_driver_extended.dart index d449d7df98936..d4226f4ecb261 100644 --- a/packages/integration_test/lib/integration_test_driver_extended.dart +++ b/packages/integration_test/lib/integration_test_driver_extended.dart @@ -20,13 +20,14 @@ import 'common.dart'; /// ```dart /// import 'dart:async'; /// +/// import 'package:flutter_driver/flutter_driver.dart'; /// import 'package:integration_test/integration_test_driver_extended.dart'; /// /// Future<void> main() async { /// final FlutterDriver driver = await FlutterDriver.connect(); /// await integrationDriver( /// driver: driver, -/// onScreenshot: (String screenshotName, List<int> screenshotBytes) async { +/// onScreenshot: (String name, List<int> image, [Map<String, Object?>? args]) async { /// return true; /// }, /// ); diff --git a/packages/integration_test/pubspec.yaml b/packages/integration_test/pubspec.yaml index b50eb44ce5b62..7e352bd877f94 100644 --- a/packages/integration_test/pubspec.yaml +++ b/packages/integration_test/pubspec.yaml @@ -3,7 +3,7 @@ description: Runs tests that use the flutter_test API as integration tests. publish_to: none environment: - sdk: '>=3.0.0-0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: flutter: @@ -13,27 +13,27 @@ dependencies: flutter_test: sdk: flutter path: 1.8.3 - vm_service: 11.7.1 + vm_service: 11.10.0 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.16 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" material_color_utilities: 0.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - meta: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stack_trace: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - test_api: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.6.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web: 0.1.4-beta # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webdriver: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: @@ -45,4 +45,4 @@ flutter: ios: pluginClass: IntegrationTestPlugin -# PUBSPEC CHECKSUM: 3234 +# PUBSPEC CHECKSUM: 53b9